Compare commits

...

44 Commits

Author SHA1 Message Date
MilosPaunovic
4e1762de6c chore(core): properly handle dashboard header button on small screens
Closes https://github.com/kestra-io/kestra/issues/13838.
2025-12-30 08:56:26 +01:00
github-actions[bot]
8dd6f827c3 chore(core): localize to languages other than english (#13862)
Co-authored-by: GitHub Action <actions@github.com>
2025-12-29 17:26:11 +01:00
Piyush Bhaskar
8dff5855f2 feat(assets): introducing assets ui 2025-12-29 17:26:11 +01:00
Loïc Mathieu
156a75c668 feat(assets): continue Assets implementation 2025-12-29 17:26:11 +01:00
brian.mulier
9743f7fe56 feat(assets): introduce Assets 2025-12-29 17:26:11 +01:00
brian.mulier
b8a363d315 fix(core): JDBC Executor will consume executions from queue per executionId in order 2025-12-29 16:04:54 +01:00
Mustafa Tarek
bd582a5a45 fix(core): remove tenantId substring check on file path (#13778)
* fix(core): remove tenantId substring check on file path

* chore(core): extract namespace extraction from file URI to  function to facilitate testing

* feat(tests): add test coverage for namespace extraction from file URI

* chore(tests): remove redundant test added

* feat(tests): enhance assertions at namespaceFromURI() test
2025-12-29 15:28:37 +01:00
Georg Traar
a254de0d0d fix(ui): Drop "Administration" from Instance and Tenant (#13864) 2025-12-29 14:43:37 +01:00
Loïc Mathieu
1b74842485 feat(system): replace our SecurityManager by a Java agent
Part-of: https://github.com/kestra-io/kestra-ee/pull/6153
2025-12-29 14:23:22 +01:00
Nicolas K.
b178483dd3 feat(execution): normalize restart behavior (#13858)
Co-authored-by: nKwiatkowski <nkwiatkowski@kestra.io>
2025-12-29 14:01:42 +01:00
Miloš Paunović
7552bdb3a1 chore(core): add missing translation key/value pair (#13859) 2025-12-29 13:09:42 +01:00
Miloš Paunović
f1bdcd6662 refactor(executions): improve execution ts types (#13855) 2025-12-29 11:10:07 +01:00
ben8t
dc37112c1d chore(executions): rearrange sections on the overview page (#13853)
Co-authored-by: Miloš Paunović <paun992@hotmail.com>
2025-12-29 10:58:35 +01:00
Sumit Shandillya
4733f7eb8a chore(core): hide chart y-axis labels on small screens (#13850)
Closes https://github.com/kestra-io/kestra/issues/12754.

Signed-off-by: Sumitsh28 <sumit.off28@gmail.com>
Co-authored-by: Miloš Paunović <paun992@hotmail.com>
2025-12-29 10:54:19 +01:00
YannC
135a9fcdaa feat: allow to logs all files that are migrated (#13851) 2025-12-29 10:49:32 +01:00
dependabot[bot]
39b83b343c build(deps): bump com.github.oshi:oshi-core from 6.9.1 to 6.9.2
Bumps [com.github.oshi:oshi-core](https://github.com/oshi/oshi) from 6.9.1 to 6.9.2.
- [Release notes](https://github.com/oshi/oshi/releases)
- [Changelog](https://github.com/oshi/oshi/blob/master/CHANGELOG.md)
- [Commits](https://github.com/oshi/oshi/compare/oshi-parent-6.9.1...oshi-parent-6.9.2)

---
updated-dependencies:
- dependency-name: com.github.oshi:oshi-core
  dependency-version: 6.9.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-29 10:46:32 +01:00
dependabot[bot]
b169f4812a build(deps): bump org.sonarqube from 7.2.1.6560 to 7.2.2.6593
Bumps org.sonarqube from 7.2.1.6560 to 7.2.2.6593.

---
updated-dependencies:
- dependency-name: org.sonarqube
  dependency-version: 7.2.2.6593
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-29 10:46:04 +01:00
dependabot[bot]
b3b3e40e53 build(deps): bump software.amazon.awssdk:bom from 2.40.10 to 2.40.15
Bumps software.amazon.awssdk:bom from 2.40.10 to 2.40.15.

---
updated-dependencies:
- dependency-name: software.amazon.awssdk:bom
  dependency-version: 2.40.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-29 10:44:38 +01:00
Mustafa Tarek
506d7ae697 chore(core): Optimize lastAttempt() by avoiding stream reduction 2025-12-29 10:25:04 +01:00
yuri1969
fc3a0fbc33 chore(test): resolve Mockito JDK21 warning
https://javadoc.io/doc/org.mockito/mockito-core/latest/org.mockito/org/mockito/Mockito.html#0.3
2025-12-29 10:14:31 +01:00
Hritik Raj
0b55f5531b fix(execution): prevent NPE in SetVariables when variables are null (#13776)
* fix(execution): prevent NPE in SetVariables when variables are null

* fix(trigger): initialize execution variables for webhook triggers

* chore: minimize diff and use emptyMap for null execution variables

---------

Co-authored-by: Miloš Paunović <paun992@hotmail.com>
2025-12-29 10:05:27 +01:00
Georg Traar
16b6a82420 feat(ui): Seperate general and optional task properties in No Code Editor (#13799)
Co-authored-by: Georg Traar <georg@kestra.io>
Co-authored-by: Miloš Paunović <paun992@hotmail.com>
2025-12-29 09:29:15 +01:00
github-actions[bot]
2fcbf72b34 chore(core): localize to languages other than english (#13849)
Extended localization support by adding translations for multiple languages using English as the base. This enhances accessibility and usability for non-English-speaking users while keeping English as the source reference.

Co-authored-by: GitHub Action <actions@github.com>
2025-12-29 08:37:49 +01:00
Siddhant Sonawane
801e347b11 chore(executions): introduce translation key/value pairs for the new replay dialog (#13704)
Related to https://github.com/kestra-io/kestra/issues/13647.

Co-authored-by: MilosPaunovic <paun992@hotmail.com>
2025-12-29 08:34:59 +01:00
Miloš Paunović
83adc8ce57 fix(namespaces): prevent users from opening creation page on open source edition (#13841)
Closes https://github.com/kestra-io/kestra/issues/13110.
2025-12-26 13:14:22 +01:00
Miloš Paunović
cb31e48f4f chore(core): improve file organization in .github folder (#13836) 2025-12-25 16:00:11 +01:00
Miloš Paunović
a3f96a2741 chore(core): make left menu appear as overlay on small screens (#13834)
Closes https://github.com/kestra-io/kestra/issues/1425.
Closes https://github.com/kestra-io/kestra/issues/13502.
Closes https://github.com/kestra-io/kestra/issues/13503.
Closes https://github.com/kestra-io/kestra/issues/13504.
Closes https://github.com/kestra-io/kestra/issues/13505.
Closes https://github.com/kestra-io/kestra/issues/13506.
Closes https://github.com/kestra-io/kestra/issues/13507.
Closes https://github.com/kestra-io/kestra/issues/13508.
Closes https://github.com/kestra-io/kestra/issues/13510.
Closes https://github.com/kestra-io/kestra/issues/13511.
Closes https://github.com/kestra-io/kestra/issues/13512.
Closes https://github.com/kestra-io/kestra/issues/13513.
Closes https://github.com/kestra-io/kestra/issues/13514.
Closes https://github.com/kestra-io/kestra/issues/13516.
2025-12-25 14:29:56 +01:00
Miloš Paunović
5ca6fa8d77 fix(secrets): mark secret field required during creation (#13833)
Closes https://github.com/kestra-io/kestra-ee/issues/6209.
2025-12-25 10:54:55 +01:00
Miloš Paunović
a3a206f3c4 chore(core): polishing of templated blueprints ui (#13806)
Related to https://github.com/kestra-io/kestra-ee/pull/6201.

Closes https://github.com/kestra-io/kestra-ee/issues/6179.
2025-12-25 09:06:49 +01:00
aflahaa
31f1e505e3 refactor(core): remove usage of unnecessary i18n composable (#13826)
Closes https://github.com/kestra-io/kestra/issues/13350.

Co-authored-by: MilosPaunovic <paun992@hotmail.com>
2025-12-24 12:56:51 +01:00
YannC
75e0c1d11f fix: updated the package-lock.json after dependencies upgrade (#13825) 2025-12-24 10:43:09 +01:00
dependabot[bot]
4cf883877d build(deps): bump the minor group in /ui with 4 updates (#13821)
Bumps the minor group in /ui with 4 updates: [element-plus](https://github.com/element-plus/element-plus), [posthog-js](https://github.com/PostHog/posthog-js), [rolldown-vite](https://github.com/vitejs/rolldown-vite/tree/HEAD/packages/vite) and [vue-tsc](https://github.com/vuejs/language-tools/tree/HEAD/packages/tsc).


Updates `element-plus` from 2.12.0 to 2.13.0
- [Release notes](https://github.com/element-plus/element-plus/releases)
- [Changelog](https://github.com/element-plus/element-plus/blob/dev/CHANGELOG.en-US.md)
- [Commits](https://github.com/element-plus/element-plus/compare/2.12.0...2.13.0)

Updates `posthog-js` from 1.308.0 to 1.310.1
- [Release notes](https://github.com/PostHog/posthog-js/releases)
- [Changelog](https://github.com/PostHog/posthog-js/blob/main/CHANGELOG.md)
- [Commits](https://github.com/PostHog/posthog-js/compare/posthog-js@1.308.0...posthog-js@1.310.1)

Updates `rolldown-vite` from 7.2.11 to 7.3.0
- [Release notes](https://github.com/vitejs/rolldown-vite/releases)
- [Changelog](https://github.com/vitejs/rolldown-vite/blob/rolldown-vite/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/rolldown-vite/commits/v7.3.0/packages/vite)

Updates `vue-tsc` from 3.1.8 to 3.2.1
- [Release notes](https://github.com/vuejs/language-tools/releases)
- [Changelog](https://github.com/vuejs/language-tools/blob/master/CHANGELOG.md)
- [Commits](https://github.com/vuejs/language-tools/commits/v3.2.1/packages/tsc)

---
updated-dependencies:
- dependency-name: element-plus
  dependency-version: 2.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: posthog-js
  dependency-version: 1.310.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: rolldown-vite
  dependency-version: 7.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor
- dependency-name: vue-tsc
  dependency-version: 3.2.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Miloš Paunović <paun992@hotmail.com>
2025-12-24 08:53:14 +01:00
dependabot[bot]
87b1e8fb01 build(deps): bump the patch group in /ui with 7 updates (#13822)
Bumps the patch group in /ui with 7 updates:

| Package | From | To |
| --- | --- | --- |
| [@vue-flow/core](https://github.com/bcakmakoglu/vue-flow/tree/HEAD/packages/core) | `1.48.0` | `1.48.1` |
| [vue](https://github.com/vuejs/core) | `3.5.25` | `3.5.26` |
| [vue-i18n](https://github.com/intlify/vue-i18n/tree/HEAD/packages/vue-i18n) | `11.2.2` | `11.2.7` |
| [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) | `8.50.0` | `8.50.1` |
| [@vitejs/plugin-vue-jsx](https://github.com/vitejs/vite-plugin-vue/tree/HEAD/packages/plugin-vue-jsx) | `5.1.2` | `5.1.3` |
| [sass](https://github.com/sass/dart-sass) | `1.97.0` | `1.97.1` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.50.0` | `8.50.1` |


Updates `@vue-flow/core` from 1.48.0 to 1.48.1
- [Release notes](https://github.com/bcakmakoglu/vue-flow/releases)
- [Changelog](https://github.com/bcakmakoglu/vue-flow/blob/master/packages/core/CHANGELOG.md)
- [Commits](https://github.com/bcakmakoglu/vue-flow/commits/@vue-flow/core@1.48.1/packages/core)

Updates `vue` from 3.5.25 to 3.5.26
- [Release notes](https://github.com/vuejs/core/releases)
- [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/core/compare/v3.5.25...v3.5.26)

Updates `vue-i18n` from 11.2.2 to 11.2.7
- [Release notes](https://github.com/intlify/vue-i18n/releases)
- [Changelog](https://github.com/intlify/vue-i18n/blob/master/CHANGELOG.md)
- [Commits](https://github.com/intlify/vue-i18n/commits/v11.2.7/packages/vue-i18n)

Updates `@typescript-eslint/parser` from 8.50.0 to 8.50.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.50.1/packages/parser)

Updates `@vitejs/plugin-vue-jsx` from 5.1.2 to 5.1.3
- [Release notes](https://github.com/vitejs/vite-plugin-vue/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-vue/blob/main/packages/plugin-vue-jsx/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-vue/commits/plugin-vue@5.1.3/packages/plugin-vue-jsx)

Updates `sass` from 1.97.0 to 1.97.1
- [Release notes](https://github.com/sass/dart-sass/releases)
- [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sass/dart-sass/compare/1.97.0...1.97.1)

Updates `typescript-eslint` from 8.50.0 to 8.50.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.50.1/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: "@vue-flow/core"
  dependency-version: 1.48.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch
- dependency-name: vue
  dependency-version: 3.5.26
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch
- dependency-name: vue-i18n
  dependency-version: 11.2.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.50.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patch
- dependency-name: "@vitejs/plugin-vue-jsx"
  dependency-version: 5.1.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patch
- dependency-name: sass
  dependency-version: 1.97.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patch
- dependency-name: typescript-eslint
  dependency-version: 8.50.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-24 08:51:19 +01:00
dependabot[bot]
b5c6101090 build(deps): bump the build group in /ui with 6 updates (#13820)
---
updated-dependencies:
- dependency-name: "@rollup/rollup-darwin-arm64"
  dependency-version: 4.54.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: build
- dependency-name: "@rollup/rollup-darwin-x64"
  dependency-version: 4.54.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: build
- dependency-name: "@rollup/rollup-linux-x64-gnu"
  dependency-version: 4.54.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: build
- dependency-name: "@swc/core-darwin-arm64"
  dependency-version: 1.15.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: "@swc/core-darwin-x64"
  dependency-version: 1.15.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: "@swc/core-linux-x64-gnu"
  dependency-version: 1.15.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Miloš Paunović <paun992@hotmail.com>
2025-12-24 08:45:45 +01:00
dependabot[bot]
e4323728d6 build(deps-dev): bump storybook from 9.1.16 to 9.1.17 in /ui (#13761)
Bumps [storybook](https://github.com/storybookjs/storybook/tree/HEAD/code/core) from 9.1.16 to 9.1.17.
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v9.1.17/code/core)

---
updated-dependencies:
- dependency-name: storybook
  dependency-version: 9.1.17
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-24 08:44:29 +01:00
Roman Acevedo
5495971ecf test: add test env by default to be marked as test in ThreadUncaughtExceptionHandler.uncaughtException() 2025-12-23 22:18:48 +01:00
Roman Acevedo
dec1ee4272 test(cli): try to unflaky PluginDocCommandTest.run 2025-12-23 20:23:30 +01:00
Roman Acevedo
69cc6b2715 build(gradle): insure test report is always generated 2025-12-23 20:23:30 +01:00
Barthélémy Ledoux
431b4ccdb9 fix: make reorder of tests work (#13774)
* rename function

* fix: make reorder of tests work

---------

Co-authored-by: Miloš Paunović <paun992@hotmail.com>
2025-12-23 17:11:54 +01:00
Georg Traar
e9207a6f53 feat(ui): update onboarding CTA to product tour (#13803)
* feat(ui): update onboarding CTA to product tour

Extended localization support by adding translations for multiple languages using English as the base. This enhances accessibility and usability for non-English-speaking users while keeping English as the source reference.

Co-authored-by: Georg Traar <gtraar@kestra.io>
Co-authored-by: Miloš Paunović <paun992@hotmail.com>
2025-12-23 16:13:48 +01:00
Alankar Jagtap
419c1041d5 refactor(core): import moment as a library directly into the component (#13800)
Closes https://github.com/kestra-io/kestra/issues/12953.

Co-authored-by: MilosPaunovic <paun992@hotmail.com>
2025-12-23 13:46:45 +01:00
Karan Suresh
02cd5efb05 refactor(core): remove usage of unnecessary i18n composable (#13804)
Closes https://github.com/kestra-io/kestra/issues/13261.

Signed-off-by: Iam-karan-suresh <karansuresh.info@gmail.com>
Co-authored-by: MilosPaunovic <paun992@hotmail.com>
2025-12-23 13:37:15 +01:00
github-actions[bot]
2d549940c4 chore(core): localize to languages other than english (#13809)
Extended localization support by adding translations for multiple languages using English as the base. This enhances accessibility and usability for non-English-speaking users while keeping English as the source reference.

Co-authored-by: GitHub Action <actions@github.com>
2025-12-23 13:24:58 +01:00
Barthélémy Ledoux
97e138fbae fix: use injection instead of store (#13777)
Co-authored-by: Miloš Paunović <paun992@hotmail.com>
2025-12-23 13:23:14 +01:00
122 changed files with 3384 additions and 1831 deletions

View File

@@ -63,9 +63,9 @@ You can also build it from a terminal using `./gradlew build`, the Gradle wrappe
- Configure the following environment variables:
- `MICRONAUT_ENVIRONMENTS`: can be set to any string and will load a custom configuration file in `cli/src/main/resources/application-{env}.yml`.
- `KESTRA_PLUGINS_PATH`: is the path where you will save plugins as Jar and will be load on startup.
- See the screenshot below for an example: ![Intellij IDEA Configuration ](run-app.png)
- See the screenshot below for an example: ![Intellij IDEA Configuration ](./assets/run-app.png)
- If you encounter **JavaScript memory heap out** error during startup, configure `NODE_OPTIONS` environment variable with some large value.
- Example `NODE_OPTIONS: --max-old-space-size=4096` or `NODE_OPTIONS: --max-old-space-size=8192` ![Intellij IDEA Configuration ](node_option_env_var.png)
- Example `NODE_OPTIONS: --max-old-space-size=4096` or `NODE_OPTIONS: --max-old-space-size=8192` ![Intellij IDEA Configuration ](./assets/node_option_env_var.png)
- The server starts by default on port 8080 and is reachable on `http://localhost:8080`
If you want to launch all tests, you need Python and some packages installed on your machine, on Ubuntu you can install them with:

View File

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 130 KiB

View File

Before

Width:  |  Height:  |  Size: 210 KiB

After

Width:  |  Height:  |  Size: 210 KiB

View File

@@ -21,7 +21,7 @@ plugins {
// test
id "com.adarshr.test-logger" version "4.0.0"
id "org.sonarqube" version "7.2.1.6560"
id "org.sonarqube" version "7.2.2.6593"
id 'jacoco-report-aggregation'
// helper
@@ -185,6 +185,7 @@ subprojects {subProj ->
canBeResolved = true
canBeConsumed = true
}
mockitoAgent
}
dependencies {
@@ -205,6 +206,9 @@ subprojects {subProj ->
testImplementation "org.junit.jupiter:junit-jupiter-params"
testImplementation "org.junit-pioneer:junit-pioneer"
testImplementation 'org.mockito:mockito-junit-jupiter'
mockitoAgent("org.mockito:mockito-core:5.21.0") {
transitive = false // just the core
}
// hamcrest
testImplementation 'org.hamcrest:hamcrest'
@@ -222,6 +226,7 @@ subprojects {subProj ->
def commonTestConfig = { Test t ->
t.ignoreFailures = true
t.finalizedBy jacocoTestReport
// set Xmx for test workers
t.maxHeapSize = '4g'
@@ -328,7 +333,10 @@ subprojects {subProj ->
junitXml.outputLocation = layout.buildDirectory.dir("test-results/test")
}
commonTestConfig(t)
jvmArgs = ["-javaagent:${configurations.agent.singleFile}"]
jvmArgs = [
"-javaagent:${configurations.agent.singleFile}",
"-javaagent:${configurations.mockitoAgent.singleFile}"
]
}
tasks.named('check') {

View File

@@ -81,7 +81,7 @@ public class MetadataMigrationService {
}));
}
public void nsFilesMigration() throws IOException {
public void nsFilesMigration(boolean verbose) throws IOException {
this.namespacesPerTenant().entrySet().stream()
.flatMap(namespacesForTenant -> namespacesForTenant.getValue().stream().map(namespace -> Map.entry(namespacesForTenant.getKey(), namespace)))
.flatMap(throwFunction(namespaceForTenant -> {
@@ -92,6 +92,9 @@ public class MetadataMigrationService {
.forEach(throwConsumer(nsFileMetadata -> {
if (namespaceFileMetadataRepository.findByPath(nsFileMetadata.getTenantId(), nsFileMetadata.getNamespace(), nsFileMetadata.getPath()).isEmpty()) {
namespaceFileMetadataRepository.save(nsFileMetadata);
if (verbose) {
System.out.println("Migrated namespace file metadata: " + nsFileMetadata.getNamespace() + " - " + nsFileMetadata.getPath());
}
}
}));
}

View File

@@ -15,11 +15,14 @@ public class NsFilesMetadataMigrationCommand extends AbstractCommand {
@Inject
private Provider<MetadataMigrationService> metadataMigrationServiceProvider;
@CommandLine.Option(names = {"-lm", "--log-migrations"}, description = "Log all files that are migrated", defaultValue = "false")
public boolean logMigrations = false;
@Override
public Integer call() throws Exception {
super.call();
try {
metadataMigrationServiceProvider.get().nsFilesMigration();
metadataMigrationServiceProvider.get().nsFilesMigration(logMigrations);
} catch (Exception e) {
System.err.println("❌ Namespace Files Metadata migration failed: " + e.getMessage());
e.printStackTrace();

View File

@@ -4,7 +4,6 @@ import io.micronaut.configuration.picocli.PicocliRunner;
import io.micronaut.context.ApplicationContext;
import io.micronaut.context.env.Environment;
import org.apache.commons.io.FileUtils;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import java.io.File;
@@ -15,7 +14,6 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
@@ -25,7 +23,8 @@ class PluginDocCommandTest {
@Test
void run() throws IOException, URISyntaxException {
Path pluginsPath = Files.createTempDirectory(PluginListCommandTest.class.getSimpleName());
var testDirectoryName = PluginListCommandTest.class.getSimpleName();
Path pluginsPath = Files.createTempDirectory(testDirectoryName + "_pluginsPath_");
pluginsPath.toFile().deleteOnExit();
FileUtils.copyFile(
@@ -34,7 +33,7 @@ class PluginDocCommandTest {
new File(URI.create("file://" + pluginsPath.toAbsolutePath() + "/" + PLUGIN_TEMPLATE_TEST))
);
Path docPath = Files.createTempDirectory(PluginInstallCommandTest.class.getSimpleName());
Path docPath = Files.createTempDirectory(testDirectoryName + "_docPath_");
docPath.toFile().deleteOnExit();
try (ApplicationContext ctx = ApplicationContext.run(Environment.CLI, Environment.TEST)) {
@@ -43,9 +42,9 @@ class PluginDocCommandTest {
List<Path> files = Files.list(docPath).toList();
assertThat(files.size()).isEqualTo(1);
assertThat(files.getFirst().getFileName().toString()).isEqualTo("plugin-template-test");
var directory = files.getFirst().toFile();
assertThat(files.stream().map(path -> path.getFileName().toString())).contains("plugin-template-test");
// don't know why, but sometimes there is an addition "plugin-notifications" directory present
var directory = files.stream().filter(path -> "plugin-template-test".equals(path.getFileName().toString())).findFirst().get().toFile();
assertThat(directory.isDirectory()).isTrue();
assertThat(directory.listFiles().length).isEqualTo(3);

View File

@@ -24,6 +24,9 @@ dependencies {
// reactor
api "io.projectreactor:reactor-core"
// awaitility
api "org.awaitility:awaitility"
// micronaut
api "io.micronaut.data:micronaut-data-model"
implementation "io.micronaut:micronaut-http-server-netty"

View File

@@ -0,0 +1,25 @@
package io.kestra.core.assets;
import io.kestra.core.models.assets.Asset;
import io.kestra.core.runners.AssetEmitter;
import jakarta.inject.Singleton;
import java.util.ArrayList;
import java.util.List;
@Singleton
public class AssetManagerFactory {
public AssetEmitter of(boolean enabled) {
return new AssetEmitter() {
@Override
public void upsert(Asset asset) {
throw new UnsupportedOperationException();
}
@Override
public List<Asset> outputs() {
return new ArrayList<>();
}
};
}
}

View File

@@ -0,0 +1,31 @@
package io.kestra.core.assets;
import io.kestra.core.models.assets.Asset;
import io.kestra.core.models.assets.AssetIdentifier;
import io.kestra.core.models.assets.AssetUser;
import io.kestra.core.queues.QueueException;
import io.micronaut.context.annotation.Secondary;
import jakarta.inject.Singleton;
import java.util.List;
public interface AssetService {
void asyncUpsert(AssetUser assetUser, Asset asset) throws QueueException;
void assetLineage(AssetUser assetUser, List<AssetIdentifier> inputs, List<AssetIdentifier> outputs) throws QueueException;
@Singleton
@Secondary
class NoopAssetService implements AssetService {
@Override
public void asyncUpsert(AssetUser assetUser, Asset asset) throws QueueException {
// no-op
}
@Override
public void assetLineage(AssetUser assetUser, List<AssetIdentifier> inputs, List<AssetIdentifier> outputs) {
// no-op
}
}
}

View File

@@ -22,6 +22,7 @@ import com.github.victools.jsonschema.module.swagger2.Swagger2Module;
import com.google.common.collect.ImmutableMap;
import io.kestra.core.models.annotations.Plugin;
import io.kestra.core.models.annotations.PluginProperty;
import io.kestra.core.models.assets.Asset;
import io.kestra.core.models.conditions.Condition;
import io.kestra.core.models.conditions.ScheduleCondition;
import io.kestra.core.models.dashboards.DataFilter;
@@ -63,7 +64,7 @@ import static io.kestra.core.serializers.JacksonMapper.MAP_TYPE_REFERENCE;
@Singleton
@Slf4j
public class JsonSchemaGenerator {
private static final List<Class<?>> TYPES_RESOLVED_AS_STRING = List.of(Duration.class, LocalTime.class, LocalDate.class, LocalDateTime.class, ZonedDateTime.class, OffsetDateTime.class, OffsetTime.class);
private static final List<Class<?>> SUBTYPE_RESOLUTION_EXCLUSION_FOR_PLUGIN_SCHEMA = List.of(Task.class, AbstractTrigger.class);
@@ -276,10 +277,10 @@ public class JsonSchemaGenerator {
.with(Option.DEFINITION_FOR_MAIN_SCHEMA)
.with(Option.PLAIN_DEFINITION_KEYS)
.with(Option.ALLOF_CLEANUP_AT_THE_END);
// HACK: Registered a custom JsonUnwrappedDefinitionProvider prior to the JacksonModule
// HACK: Registered a custom JsonUnwrappedDefinitionProvider prior to the JacksonModule
// to be able to return an CustomDefinition with an empty node when the ResolvedType can't be found.
builder.forTypesInGeneral().withCustomDefinitionProvider(new JsonUnwrappedDefinitionProvider(){
builder.forTypesInGeneral().withCustomDefinitionProvider(new JsonUnwrappedDefinitionProvider() {
@Override
public CustomDefinition provideCustomSchemaDefinition(ResolvedType javaType, SchemaGenerationContext context) {
try {
@@ -321,7 +322,7 @@ public class JsonSchemaGenerator {
// inline some type
builder.forTypesInGeneral()
.withCustomDefinitionProvider(new CustomDefinitionProviderV2() {
@Override
public CustomDefinition provideCustomSchemaDefinition(ResolvedType javaType, SchemaGenerationContext context) {
if (javaType.isInstanceOf(Map.class) || javaType.isInstanceOf(Enum.class)) {
@@ -589,7 +590,8 @@ public class JsonSchemaGenerator {
// The `const` property is used by editors for auto-completion based on that schema.
builder.forTypesInGeneral().withTypeAttributeOverride((collectedTypeAttributes, scope, context) -> {
final Class<?> pluginType = scope.getType().getErasedType();
if (pluginType.getAnnotation(Plugin.class) != null) {
Plugin pluginAnnotation = pluginType.getAnnotation(Plugin.class);
if (pluginAnnotation != null) {
ObjectNode properties = (ObjectNode) collectedTypeAttributes.get("properties");
if (properties != null) {
properties.set("type", context.getGeneratorConfig().createObjectNode()
@@ -764,6 +766,14 @@ public class JsonSchemaGenerator {
consumer.accept(typeContext.resolve(clz));
}
}).toList();
} else if (declaredType.getErasedType() == Asset.class) {
return getRegisteredPlugins()
.stream()
.flatMap(registeredPlugin -> registeredPlugin.getAssets().stream())
.filter(p -> allowedPluginTypes.isEmpty() || allowedPluginTypes.contains(p.getName()))
.filter(Predicate.not(io.kestra.core.models.Plugin::isInternal))
.map(typeContext::resolve)
.toList();
}
return null;

View File

@@ -103,12 +103,48 @@ public record QueryFilter(
return List.of(Op.EQUALS, Op.NOT_EQUALS, Op.IN, Op.NOT_IN, Op.CONTAINS);
}
},
METADATA("metadata") {
@Override
public List<Op> supportedOp() {
return List.of(Op.EQUALS, Op.NOT_EQUALS, Op.IN, Op.NOT_IN, Op.CONTAINS);
}
},
FLOW_ID("flowId") {
@Override
public List<Op> supportedOp() {
return List.of(Op.EQUALS, Op.NOT_EQUALS, Op.CONTAINS, Op.STARTS_WITH, Op.ENDS_WITH, Op.REGEX, Op.IN, Op.NOT_IN, Op.PREFIX);
}
},
FLOW_REVISION("flowRevision") {
@Override
public List<Op> supportedOp() {
return List.of(Op.EQUALS, Op.NOT_EQUALS, Op.IN, Op.NOT_IN);
}
},
ID("id") {
@Override
public List<Op> supportedOp() {
return List.of(Op.EQUALS, Op.NOT_EQUALS, Op.CONTAINS, Op.STARTS_WITH, Op.ENDS_WITH, Op.REGEX);
}
},
ASSET_ID("assetId") {
@Override
public List<Op> supportedOp() {
return List.of(Op.EQUALS, Op.NOT_EQUALS, Op.CONTAINS, Op.STARTS_WITH, Op.ENDS_WITH, Op.REGEX);
}
},
TYPE("type") {
@Override
public List<Op> supportedOp() {
return List.of(Op.EQUALS, Op.NOT_EQUALS, Op.CONTAINS, Op.STARTS_WITH, Op.ENDS_WITH, Op.REGEX);
}
},
CREATED("created") {
@Override
public List<Op> supportedOp() {
return List.of(Op.GREATER_THAN_OR_EQUAL_TO, Op.GREATER_THAN, Op.LESS_THAN_OR_EQUAL_TO, Op.LESS_THAN, Op.EQUALS, Op.NOT_EQUALS);
}
},
UPDATED("updated") {
@Override
public List<Op> supportedOp() {
@@ -163,6 +199,18 @@ public record QueryFilter(
return List.of(Op.EQUALS, Op.NOT_EQUALS, Op.CONTAINS, Op.STARTS_WITH, Op.ENDS_WITH, Op.IN, Op.NOT_IN);
}
},
TASK_ID("taskId") {
@Override
public List<Op> supportedOp() {
return List.of(Op.EQUALS, Op.NOT_EQUALS, Op.CONTAINS, Op.STARTS_WITH, Op.ENDS_WITH, Op.IN, Op.NOT_IN);
}
},
TASK_RUN_ID("taskRunId") {
@Override
public List<Op> supportedOp() {
return List.of(Op.EQUALS, Op.NOT_EQUALS, Op.CONTAINS, Op.STARTS_WITH, Op.ENDS_WITH, Op.IN, Op.NOT_IN);
}
},
CHILD_FILTER("childFilter") {
@Override
public List<Op> supportedOp() {
@@ -312,6 +360,34 @@ public record QueryFilter(
Field.UPDATED
);
}
},
ASSET {
@Override
public List<Field> supportedField() {
return List.of(
Field.QUERY,
Field.ID,
Field.TYPE,
Field.NAMESPACE,
Field.METADATA,
Field.UPDATED
);
}
},
ASSET_USAGE {
@Override
public List<Field> supportedField() {
return List.of(
Field.ASSET_ID,
Field.NAMESPACE,
Field.FLOW_ID,
Field.FLOW_REVISION,
Field.EXECUTION_ID,
Field.TASK_ID,
Field.TASK_RUN_ID,
Field.CREATED
);
}
};
public abstract List<Field> supportedField();

View File

@@ -0,0 +1,111 @@
package io.kestra.core.models.assets;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import io.kestra.core.models.DeletedInterface;
import io.kestra.core.models.HasUID;
import io.kestra.core.models.Plugin;
import io.kestra.core.utils.IdUtils;
import io.swagger.v3.oas.annotations.Hidden;
import jakarta.annotation.Nullable;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.time.Instant;
import java.util.*;
@Getter
@NoArgsConstructor
public abstract class Asset implements HasUID, DeletedInterface, Plugin {
@Hidden
@Pattern(regexp = "^[a-z0-9][a-z0-9_-]*")
protected String tenantId;
@Pattern(regexp = "^[a-z0-9][a-z0-9._-]*")
@Size(min = 1, max = 150)
protected String namespace;
@NotBlank
@Pattern(regexp = "^[a-zA-Z0-9][a-zA-Z0-9._-]*")
@Size(min = 1, max = 150)
protected String id;
@NotBlank
protected String type;
protected String displayName;
protected String description;
protected Map<String, Object> metadata;
@Nullable
@Hidden
private Instant created;
@Nullable
@Hidden
private Instant updated;
@Hidden
private boolean deleted;
public Asset(
String tenantId,
String namespace,
String id,
String type,
String displayName,
String description,
Map<String, Object> metadata,
Instant created,
Instant updated,
boolean deleted
) {
this.tenantId = tenantId;
this.namespace = namespace;
this.id = id;
this.type = type;
this.displayName = displayName;
this.description = description;
this.metadata = Optional.ofNullable(metadata).map(HashMap::new).orElse(new HashMap<>());
Instant now = Instant.now();
this.created = Optional.ofNullable(created).orElse(now);
this.updated = Optional.ofNullable(updated).orElse(now);
this.deleted = deleted;
}
public <T extends Asset> T toUpdated() {
if (this.created == null) {
this.created = Instant.now();
}
this.updated = Instant.now();
return (T) this;
}
public Asset toDeleted() {
this.deleted = true;
return this;
}
@JsonAnySetter
public void setMetadata(String name, Object value) {
metadata.put(name, value);
}
@Override
public String uid() {
return Asset.uid(tenantId, id);
}
public static String uid(String tenantId, String id) {
return IdUtils.fromParts(tenantId, id);
}
public Asset withTenantId(String tenantId) {
this.tenantId = tenantId;
return this;
}
}

View File

@@ -0,0 +1,19 @@
package io.kestra.core.models.assets;
import io.kestra.core.utils.IdUtils;
import io.swagger.v3.oas.annotations.Hidden;
public record AssetIdentifier(@Hidden String tenantId, @Hidden String namespace, String id){
public AssetIdentifier withTenantId(String tenantId) {
return new AssetIdentifier(tenantId, this.namespace, this.id);
}
public String uid() {
return IdUtils.fromParts(tenantId, id);
}
public static AssetIdentifier of(Asset asset) {
return new AssetIdentifier(asset.getTenantId(), asset.getNamespace(), asset.getId());
}
}

View File

@@ -0,0 +1,18 @@
package io.kestra.core.models.assets;
import io.kestra.core.models.HasUID;
import io.kestra.core.models.flows.FlowId;
import io.kestra.core.utils.IdUtils;
/**
* Represents an entity that used an asset
*/
public record AssetUser(String tenantId, String namespace, String flowId, Integer flowRevision, String executionId, String taskId, String taskRunId) implements HasUID {
public String uid() {
return IdUtils.fromParts(tenantId, namespace, flowId, String.valueOf(flowRevision), executionId, taskRunId);
}
public FlowId toFlowId() {
return FlowId.of(tenantId, namespace, flowId, flowRevision);
}
}

View File

@@ -0,0 +1,22 @@
package io.kestra.core.models.assets;
import com.fasterxml.jackson.annotation.JsonCreator;
import io.micronaut.core.annotation.Introspected;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.Optional;
@Getter
public class AssetsDeclaration extends AssetsInOut {
private boolean enableAuto;
@JsonCreator
public AssetsDeclaration(Boolean enableAuto, List<AssetIdentifier> inputs, List<Asset> outputs) {
super(inputs, outputs);
this.enableAuto = Optional.ofNullable(enableAuto).orElse(false);
}
}

View File

@@ -0,0 +1,21 @@
package io.kestra.core.models.assets;
import com.fasterxml.jackson.annotation.JsonCreator;
import lombok.Getter;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
@Getter
public class AssetsInOut {
private List<AssetIdentifier> inputs;
private List<Asset> outputs;
@JsonCreator
public AssetsInOut(List<AssetIdentifier> inputs, List<Asset> outputs) {
this.inputs = Optional.ofNullable(inputs).orElse(Collections.emptyList());
this.outputs = Optional.ofNullable(outputs).orElse(Collections.emptyList());
}
}

View File

@@ -0,0 +1,32 @@
package io.kestra.core.models.assets;
import com.fasterxml.jackson.annotation.JsonCreator;
import io.kestra.core.models.annotations.Plugin;
import io.swagger.v3.oas.annotations.Hidden;
import lombok.Builder;
import lombok.NoArgsConstructor;
import java.time.Instant;
import java.util.Map;
@NoArgsConstructor
@Plugin
@Hidden
public class Custom extends Asset {
@Builder
@JsonCreator
public Custom(
String tenantId,
String namespace,
String id,
String type,
String displayName,
String description,
Map<String, Object> metadata,
Instant created,
Instant updated,
boolean deleted
) {
super(tenantId, namespace, id, type, displayName, description, metadata, created, updated, deleted);
}
}

View File

@@ -0,0 +1,32 @@
package io.kestra.core.models.assets;
import com.fasterxml.jackson.annotation.JsonCreator;
import io.kestra.core.models.annotations.Plugin;
import io.swagger.v3.oas.annotations.Hidden;
import lombok.Builder;
import lombok.NoArgsConstructor;
import java.time.Instant;
import java.util.Map;
@NoArgsConstructor
@Plugin
public class External extends Asset {
public static final String ASSET_TYPE = External.class.getName();
@Builder
@JsonCreator
public External(
String tenantId,
String namespace,
String id,
String displayName,
String description,
Map<String, Object> metadata,
Instant created,
Instant updated,
boolean deleted
) {
super(tenantId, namespace, id, ASSET_TYPE, displayName, description, metadata, created, updated, deleted);
}
}

View File

@@ -2,6 +2,7 @@ package io.kestra.core.models.executions;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.kestra.core.models.TenantInterface;
import io.kestra.core.models.assets.AssetsInOut;
import io.kestra.core.models.flows.State;
import io.kestra.core.models.tasks.ResolvedTask;
import io.kestra.core.models.tasks.retrys.AbstractRetry;
@@ -57,6 +58,10 @@ public class TaskRun implements TenantInterface {
@Schema(implementation = Object.class)
Variables outputs;
@With
@Nullable
AssetsInOut assets;
@NotNull
State state;
@@ -87,6 +92,7 @@ public class TaskRun implements TenantInterface {
this.value,
this.attempts,
this.outputs,
this.assets,
this.state.withState(state),
this.iteration,
this.dynamic,
@@ -114,6 +120,7 @@ public class TaskRun implements TenantInterface {
this.value,
newAttempts,
this.outputs,
this.assets,
this.state.withState(state),
this.iteration,
this.dynamic,
@@ -137,6 +144,7 @@ public class TaskRun implements TenantInterface {
this.value,
newAttempts,
this.outputs,
this.assets,
this.state.withState(State.Type.FAILED),
this.iteration,
this.dynamic,
@@ -156,6 +164,7 @@ public class TaskRun implements TenantInterface {
.value(this.getValue())
.attempts(this.getAttempts())
.outputs(this.getOutputs())
.assets(this.getAssets())
.state(state == null ? this.getState() : state)
.iteration(this.getIteration())
.build();
@@ -185,15 +194,11 @@ public class TaskRun implements TenantInterface {
}
public TaskRunAttempt lastAttempt() {
if (this.attempts == null) {
if (this.attempts == null || this.attempts.isEmpty()) {
return null;
}
return this
.attempts
.stream()
.reduce((a, b) -> b)
.orElse(null);
return this.attempts.getLast();
}
public TaskRun onRunningResend() {
@@ -242,6 +247,7 @@ public class TaskRun implements TenantInterface {
", parentTaskRunId=" + this.getParentTaskRunId() +
", state=" + this.getState().getCurrent().toString() +
", outputs=" + this.getOutputs() +
", assets=" + this.getAssets() +
", attempts=" + this.getAttempts() +
")";
}
@@ -264,8 +270,7 @@ public class TaskRun implements TenantInterface {
* @return The next retry date, null if maxAttempt || maxDuration is reached
*/
public Instant nextRetryDate(AbstractRetry retry, Execution execution) {
if (retry.getMaxAttempts() != null && execution.getMetadata().getAttemptNumber() >= retry.getMaxAttempts()) {
if (this.attempts == null || this.attempts.isEmpty() || retry.getMaxAttempts() != null && execution.getMetadata().getAttemptNumber() >= retry.getMaxAttempts()) {
return null;
}
Instant base = this.lastAttempt().getState().maxDate();

View File

@@ -148,6 +148,11 @@ public class State {
return this.current.isTerminated();
}
@JsonIgnore
public boolean canBeRestarted() {
return this.current.isTerminated() || this.current.isPaused();
}
@JsonIgnore
public boolean isTerminatedNoFail() {
return this.current.isTerminatedNoFail();

View File

@@ -5,11 +5,13 @@ import com.fasterxml.jackson.annotation.JsonInclude;
import io.kestra.core.exceptions.IllegalVariableEvaluationException;
import io.kestra.core.models.annotations.Plugin;
import io.kestra.core.models.annotations.PluginProperty;
import io.kestra.core.models.assets.AssetsDeclaration;
import io.kestra.core.models.executions.TaskRun;
import io.kestra.core.models.property.Property;
import io.kestra.core.models.tasks.retrys.AbstractRetry;
import io.kestra.core.runners.RunContext;
import io.kestra.plugin.core.flow.WorkingDirectory;
import jakarta.annotation.Nullable;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Size;
import lombok.Builder;
@@ -78,6 +80,11 @@ abstract public class Task implements TaskInterface {
@Valid
private Cache taskCache;
@PluginProperty(hidden = true, group = PluginProperty.CORE_GROUP)
@Valid
@Nullable
private Property<AssetsDeclaration> assets;
public Optional<Task> findById(String id) {
if (this.getId().equals(id)) {
return Optional.of(this);

View File

@@ -1,10 +1,13 @@
package io.kestra.core.models.tasks.runners;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.kestra.core.exceptions.IllegalVariableEvaluationException;
import io.kestra.core.models.assets.Asset;
import io.kestra.core.models.executions.AbstractMetricEntry;
import io.kestra.core.queues.QueueException;
import io.kestra.core.runners.AssetEmitter;
import io.kestra.core.runners.RunContext;
import io.kestra.core.serializers.JacksonMapper;
import jakarta.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.event.Level;
import org.slf4j.spi.LoggingEventBuilder;
@@ -18,6 +21,7 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static io.kestra.core.runners.RunContextLogger.ORIGINAL_TIMESTAMP_KEY;
import static io.kestra.core.utils.Rethrow.throwConsumer;
/**
* Service for matching and capturing structured data from task execution logs.
@@ -76,6 +80,18 @@ public class TaskLogLineMatcher {
}
});
}
if (match.assets() != null) {
try {
AssetEmitter assetEmitter = runContext.assets();
match.assets().forEach(throwConsumer(assetEmitter::upsert));
} catch (IllegalVariableEvaluationException e) {
logger.warn("Unable to get asset emitter for log '{}'", data, e);
} catch (QueueException e) {
logger.warn("Unable to emit asset for log '{}'", data, e);
}
}
return match;
}
@@ -94,8 +110,9 @@ public class TaskLogLineMatcher {
public record TaskLogMatch(
Map<String, Object> outputs,
List<AbstractMetricEntry<?>> metrics,
List<LogLine> logs
) {
List<LogLine> logs,
List<Asset> assets
) {
@Override
public Map<String, Object> outputs() {
return Optional.ofNullable(outputs).orElse(Map.of());

View File

@@ -6,8 +6,10 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import io.kestra.core.models.Label;
import io.kestra.core.models.annotations.Plugin;
import io.kestra.core.models.annotations.PluginProperty;
import io.kestra.core.models.assets.AssetsDeclaration;
import io.kestra.core.models.conditions.Condition;
import io.kestra.core.models.flows.State;
import io.kestra.core.models.property.Property;
import io.kestra.core.models.tasks.WorkerGroup;
import io.kestra.core.serializers.ListOrMapOfLabelDeserializer;
import io.kestra.core.serializers.ListOrMapOfLabelSerializer;
@@ -88,6 +90,9 @@ abstract public class AbstractTrigger implements TriggerInterface {
)
private boolean allowConcurrent = false;
@PluginProperty(hidden = true, group = PluginProperty.CORE_GROUP)
private Property<AssetsDeclaration> assets;
/**
* For backward compatibility: we rename minLogLevel to logLevel.
* @deprecated use {@link #logLevel} instead

View File

@@ -1,6 +1,7 @@
package io.kestra.core.plugins;
import io.kestra.core.models.Plugin;
import io.kestra.core.models.assets.Asset;
import jakarta.annotation.Nullable;
import jakarta.validation.constraints.NotNull;
import org.slf4j.Logger;

View File

@@ -2,6 +2,7 @@ package io.kestra.core.plugins;
import com.fasterxml.jackson.databind.module.SimpleModule;
import io.kestra.core.app.AppPluginInterface;
import io.kestra.core.models.assets.Asset;
import io.kestra.core.models.conditions.Condition;
import io.kestra.core.models.dashboards.DataFilter;
import io.kestra.core.models.dashboards.DataFilterKPI;
@@ -11,6 +12,7 @@ import io.kestra.core.models.tasks.Task;
import io.kestra.core.models.tasks.logs.LogExporter;
import io.kestra.core.models.tasks.runners.TaskRunner;
import io.kestra.core.models.triggers.AbstractTrigger;
import io.kestra.core.plugins.serdes.AssetDeserializer;
import io.kestra.core.plugins.serdes.PluginDeserializer;
import io.kestra.core.secret.SecretPluginInterface;
import io.kestra.core.storages.StorageInterface;
@@ -45,5 +47,6 @@ public class PluginModule extends SimpleModule {
addDeserializer(SecretPluginInterface.class, new PluginDeserializer<>());
addDeserializer(AppPluginInterface.class, new PluginDeserializer<>());
addDeserializer(LogExporter.class, new PluginDeserializer<>());
addDeserializer(Asset.class, new AssetDeserializer());
}
}

View File

@@ -3,6 +3,7 @@ package io.kestra.core.plugins;
import io.kestra.core.app.AppBlockInterface;
import io.kestra.core.app.AppPluginInterface;
import io.kestra.core.models.Plugin;
import io.kestra.core.models.assets.Asset;
import io.kestra.core.models.conditions.Condition;
import io.kestra.core.models.dashboards.DataFilter;
import io.kestra.core.models.dashboards.DataFilterKPI;
@@ -108,6 +109,7 @@ public class PluginScanner {
List<Class<? extends StorageInterface>> storages = new ArrayList<>();
List<Class<? extends SecretPluginInterface>> secrets = new ArrayList<>();
List<Class<? extends TaskRunner<?>>> taskRunners = new ArrayList<>();
List<Class<? extends Asset>> assets = new ArrayList<>();
List<Class<? extends AppPluginInterface>> apps = new ArrayList<>();
List<Class<? extends AppBlockInterface>> appBlocks = new ArrayList<>();
List<Class<? extends Chart<?>>> charts = new ArrayList<>();
@@ -155,6 +157,10 @@ public class PluginScanner {
//noinspection unchecked
taskRunners.add((Class<? extends TaskRunner<?>>) runner.getClass());
}
case Asset asset -> {
log.debug("Loading Asset plugin: '{}'", plugin.getClass());
assets.add(asset.getClass());
}
case AppPluginInterface app -> {
log.debug("Loading App plugin: '{}'", plugin.getClass());
apps.add(app.getClass());
@@ -223,6 +229,7 @@ public class PluginScanner {
.conditions(conditions)
.storages(storages)
.secrets(secrets)
.assets(assets)
.apps(apps)
.appBlocks(appBlocks)
.taskRunners(taskRunners)

View File

@@ -3,6 +3,7 @@ package io.kestra.core.plugins;
import io.kestra.core.app.AppBlockInterface;
import io.kestra.core.app.AppPluginInterface;
import io.kestra.core.models.annotations.PluginSubGroup;
import io.kestra.core.models.assets.Asset;
import io.kestra.core.models.conditions.Condition;
import io.kestra.core.models.dashboards.DataFilter;
import io.kestra.core.models.dashboards.DataFilterKPI;
@@ -39,6 +40,7 @@ public class RegisteredPlugin {
public static final String STORAGES_GROUP_NAME = "storages";
public static final String SECRETS_GROUP_NAME = "secrets";
public static final String TASK_RUNNERS_GROUP_NAME = "task-runners";
public static final String ASSETS_GROUP_NAME = "assets";
public static final String APPS_GROUP_NAME = "apps";
public static final String APP_BLOCKS_GROUP_NAME = "app-blocks";
public static final String CHARTS_GROUP_NAME = "charts";
@@ -56,6 +58,7 @@ public class RegisteredPlugin {
private final List<Class<? extends StorageInterface>> storages;
private final List<Class<? extends SecretPluginInterface>> secrets;
private final List<Class<? extends TaskRunner<?>>> taskRunners;
private final List<Class<? extends Asset>> assets;
private final List<Class<? extends AppPluginInterface>> apps;
private final List<Class<? extends AppBlockInterface>> appBlocks;
private final List<Class<? extends Chart<?>>> charts;
@@ -74,6 +77,7 @@ public class RegisteredPlugin {
!storages.isEmpty() ||
!secrets.isEmpty() ||
!taskRunners.isEmpty() ||
!assets.isEmpty() ||
!apps.isEmpty() ||
!appBlocks.isEmpty() ||
!charts.isEmpty() ||
@@ -145,6 +149,10 @@ public class RegisteredPlugin {
return AppPluginInterface.class;
}
if (this.getAssets().stream().anyMatch(r -> r.getName().equals(cls))) {
return Asset.class;
}
if (this.getLogExporters().stream().anyMatch(r -> r.getName().equals(cls))) {
return LogExporter.class;
}
@@ -180,6 +188,7 @@ public class RegisteredPlugin {
result.put(STORAGES_GROUP_NAME, Arrays.asList(this.getStorages().toArray(Class[]::new)));
result.put(SECRETS_GROUP_NAME, Arrays.asList(this.getSecrets().toArray(Class[]::new)));
result.put(TASK_RUNNERS_GROUP_NAME, Arrays.asList(this.getTaskRunners().toArray(Class[]::new)));
result.put(ASSETS_GROUP_NAME, Arrays.asList(this.getAssets().toArray(Class[]::new)));
result.put(APPS_GROUP_NAME, Arrays.asList(this.getApps().toArray(Class[]::new)));
result.put(APP_BLOCKS_GROUP_NAME, Arrays.asList(this.getAppBlocks().toArray(Class[]::new)));
result.put(CHARTS_GROUP_NAME, Arrays.asList(this.getCharts().toArray(Class[]::new)));
@@ -359,6 +368,12 @@ public class RegisteredPlugin {
b.append("] ");
}
if (!this.getAssets().isEmpty()) {
b.append("[Assets: ");
b.append(this.getAssets().stream().map(Class::getName).collect(Collectors.joining(", ")));
b.append("] ");
}
if (!this.getApps().isEmpty()) {
b.append("[Apps: ");
b.append(this.getApps().stream().map(Class::getName).collect(Collectors.joining(", ")));

View File

@@ -0,0 +1,16 @@
package io.kestra.core.plugins.serdes;
import com.fasterxml.jackson.databind.JsonDeserializer;
import io.kestra.core.models.Plugin;
import io.kestra.core.models.assets.Asset;
import io.kestra.core.models.assets.Custom;
/**
* Specific {@link JsonDeserializer} for deserializing {@link Asset}.
*/
public final class AssetDeserializer extends PluginDeserializer<Asset> {
@Override
protected Class<? extends Plugin> fallbackClass() {
return Custom.class;
}
}

View File

@@ -29,7 +29,7 @@ import java.util.Optional;
* The {@link PluginDeserializer} uses the {@link PluginRegistry} to found the plugin class corresponding to
* a plugin type.
*/
public final class PluginDeserializer<T extends Plugin> extends JsonDeserializer<T> {
public class PluginDeserializer<T extends Plugin> extends JsonDeserializer<T> {
private static final Logger log = LoggerFactory.getLogger(PluginDeserializer.class);
@@ -93,6 +93,10 @@ public final class PluginDeserializer<T extends Plugin> extends JsonDeserializer
identifier
);
pluginType = pluginRegistry.findClassByIdentifier(identifier);
if (pluginType == null) {
pluginType = fallbackClass();
}
}
if (pluginType == null) {
@@ -153,4 +157,8 @@ public final class PluginDeserializer<T extends Plugin> extends JsonDeserializer
return isVersioningSupported && version != null && !version.isEmpty() ? type + ":" + version : type;
}
protected Class<? extends Plugin> fallbackClass() {
return null;
}
}

View File

@@ -0,0 +1,12 @@
package io.kestra.core.runners;
import io.kestra.core.models.assets.Asset;
import io.kestra.core.queues.QueueException;
import java.util.List;
public interface AssetEmitter {
void upsert(Asset asset) throws QueueException;
List<Asset> outputs();
}

View File

@@ -6,11 +6,13 @@ import com.google.common.base.CaseFormat;
import com.google.common.collect.ImmutableMap;
import io.kestra.core.exceptions.IllegalVariableEvaluationException;
import io.kestra.core.metrics.MetricRegistry;
import io.kestra.core.models.assets.AssetsDeclaration;
import io.kestra.core.models.Plugin;
import io.kestra.core.models.executions.AbstractMetricEntry;
import io.kestra.core.models.property.Property;
import io.kestra.core.models.tasks.Task;
import io.kestra.core.models.triggers.AbstractTrigger;
import io.kestra.core.assets.AssetManagerFactory;
import io.kestra.core.plugins.PluginConfigurations;
import io.kestra.core.services.KVStoreService;
import io.kestra.core.storages.Storage;
@@ -54,6 +56,7 @@ public class DefaultRunContext extends RunContext {
private MetricRegistry meterRegistry;
private VersionProvider version;
private KVStoreService kvStoreService;
private AssetManagerFactory assetManagerFactory;
private Optional<String> secretKey;
private WorkingDir workingDir;
private Validator validator;
@@ -73,6 +76,8 @@ public class DefaultRunContext extends RunContext {
private Task task;
private AbstractTrigger trigger;
private volatile AssetEmitter assetEmitter;
private final AtomicBoolean isInitialized = new AtomicBoolean(false);
@@ -161,6 +166,7 @@ public class DefaultRunContext extends RunContext {
this.secretKey = applicationContext.getProperty("kestra.encryption.secret-key", String.class);
this.validator = applicationContext.getBean(Validator.class);
this.localPath = applicationContext.getBean(LocalPathFactory.class).createLocalPath(this);
this.assetManagerFactory = applicationContext.getBean(AssetManagerFactory.class);
}
}
@@ -537,6 +543,23 @@ public class DefaultRunContext extends RunContext {
return flow != null ? flow.get("tenantId") : null;
}
/**
* {@inheritDoc}
*/
@Override
public TaskRunInfo taskRunInfo() {
Optional<Map<String, Object>> maybeTaskRunMap = Optional.ofNullable(this.getVariables().get("taskrun"))
.map(Map.class::cast);
return new TaskRunInfo(
(String) this.getVariables().get("executionId"),
(String) this.getVariables().get("taskId"),
maybeTaskRunMap.map(m -> (String) m.get("id"))
.orElse(null),
maybeTaskRunMap.map(m -> (String) m.get("value"))
.orElse(null)
);
}
/**
* {@inheritDoc}
*/
@@ -545,12 +568,7 @@ public class DefaultRunContext extends RunContext {
public FlowInfo flowInfo() {
Map<String, Object> flow = (Map<String, Object>) this.getVariables().get("flow");
// normally only tests should not have the flow variable
return flow == null ? new FlowInfo(null, null, null, null) : new FlowInfo(
(String) flow.get("tenantId"),
(String) flow.get("namespace"),
(String) flow.get("id"),
(Integer) flow.get("revision")
);
return flow == null ? new FlowInfo(null, null, null, null) : FlowInfo.from(flow);
}
/**
@@ -594,6 +612,25 @@ public class DefaultRunContext extends RunContext {
return new AclCheckerImpl(this.applicationContext, flowInfo());
}
@Override
public AssetEmitter assets() throws IllegalVariableEvaluationException {
if (this.assetEmitter == null) {
synchronized (this) {
if (this.assetEmitter == null) {
this.assetEmitter = assetManagerFactory.of(
Optional.ofNullable(task).map(Task::getAssets)
.or(() -> Optional.ofNullable(trigger).map(AbstractTrigger::getAssets))
.flatMap(throwFunction(asset -> this.render(asset).as(AssetsDeclaration.class)))
.map(AssetsDeclaration::isEnableAuto)
.orElse(false)
);
}
}
}
return this.assetEmitter;
}
@Override
public LocalPath localPath() {
return localPath;

View File

@@ -143,6 +143,8 @@ public abstract class RunContext implements PropertyContext {
@Deprecated(forRemoval = true)
public abstract String tenantId();
public abstract TaskRunInfo taskRunInfo();
public abstract FlowInfo flowInfo();
/**
@@ -190,7 +192,19 @@ public abstract class RunContext implements PropertyContext {
*/
public abstract LocalPath localPath();
public record TaskRunInfo(String executionId, String taskId, String taskRunId, Object value) {
}
public record FlowInfo(String tenantId, String namespace, String id, Integer revision) {
public static FlowInfo from(Map<String, Object> flowInfoMap) {
return new FlowInfo(
(String) flowInfoMap.get("tenantId"),
(String) flowInfoMap.get("namespace"),
(String) flowInfoMap.get("id"),
(Integer) flowInfoMap.get("revision")
);
}
}
/**
@@ -206,6 +220,11 @@ public abstract class RunContext implements PropertyContext {
*/
public abstract AclChecker acl();
/**
* Get access to the Assets handler.
*/
public abstract AssetEmitter assets() throws IllegalVariableEvaluationException;
/**
* Clone this run context for a specific plugin.
* @return a new run context with the plugin configuration of the given plugin.

View File

@@ -21,8 +21,7 @@ public class WorkerTaskResult implements HasUID {
List<TaskRun> dynamicTaskRuns;
public WorkerTaskResult(TaskRun taskRun) {
this.taskRun = taskRun;
this.dynamicTaskRuns = new ArrayList<>();
this(taskRun, new ArrayList<>(1)); // there are usually very few dynamic task runs, so we init the list with a capacity of 1
}
/**

View File

@@ -1,5 +1,6 @@
package io.kestra.core.runners.pebble.functions;
import com.cronutils.utils.VisibleForTesting;
import io.kestra.core.runners.LocalPath;
import io.kestra.core.runners.LocalPathFactory;
import io.kestra.core.services.NamespaceService;
@@ -155,31 +156,11 @@ abstract class AbstractFileFunction implements Function {
}
private String checkIfFileFromAllowedNamespaceAndReturnIt(URI path, String tenantId, String fromNamespace) {
// Extract namespace from the path, it should be of the form: kestra:///({tenantId}/){namespace}/{flowId}/executions/{executionId}/tasks/{taskId}/{taskRunId}/{fileName}'
// To extract the namespace, we must do it step by step as tenantId, namespace and taskId can contain the words 'executions' and 'tasks'
String namespace = path.toString().substring(KESTRA_SCHEME.length());
if (!EXECUTION_FILE.matcher(namespace).matches()) {
throw new IllegalArgumentException("Unable to read the file '" + path + "' as it is not an execution file");
}
// 1. remove the tenantId if existing
if (tenantId != null) {
namespace = namespace.substring(tenantId.length() + 1);
}
// 2. remove everything after tasks
namespace = namespace.substring(0, namespace.lastIndexOf("/tasks/"));
// 3. remove everything after executions
namespace = namespace.substring(0, namespace.lastIndexOf("/executions/"));
// 4. remove the flowId
namespace = namespace.substring(0, namespace.lastIndexOf('/'));
// 5. replace '/' with '.'
namespace = namespace.replace("/", ".");
String namespace = extractNamespace(path);
namespaceService.checkAllowedNamespace(tenantId, namespace, tenantId, fromNamespace);
return namespace;
}
private String checkEnabledLocalFileAndReturnNamespace(Map<String, Object> args, Map<String, String> flow) {
if (!enableFileProtocol) {
throw new SecurityException("The file:// protocol has been disabled inside the Kestra configuration.");
@@ -200,4 +181,24 @@ abstract class AbstractFileFunction implements Function {
}
return Optional.ofNullable(customNs).orElse(flow.get(NAMESPACE));
}
@VisibleForTesting
String extractNamespace( URI path){
// Extract namespace from the path, it should be of the form: kestra:///{namespace}/{flowId}/executions/{executionId}/tasks/{taskId}/{taskRunId}/{fileName}'
// To extract the namespace, we must do it step by step as namespace and taskId can contain the words 'executions' and 'tasks'
String namespace = path.toString().substring(KESTRA_SCHEME.length());
if (!EXECUTION_FILE.matcher(namespace).matches()) {
throw new IllegalArgumentException("Unable to read the file '" + path + "' as it is not an execution file");
}
// 1. remove everything after tasks
namespace = namespace.substring(0, namespace.lastIndexOf("/tasks/"));
// 2. remove everything after executions
namespace = namespace.substring(0, namespace.lastIndexOf("/executions/"));
// 3. remove the flowId
namespace = namespace.substring(0, namespace.lastIndexOf('/'));
// 4. replace '/' with '.'
namespace = namespace.replace("/", ".");
return namespace;
}
}

View File

@@ -187,7 +187,7 @@ public class ExecutionService {
}
public Execution restart(final Execution execution, @Nullable Integer revision) throws Exception {
if (!(execution.getState().isTerminated() || execution.getState().isPaused())) {
if (!execution.getState().canBeRestarted()) {
throw new IllegalStateException("Execution must be terminated to be restarted, " +
"current state is '" + execution.getState().getCurrent() + "' !"
);

View File

@@ -1,5 +1,6 @@
package io.kestra.core.test.flow;
import io.kestra.core.models.assets.Asset;
import io.kestra.core.models.flows.State;
import io.kestra.core.models.property.Property;
import jakarta.validation.constraints.NotNull;
@@ -8,6 +9,7 @@ import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.Map;
@Getter
@@ -25,5 +27,7 @@ public class TaskFixture {
private Map<String, Object> outputs;
private List<Asset> assets;
private Property<String> description;
}

View File

@@ -19,15 +19,7 @@ import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.*;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@@ -155,6 +147,8 @@ public class FlowValidator implements ConstraintValidator<FlowValidation, Flow>
.map(task -> task.getId())
.collect(Collectors.toList());
violations.addAll(assetsViolations(allTasks));
if (!invalidTasks.isEmpty()) {
violations.add("Invalid output reference: use outputs[key-name] instead of outputs.key-name — keys with dashes require bracket notation, offending tasks:" +
" [" + String.join(", ", invalidTasks) + "]");
@@ -181,6 +175,12 @@ public class FlowValidator implements ConstraintValidator<FlowValidation, Flow>
}
}
protected List<String> assetsViolations(List<Task> allTasks) {
return allTasks.stream().filter(task -> task.getAssets() != null)
.map(taskWithAssets -> "Task '" + taskWithAssets.getId() + "' can't have any `assets` because assets are only available in Enterprise Edition.")
.toList();
}
private static boolean checkObjectFieldsWithPatterns(Object object, List<Pattern> patterns) {
if (object == null) {
return true;

View File

@@ -15,6 +15,7 @@ import lombok.experimental.SuperBuilder;
import java.util.List;
import java.util.Map;
import java.util.Collections;
@SuperBuilder
@ToString
@@ -62,14 +63,24 @@ public class SetVariables extends Task implements ExecutionUpdatableTask {
public Execution update(Execution execution, RunContext runContext) throws Exception {
Map<String, Object> renderedVars = runContext.render(this.variables).asMap(String.class, Object.class);
boolean renderedOverwrite = runContext.render(overwrite).as(Boolean.class).orElseThrow();
Map<String, Object> currentVariables =
execution.getVariables() == null ? Collections.emptyMap() : execution.getVariables();
if (!renderedOverwrite) {
// check that none of the new variables already exist
List<String> duplicated = renderedVars.keySet().stream().filter(key -> execution.getVariables().containsKey(key)).toList();
List<String> duplicated = renderedVars.keySet().stream()
.filter(currentVariables::containsKey)
.toList();
if (!duplicated.isEmpty()) {
throw new IllegalArgumentException("`overwrite` is set to false and the following variables already exist: " + String.join(",", duplicated));
throw new IllegalArgumentException(
"`overwrite` is set to false and the following variables already exist: " +
String.join(",", duplicated)
);
}
}
return execution.withVariables(MapUtils.deepMerge(execution.getVariables(), renderedVars));
return execution.withVariables(MapUtils.deepMerge(currentVariables, renderedVars));
}
}

View File

@@ -183,6 +183,7 @@ public class Webhook extends AbstractTrigger implements TriggerOutput<Webhook.Ou
.flowId(flow.getId())
.flowRevision(flow.getRevision())
.inputs(inputs)
.variables(flow.getVariables())
.state(new State())
.trigger(ExecutionTrigger.of(
this,

View File

@@ -103,7 +103,7 @@ class ClassPluginDocumentationTest {
PluginClassAndMetadata<AbstractTrigger> metadata = PluginClassAndMetadata.create(scan, Schedule.class, AbstractTrigger.class, null);
ClassPluginDocumentation<? extends AbstractTrigger> doc = ClassPluginDocumentation.of(jsonSchemaGenerator, metadata, scan.version(), true);
assertThat(doc.getDefs().size()).isEqualTo(20);
assertThat(doc.getDefs()).hasSize(23);
assertThat(doc.getDocLicense()).isNull();
assertThat(((Map<String, Object>) doc.getDefs().get("io.kestra.core.models.tasks.WorkerGroup")).get("type")).isEqualTo("object");
@@ -142,9 +142,9 @@ class ClassPluginDocumentationTest {
ClassPluginDocumentation<? extends DynamicPropertyExampleTask> doc = ClassPluginDocumentation.of(jsonSchemaGenerator, metadata, scan.version(), true);
assertThat(doc.getCls()).isEqualTo("io.kestra.core.models.property.DynamicPropertyExampleTask");
assertThat(doc.getDefs()).hasSize(6);
assertThat(doc.getDefs()).hasSize(9);
Map<String, Object> properties = (Map<String, Object>) doc.getPropertiesSchema().get("properties");
assertThat(properties).hasSize(21);
assertThat(properties).hasSize(22);
Map<String, Object> number = (Map<String, Object>) properties.get("number");
assertThat(number.get("anyOf")).isNotNull();

View File

@@ -47,7 +47,6 @@ import static org.hamcrest.Matchers.*;
@KestraTest
class JsonSchemaGeneratorTest {
@Inject
JsonSchemaGenerator jsonSchemaGenerator;
@@ -346,7 +345,7 @@ class JsonSchemaGeneratorTest {
void pluginSchemaShouldNotResolveTaskAndTriggerSubtypes() {
Map<String, Object> generate = jsonSchemaGenerator.properties(null, TaskWithSubTaskAndSubTrigger.class);
var definitions = (Map<String, Map<String, Object>>) generate.get("$defs");
assertThat(definitions.size(), is(27));
assertThat(definitions.size(), is(30));
}
@SuppressWarnings("unchecked")

View File

@@ -15,13 +15,11 @@ import jakarta.inject.Inject;
import jakarta.validation.constraints.NotNull;
import lombok.*;
import lombok.experimental.SuperBuilder;
import org.hamcrest.MatcherAssert;
import org.junit.jupiter.api.Test;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.is;
@KestraTest(startRunner = true)
class AdditionalPluginTest {
@@ -43,7 +41,7 @@ class AdditionalPluginTest {
void shouldResolveAdditionalPluginSubtypes() {
Map<String, Object> generate = jsonSchemaGenerator.properties(null, AdditionalPluginTest.AdditionalPluginTestTask.class);
var definitions = (Map<String, Map<String, Object>>) generate.get("$defs");
assertThat(definitions).hasSize(7);
assertThat(definitions).hasSize(10);
assertThat(definitions).containsKey("io.kestra.core.plugins.AdditionalPluginTest-AdditionalPluginTest1");
assertThat(definitions).containsKey("io.kestra.core.plugins.AdditionalPluginTest-AdditionalPluginTest2");
}

View File

@@ -95,4 +95,4 @@ class PluginDeserializerTest {
public record TestPlugin(String type) implements Plugin {
}
}
}

View File

@@ -558,4 +558,4 @@ public abstract class AbstractRunnerTest {
public void shouldCallTasksAfterListener(Execution execution) {
afterExecutionTestCase.shouldCallTasksAfterListener(execution);
}
}
}

View File

@@ -0,0 +1,28 @@
package io.kestra.core.runners.pebble.functions;
import io.kestra.core.junit.annotations.KestraTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;
import java.net.URI;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
@KestraTest
public class AbstractFileFunctionTest {
@Inject
ReadFileFunction readFileFunction;
@Test
void namespaceFromURI(){
String namespace1 = readFileFunction.extractNamespace(URI.create("kestra:///demo/simple-write-oss/executions/4Tnd2zrWGoHGrufwyt738j/tasks/write/2FOeylkRr5tktwIQqFh56w/18316959863401460785.txt"));
assertThat(namespace1).isEqualTo("demo");
String namespace2 = readFileFunction.extractNamespace(URI.create("kestra:///io/kestra/tests/simple-write-oss/executions/4Tnd2zrWGoHGrufwyt738j/tasks/write/2FOeylkRr5tktwIQqFh56w/18316959863401460785.txt"));
assertThat(namespace2).isEqualTo("io.kestra.tests");
assertThrows(IllegalArgumentException.class, () ->readFileFunction.extractNamespace(URI.create("kestra:///simple-write-oss/executions/4Tnd2zrWGoHGrufwyt738j/tasks/write/2FOeylkRr5tktwIQqFh56w/18316959863401460785.txt")));
assertThrows(IllegalArgumentException.class, () ->readFileFunction.extractNamespace(URI.create("kestra:///executions/4Tnd2zrWGoHGrufwyt738j/tasks/write/2FOeylkRr5tktwIQqFh56w/18316959863401460785.txt")));
}
}

View File

@@ -0,0 +1,29 @@
package io.kestra.core.runners.test;
import io.kestra.core.models.annotations.Plugin;
import io.kestra.core.models.annotations.PluginProperty;
import io.kestra.core.models.assets.Asset;
import io.kestra.core.models.tasks.*;
import io.kestra.core.runners.RunContext;
import jakarta.validation.constraints.NotNull;
import lombok.*;
import lombok.experimental.SuperBuilder;
@SuperBuilder
@ToString
@EqualsAndHashCode
@Getter
@NoArgsConstructor
@Plugin
public class AssetEmitter extends Task implements RunnableTask<VoidOutput> {
@NotNull
@PluginProperty
private Asset assetToEmit;
@Override
public VoidOutput run(RunContext runContext) throws Exception {
runContext.assets().upsert(assetToEmit);
return null;
}
}

View File

@@ -1,5 +1,7 @@
package io.kestra.core.validations;
import io.kestra.core.models.assets.AssetIdentifier;
import io.kestra.core.models.assets.AssetsDeclaration;
import io.kestra.core.models.flows.Flow;
import io.kestra.core.models.flows.GenericFlow;
import io.kestra.core.models.validations.ModelValidator;
@@ -7,7 +9,9 @@ import io.kestra.core.serializers.YamlParser;
import io.kestra.core.tenant.TenantService;
import io.kestra.core.utils.TestsUtils;
import io.kestra.core.junit.annotations.KestraTest;
import io.kestra.plugin.core.log.Log;
import jakarta.inject.Inject;
import jakarta.validation.ConstraintViolation;
import org.junit.jupiter.api.Test;
import io.kestra.core.models.validations.ValidateConstraintViolation;
import io.kestra.core.services.FlowService;
@@ -229,6 +233,31 @@ class FlowValidationTest {
assertThat(validate.get().getMessage()).contains("Duplicate preconditions with id [flows]");
}
@Test
void eeAllowsDefiningAssets() {
Flow flow = Flow.builder()
.id(TestsUtils.randomString())
.namespace(TestsUtils.randomNamespace())
.tasks(List.of(
Log.builder()
.id("log")
.type(Log.class.getName())
.message("any")
.assets(io.kestra.core.models.property.Property.ofValue(
new AssetsDeclaration(true, List.of(new AssetIdentifier(null, null, "anyId")), null))
)
.build()
))
.build();
Optional<ConstraintViolationException> violations = modelValidator.isValid(flow);
assertThat(violations.isPresent()).isEqualTo(true);
assertThat(violations.get().getConstraintViolations().stream().map(ConstraintViolation::getMessage)).satisfiesExactly(
message -> assertThat(message).contains("Task 'log' can't have any `assets` because assets are only available in Enterprise Edition.")
);
};
private Flow parse(String path) {
URL resource = TestsUtils.class.getClassLoader().getResource(path);
assert resource != null;
@@ -237,4 +266,4 @@ class FlowValidationTest {
return YamlParser.parse(file, Flow.class);
}
}
}

View File

@@ -10,7 +10,7 @@ tasks:
message: "{{ task.id }}"
- id: pause
type: io.kestra.plugin.core.flow.Pause
delay: PT1S
pauseDuration: PT1S
tasks:
- id: c
type: io.kestra.plugin.core.log.Log

View File

@@ -1,9 +1,14 @@
package io.kestra.executor;
import io.kestra.core.assets.AssetService;
import io.kestra.core.debug.Breakpoint;
import io.kestra.core.exceptions.InternalException;
import io.kestra.core.metrics.MetricRegistry;
import io.kestra.core.models.Label;
import io.kestra.core.models.assets.AssetIdentifier;
import io.kestra.core.models.assets.AssetUser;
import io.kestra.core.models.assets.AssetsDeclaration;
import io.kestra.core.models.assets.AssetsInOut;
import io.kestra.core.models.executions.*;
import io.kestra.core.models.flows.FlowInterface;
import io.kestra.core.models.flows.FlowWithSource;
@@ -96,6 +101,12 @@ public class ExecutorService {
@Named(QueueFactoryInterface.WORKERTASKLOG_NAMED)
private QueueInterface<LogEntry> logQueue;
@Inject
private AssetService assetService;
@Inject
private RunContextInitializer runContextInitializer;
protected FlowMetaStoreInterface flowExecutorInterface() {
// bean is injected late, so we need to wait
if (this.flowExecutorInterface == null) {
@@ -896,21 +907,35 @@ public class ExecutorService {
boolean hasMockedWorkerTask = false;
record FixtureAndTaskRun(TaskFixture fixture, TaskRun taskRun) {}
if (executor.getExecution().getFixtures() != null) {
RunContext runContext = runContextFactory.of(executor.getFlow(), executor.getExecution());
RunContext runContext = runContextInitializer.forExecutor((DefaultRunContext) runContextFactory.of(
executor.getFlow(),
executor.getExecution()
));
List<WorkerTaskResult> workerTaskResults = executor.getExecution()
.getTaskRunList()
.stream()
.filter(taskRun -> taskRun.getState().getCurrent().isCreated())
.flatMap(taskRun -> executor.getExecution().getFixtureForTaskRun(taskRun).stream().map(fixture -> new FixtureAndTaskRun(fixture, taskRun)))
.map(throwFunction(fixtureAndTaskRun -> WorkerTaskResult.builder()
.taskRun(fixtureAndTaskRun.taskRun()
.withState(Optional.ofNullable(fixtureAndTaskRun.fixture().getState()).orElse(State.Type.SUCCESS))
.withOutputs(
variablesService.of(StorageContext.forTask(fixtureAndTaskRun.taskRun),
fixtureAndTaskRun.fixture().getOutputs() == null ? null : runContext.render(fixtureAndTaskRun.fixture().getOutputs()))
)
)
.build()
.map(throwFunction(fixtureAndTaskRun -> {
Optional<AssetsDeclaration> renderedAssetsDeclaration = runContext.render(executor.getFlow().findTaskByTaskId(fixtureAndTaskRun.taskRun.getTaskId()).getAssets()).as(AssetsDeclaration.class);
return WorkerTaskResult.builder()
.taskRun(fixtureAndTaskRun.taskRun()
.withState(Optional.ofNullable(fixtureAndTaskRun.fixture().getState()).orElse(State.Type.SUCCESS))
.withOutputs(
variablesService.of(StorageContext.forTask(fixtureAndTaskRun.taskRun),
fixtureAndTaskRun.fixture().getOutputs() == null ? null : runContext.render(fixtureAndTaskRun.fixture().getOutputs()))
)
.withAssets(new AssetsInOut(
renderedAssetsDeclaration.map(AssetsDeclaration::getInputs).orElse(Collections.emptyList()).stream()
.map(assetIdentifier -> assetIdentifier.withTenantId(executor.getFlow().getTenantId()))
.toList(),
fixtureAndTaskRun.fixture().getAssets() == null ? null : fixtureAndTaskRun.fixture().getAssets().stream()
.map(asset -> asset.withTenantId(executor.getFlow().getTenantId()))
.toList()
))
)
.build();
}
))
.toList();
@@ -1172,6 +1197,47 @@ public class ExecutorService {
metricRegistry.tags(workerTaskResult)
)
.record(taskRun.getState().getDurationOrComputeIt());
if (
!taskRun.getState().isFailed()
&& taskRun.getAssets() != null &&
(!taskRun.getAssets().getInputs().isEmpty() || !taskRun.getAssets().getOutputs().isEmpty())
) {
AssetUser assetUser = new AssetUser(
taskRun.getTenantId(),
taskRun.getNamespace(),
taskRun.getFlowId(),
newExecution.getFlowRevision(),
taskRun.getExecutionId(),
taskRun.getTaskId(),
taskRun.getId()
);
List<AssetIdentifier> outputIdentifiers = taskRun.getAssets().getOutputs().stream()
.map(asset -> asset.withTenantId(taskRun.getTenantId()))
.map(AssetIdentifier::of)
.toList();
List<AssetIdentifier> inputAssets = taskRun.getAssets().getInputs().stream()
.map(assetIdentifier -> assetIdentifier.withTenantId(taskRun.getTenantId()))
.toList();
try {
assetService.assetLineage(
assetUser,
inputAssets,
outputIdentifiers
);
} catch (QueueException e) {
log.warn("Unable to submit asset lineage event for {} -> {}", inputAssets, outputIdentifiers, e);
}
taskRun.getAssets().getOutputs().forEach(asset -> {
try {
assetService.asyncUpsert(assetUser, asset);
} catch (QueueException e) {
log.warn("Unable to submit asset upsert event for asset {}", asset.getId(), e);
}
});
}
}
}

View File

@@ -0,0 +1,22 @@
ALTER TABLE queues ALTER COLUMN "type" ENUM(
'io.kestra.core.models.executions.Execution',
'io.kestra.core.models.templates.Template',
'io.kestra.core.models.executions.ExecutionKilled',
'io.kestra.core.runners.WorkerJob',
'io.kestra.core.runners.WorkerTaskResult',
'io.kestra.core.runners.WorkerInstance',
'io.kestra.core.runners.WorkerTaskRunning',
'io.kestra.core.models.executions.LogEntry',
'io.kestra.core.models.triggers.Trigger',
'io.kestra.ee.models.audits.AuditLog',
'io.kestra.core.models.executions.MetricEntry',
'io.kestra.core.runners.WorkerTriggerResult',
'io.kestra.core.runners.SubflowExecutionResult',
'io.kestra.core.server.ClusterEvent',
'io.kestra.core.runners.SubflowExecutionEnd',
'io.kestra.core.models.flows.FlowInterface',
'io.kestra.core.runners.MultipleConditionEvent',
'io.kestra.ee.assets.AssetLineageEvent',
'io.kestra.ee.assets.AssetUpsertCommand',
'io.kestra.ee.assets.AssetStateEvent'
) NOT NULL

View File

@@ -0,0 +1,22 @@
ALTER TABLE queues MODIFY COLUMN `type` ENUM(
'io.kestra.core.models.executions.Execution',
'io.kestra.core.models.templates.Template',
'io.kestra.core.models.executions.ExecutionKilled',
'io.kestra.core.runners.WorkerJob',
'io.kestra.core.runners.WorkerTaskResult',
'io.kestra.core.runners.WorkerInstance',
'io.kestra.core.runners.WorkerTaskRunning',
'io.kestra.core.models.executions.LogEntry',
'io.kestra.core.models.triggers.Trigger',
'io.kestra.ee.models.audits.AuditLog',
'io.kestra.core.models.executions.MetricEntry',
'io.kestra.core.runners.WorkerTriggerResult',
'io.kestra.core.runners.SubflowExecutionResult',
'io.kestra.core.server.ClusterEvent',
'io.kestra.core.runners.SubflowExecutionEnd',
'io.kestra.core.models.flows.FlowInterface',
'io.kestra.core.runners.MultipleConditionEvent',
'io.kestra.ee.assets.AssetLineageEvent',
'io.kestra.ee.assets.AssetUpsertCommand',
'io.kestra.ee.assets.AssetStateEvent'
) NOT NULL;

View File

@@ -0,0 +1,3 @@
ALTER TYPE queue_type ADD VALUE IF NOT EXISTS 'io.kestra.ee.assets.AssetLineageEvent';
ALTER TYPE queue_type ADD VALUE IF NOT EXISTS 'io.kestra.ee.assets.AssetUpsertCommand';
ALTER TYPE queue_type ADD VALUE IF NOT EXISTS 'io.kestra.ee.assets.AssetStateEvent';

View File

@@ -328,6 +328,10 @@ public abstract class AbstractJdbcRepository {
return applyTriggerStateCondition(value, operation);
}
if (field.equals(QueryFilter.Field.METADATA)) {
return findMetadataCondition((Map<?, ?>) value, operation);
}
// Convert the field name to lowercase and quote it
Name columnName = getColumnName(field);
@@ -380,6 +384,10 @@ public abstract class AbstractJdbcRepository {
throw new InvalidQueryFiltersException("Unsupported operation: " + operation);
}
protected Condition findMetadataCondition(Map<?, ?> metadata, QueryFilter.Op operation) {
throw new InvalidQueryFiltersException("Unsupported operation: " + operation);
}
// Generate the condition for Field.STATE
@SuppressWarnings("unchecked")
private Condition generateStateCondition(Object value, QueryFilter.Op operation) {

View File

@@ -297,10 +297,23 @@ public class JdbcExecutor implements ExecutorInterface {
this.receiveCancellations.addFirst(((JdbcQueue<Execution>) this.executionQueue).receiveBatch(
Executor.class,
executions -> {
List<CompletableFuture<Void>> futures = executions.stream()
.map(execution -> CompletableFuture.runAsync(() -> executionQueue(execution), executionExecutorService))
// process execution message grouped by executionId to avoid concurrency as the execution level as it would
List<CompletableFuture<Void>> perExecutionFutures = executions.stream()
.filter(Either::isLeft)
.collect(Collectors.groupingBy(either -> either.getLeft().getId()))
.values()
.stream()
.map(eithers -> CompletableFuture.runAsync(() -> {
eithers.forEach(this::executionQueue);
}, executionExecutorService))
.toList();
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
// directly process deserialization issues as most of the time there will be none
executions.stream()
.filter(Either::isRight)
.forEach(either -> executionQueue(either));
CompletableFuture.allOf(perExecutionFutures.toArray(CompletableFuture[]::new)).join();
}
));
this.receiveCancellations.addFirst(((JdbcQueue<WorkerTaskResult>) this.workerTaskResultQueue).receiveBatch(

View File

@@ -35,7 +35,7 @@ dependencies {
// we define cloud bom here for GCP, Azure and AWS so they are aligned for all plugins that use them (secret, storage, oss and ee plugins)
api platform('com.google.cloud:libraries-bom:26.73.0')
api platform("com.azure:azure-sdk-bom:1.3.3")
api platform('software.amazon.awssdk:bom:2.40.10')
api platform('software.amazon.awssdk:bom:2.40.15')
api platform("dev.langchain4j:langchain4j-bom:$langchain4jVersion")
api platform("dev.langchain4j:langchain4j-community-bom:$langchain4jCommunityVersion")
@@ -98,7 +98,7 @@ dependencies {
api group: 'org.apache.maven.resolver', name: 'maven-resolver-connector-basic', version: mavenResolverVersion
api group: 'org.apache.maven.resolver', name: 'maven-resolver-transport-file', version: mavenResolverVersion
api group: 'org.apache.maven.resolver', name: 'maven-resolver-transport-apache', version: mavenResolverVersion
api 'com.github.oshi:oshi-core:6.9.1'
api 'com.github.oshi:oshi-core:6.9.2'
api 'io.pebbletemplates:pebble:4.1.0'
api group: 'co.elastic.logging', name: 'logback-ecs-encoder', version: '1.7.0'
api group: 'de.focus-shift', name: 'jollyday-core', version: jollydayVersion

View File

@@ -8,25 +8,31 @@ import io.micronaut.test.extensions.junit5.MicronautJunit5Extension;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.platform.commons.support.AnnotationSupport;
import java.util.Set;
public class KestraTestExtension extends MicronautJunit5Extension {
@Override
protected MicronautTestValue buildMicronautTestValue(Class<?> testClass) {
testProperties.put("kestra.jdbc.executor.thread-count", Runtime.getRuntime().availableProcessors() * 4);
return AnnotationSupport
.findAnnotation(testClass, KestraTest.class)
.map(kestraTestAnnotation -> new MicronautTestValue(
kestraTestAnnotation.application(),
kestraTestAnnotation.environments(),
kestraTestAnnotation.packages(),
kestraTestAnnotation.propertySources(),
kestraTestAnnotation.rollback(),
kestraTestAnnotation.transactional(),
kestraTestAnnotation.rebuildContext(),
kestraTestAnnotation.contextBuilder(),
kestraTestAnnotation.transactionMode(),
kestraTestAnnotation.startApplication(),
kestraTestAnnotation.resolveParameters()
))
.map(kestraTestAnnotation -> {
var envsSet = new java.util.HashSet<>(Set.of(kestraTestAnnotation.environments()));
envsSet.add("test");// add test env if not already present
return new MicronautTestValue(
kestraTestAnnotation.application(),
envsSet.toArray(new String[0]),
kestraTestAnnotation.packages(),
kestraTestAnnotation.propertySources(),
kestraTestAnnotation.rollback(),
kestraTestAnnotation.transactional(),
kestraTestAnnotation.rebuildContext(),
kestraTestAnnotation.contextBuilder(),
kestraTestAnnotation.transactionMode(),
kestraTestAnnotation.startApplication(),
kestraTestAnnotation.resolveParameters()
);
})
.orElse(null);
}

View File

@@ -74,7 +74,7 @@ abstract public class TestsUtils {
* @param prefix
* @return
*/
private static String randomString(String... prefix) {
public static String randomString(String... prefix) {
if (prefix.length == 0) {
prefix = new String[]{String.join("-", stackTraceToParts())};
}

3108
ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -27,7 +27,7 @@
"@kestra-io/ui-libs": "^0.0.268",
"@vue-flow/background": "^1.3.2",
"@vue-flow/controls": "^1.1.2",
"@vue-flow/core": "^1.48.0",
"@vue-flow/core": "^1.48.1",
"@vueuse/core": "^14.1.0",
"ansi-to-html": "^0.7.2",
"axios": "^1.13.2",
@@ -39,7 +39,7 @@
"cytoscape": "^3.33.0",
"dagre": "^0.8.5",
"dotenv": "^17.2.3",
"element-plus": "2.12.0",
"element-plus": "2.13.0",
"humanize-duration": "^3.33.2",
"js-yaml": "^4.1.1",
"lodash": "^4.17.21",
@@ -59,15 +59,15 @@
"path-browserify": "^1.0.1",
"pdfjs-dist": "^5.4.449",
"pinia": "^3.0.4",
"posthog-js": "^1.308.0",
"posthog-js": "^1.310.1",
"rapidoc": "^9.3.8",
"semver": "^7.7.3",
"shiki": "^3.20.0",
"vue": "^3.5.25",
"vue": "^3.5.26",
"vue-axios": "^3.5.2",
"vue-chartjs": "^5.3.3",
"vue-gtag": "^3.6.3",
"vue-i18n": "^11.2.2",
"vue-i18n": "^11.2.7",
"vue-material-design-icons": "^5.3.1",
"vue-router": "^4.6.4",
"vue-sidebar-menu": "^5.9.1",
@@ -97,9 +97,9 @@
"@types/semver": "^7.7.1",
"@types/testing-library__jest-dom": "^6.0.0",
"@types/testing-library__user-event": "^4.2.0",
"@typescript-eslint/parser": "^8.50.0",
"@typescript-eslint/parser": "^8.50.1",
"@vitejs/plugin-vue": "^6.0.3",
"@vitejs/plugin-vue-jsx": "^5.1.2",
"@vitejs/plugin-vue-jsx": "^5.1.3",
"@vitest/browser": "^3.2.4",
"@vitest/coverage-v8": "^3.2.4",
"@vue/eslint-config-prettier": "^10.2.0",
@@ -120,29 +120,29 @@
"playwright": "^1.55.0",
"prettier": "^3.7.4",
"rimraf": "^6.1.2",
"rolldown-vite": "^7.2.11",
"rolldown-vite": "^7.3.0",
"rollup-plugin-copy": "^3.5.0",
"sass": "^1.97.0",
"storybook": "^9.1.16",
"sass": "^1.97.1",
"storybook": "^9.1.17",
"storybook-vue3-router": "^6.0.2",
"ts-node": "^10.9.2",
"typescript": "^5.9.3",
"typescript-eslint": "^8.50.0",
"typescript-eslint": "^8.50.1",
"uuid": "^13.0.0",
"vite": "npm:rolldown-vite@latest",
"vitest": "^3.2.4",
"vue-tsc": "^3.1.8"
"vue-tsc": "^3.2.1"
},
"optionalDependencies": {
"@esbuild/darwin-arm64": "^0.27.2",
"@esbuild/darwin-x64": "^0.27.2",
"@esbuild/linux-x64": "^0.27.2",
"@rollup/rollup-darwin-arm64": "^4.53.5",
"@rollup/rollup-darwin-x64": "^4.53.5",
"@rollup/rollup-linux-x64-gnu": "^4.53.5",
"@swc/core-darwin-arm64": "^1.15.5",
"@swc/core-darwin-x64": "^1.15.5",
"@swc/core-linux-x64-gnu": "^1.15.5"
"@rollup/rollup-darwin-arm64": "^4.54.0",
"@rollup/rollup-darwin-x64": "^4.54.0",
"@rollup/rollup-linux-x64-gnu": "^4.54.0",
"@swc/core-darwin-arm64": "^1.15.7",
"@swc/core-darwin-x64": "^1.15.7",
"@swc/core-linux-x64-gnu": "^1.15.7"
},
"overrides": {
"bootstrap": {

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 KiB

View File

@@ -43,7 +43,7 @@
REF_PATH_INJECTION_KEY,
ROOT_SCHEMA_INJECTION_KEY,
SCHEMA_DEFINITIONS_INJECTION_KEY,
UPDATE_TASK_FUNCTION_INJECTION_KEY
UPDATE_YAML_FUNCTION_INJECTION_KEY
} from "../../no-code/injectionKeys";
import {NoCodeProps} from "../../flows/noCodeTypes";
import {deepEqual} from "../../../utils/utils";
@@ -68,7 +68,7 @@
dashboardStore.sourceCode = YAML_UTILS.stringify(app);
}
provide(UPDATE_TASK_FUNCTION_INJECTION_KEY, (yaml) => {
provide(UPDATE_YAML_FUNCTION_INJECTION_KEY, (yaml) => {
editorUpdate(yaml)
})

View File

@@ -1,7 +1,7 @@
<template>
<el-dropdown trigger="click" hideOnClick placement="bottom-end">
<el-button :icon="Menu" class="selected">
<span class="text-truncate">
<el-button :icon="ChartLineVariant" class="selected">
<span v-if="!verticalLayout" class="text-truncate">
{{ selected ?? t("dashboards.default") }}
</span>
</el-button>
@@ -75,7 +75,10 @@
import Item from "./Item.vue";
import Menu from "vue-material-design-icons/Menu.vue";
import {useBreakpoints, breakpointsElement} from "@vueuse/core";
const verticalLayout = useBreakpoints(breakpointsElement).smallerOrEqual("sm");
import ChartLineVariant from "vue-material-design-icons/ChartLineVariant.vue";
import Plus from "vue-material-design-icons/Plus.vue";
import Magnify from "vue-material-design-icons/Magnify.vue";

View File

@@ -19,7 +19,8 @@
import {Chart, getDashboard} from "../composables/useDashboards";
import {useChartGenerator} from "../composables/useDashboards";
import {useBreakpoints, breakpointsElement} from "@vueuse/core";
const verticalLayout = useBreakpoints(breakpointsElement).smallerOrEqual("sm");
import {customBarLegend} from "../composables/useLegend";
import {useTheme} from "../../../utils/utils";
@@ -102,7 +103,7 @@
beginAtZero: true,
position: "left",
...DEFAULTS,
display: props.short ? false : true,
display: verticalLayout.value ? false : (props.short ? false : true),
ticks: {
...DEFAULTS.ticks,
callback: value => isDurationAgg() ? Utils.humanDuration(value) : value

View File

@@ -36,6 +36,9 @@
import KestraUtils, {useTheme} from "../../../utils/utils";
import {FilterObject} from "../../../utils/filters";
import {useBreakpoints, breakpointsElement} from "@vueuse/core";
const verticalLayout = useBreakpoints(breakpointsElement).smallerOrEqual("sm");
import {useI18n} from "vue-i18n";
const {t} = useI18n();
@@ -129,7 +132,7 @@
},
position: "left",
...DEFAULTS,
display: props.short || props.execution ? false : true,
display: verticalLayout.value ? false : (props.short || props.execution ? false : true),
ticks: {
...DEFAULTS.ticks,
callback: (value: any) => isDuration(aggregator.value[0]?.[1]?.field) ? KestraUtils.humanDuration(value) : value
@@ -143,7 +146,7 @@
},
position: "right",
...DEFAULTS,
display: props.short ? false : true,
display: verticalLayout.value ? false : (props.short ? false : true),
ticks: {
...DEFAULTS.ticks,
callback: (value: any) => isDuration(aggregator.value[1]?.[1]?.field) ? KestraUtils.humanDuration(value) : value

View File

@@ -0,0 +1,34 @@
<template>
<TopNavBar :title="routeInfo.title" />
<Layout
:title="t(`demos.assets.title`)"
:image="{
source: img,
alt: t(`demos.assets.title`)
}"
:video="{
//TODO: replace with ASSET video
source: 'https://www.youtube.com/embed/jMZ9Cs3xxpo',
}"
>
<template #message>
{{ $t(`demos.assets.message`) }}
</template>
</Layout>
</template>
<script setup lang="ts">
import {computed} from "vue";
import {useI18n} from "vue-i18n";
import img from "../../assets/demo/assets.png";
import useRouteContext from "../../composables/useRouteContext";
import Layout from "./Layout.vue";
import TopNavBar from "../../components/layout/TopNavBar.vue";
const {t} = useI18n();
const routeInfo = computed(() => ({title: t("demos.assets.header")}));
useRouteContext(routeInfo);
</script>

View File

@@ -105,7 +105,7 @@
position: relative;
background: $base-gray-200;
padding: .125rem 0.5rem;
border-radius: $border-radius;
border-radius: 1rem;
display: inline-block;
z-index: 2;
margin: 0 auto;
@@ -175,6 +175,7 @@
line-height: 16px;
font-size: 11px;
text-align: left;
color: var(--ks-content-secondary);
}
.video-container {
@@ -261,7 +262,7 @@
}
p {
font-size: 14px;
font-size: 1rem;
line-height: 22px;
}
}

View File

@@ -42,6 +42,7 @@
:elements="getElements()"
@select="selectNode"
:selected="selectedNodeID"
:subtype="SUBTYPE"
/>
</el-splitter-panel>
</el-splitter>
@@ -54,7 +55,7 @@
import Empty from "../layout/empty/Empty.vue";
import {useDependencies} from "./composables/useDependencies";
import {FLOW, EXECUTION, NAMESPACE} from "./utils/types";
import {FLOW, EXECUTION, NAMESPACE, ASSET} from "./utils/types";
const PANEL = {size: "70%", min: "30%", max: "80%"};
@@ -66,13 +67,27 @@
import SelectionRemove from "vue-material-design-icons/SelectionRemove.vue";
import FitToScreenOutline from "vue-material-design-icons/FitToScreenOutline.vue";
const SUBTYPE = route.name === "flows/update" ? FLOW : route.name === "namespaces/update" ? NAMESPACE : EXECUTION;
const props = defineProps<{
fetchAssetDependencies?: () => Promise<{
data: any[];
count: number;
}>;
}>();
const SUBTYPE = route.name === "flows/update" ? FLOW : route.name === "namespaces/update" ? NAMESPACE : route.name === "assets/update" ? ASSET : EXECUTION;
const container = ref(null);
const initialNodeID: string = SUBTYPE === FLOW || SUBTYPE === NAMESPACE ? String(route.params.id) : String(route.params.flowId);
const initialNodeID: string = SUBTYPE === FLOW || SUBTYPE === NAMESPACE || SUBTYPE === ASSET ? String(route.params.id || route.params.assetId) : String(route.params.flowId);
const TESTING = false; // When true, bypasses API data fetching and uses mock/test data.
const {getElements, isLoading, isRendering, selectedNodeID, selectNode, handlers} = useDependencies(container, SUBTYPE, initialNodeID, route.params, TESTING);
const {
getElements,
isLoading,
isRendering,
selectedNodeID,
selectNode,
handlers,
} = useDependencies(container, SUBTYPE, initialNodeID, route.params, TESTING, props.fetchAssetDependencies);
</script>
<style scoped lang="scss">
@@ -95,7 +110,7 @@
& .controls {
position: absolute;
bottom: 10px;
bottom: 16px;
left: 10px;
display: flex;
flex-direction: column;

View File

@@ -9,26 +9,32 @@
<script setup lang="ts">
import {computed} from "vue";
import {FLOW, EXECUTION, NAMESPACE, type Node} from "../utils/types";
import {FLOW, EXECUTION, NAMESPACE, ASSET, type Node} from "../utils/types";
const props = defineProps<{
node: Node;
subtype: typeof FLOW | typeof EXECUTION | typeof NAMESPACE;
subtype: typeof FLOW | typeof EXECUTION | typeof NAMESPACE | typeof ASSET;
}>();
const to = computed(() => {
const base = {namespace: props.node.namespace};
if ("id" in props.node.metadata && props.node.metadata.id)
if (props.subtype === ASSET) {
return {
name: "assets/update",
params: {...base, assetId: props.node.flow},
};
} else if ("id" in props.node.metadata && props.node.metadata.id) {
return {
name: "executions/update",
params: {...base, flowId: props.node.flow, id: props.node.metadata.id},
};
else
} else {
return {
name: "flows/update",
params: {...base, id: props.node.flow},
};
}
});
</script>

View File

@@ -2,7 +2,7 @@
<section id="input">
<el-input
v-model="search"
:placeholder="$t('dependency.search.placeholder')"
:placeholder="$t(props.subtype === ASSET ? 'dependency.search.asset_placeholder' : 'dependency.search.placeholder')"
clearable
/>
</section>
@@ -38,10 +38,13 @@
size="small"
/>
<RouterLink
v-if="[FLOW, NAMESPACE].includes(row.data.metadata.subtype)"
v-if="[FLOW, NAMESPACE, ASSET].includes(row.data.metadata.subtype)"
:to="{
name: 'flows/update',
params: {namespace: row.data.namespace, id: row.data.flow}}"
name: row.data.metadata.subtype === ASSET ? 'assets/update' : 'flows/update',
params: row.data.metadata.subtype === ASSET
? {namespace: row.data.namespace, assetId: row.data.flow}
: {namespace: row.data.namespace, id: row.data.flow}
}"
>
<el-icon :size="16">
<OpenInNew />
@@ -64,12 +67,13 @@
import OpenInNew from "vue-material-design-icons/OpenInNew.vue";
import {NODE, FLOW, EXECUTION, NAMESPACE, type Node} from "../utils/types";
import {NODE, FLOW, EXECUTION, NAMESPACE, ASSET, type Node} from "../utils/types";
const emits = defineEmits<{ (e: "select", id: Node["id"]): void }>();
const props = defineProps<{
elements: cytoscape.ElementDefinition[];
selected: Node["id"] | undefined;
subtype?: typeof FLOW | typeof EXECUTION | typeof NAMESPACE | typeof ASSET;
}>();
const focusSelectedRow = () => {
@@ -177,6 +181,10 @@ section#row {
& section#right {
flex-shrink: 0;
margin-left: 0.5rem;
:deep(a:hover .el-icon) {
color: var(--ks-content-link-hover);
}
}
}
</style>

View File

@@ -163,7 +163,7 @@ const setExecutionEdgeColors = throttle(
* @param classes - An array of class names to remove from all elements.
* Defaults to [`selected`, `faded`, `hovered`, `executions`].
*/
export function clearClasses(cy: cytoscape.Core, subtype: typeof FLOW | typeof EXECUTION | typeof NAMESPACE, classes: string[] = [SELECTED, FADED, HOVERED, EXECUTIONS]): void {
export function clearClasses(cy: cytoscape.Core, subtype: typeof FLOW | typeof EXECUTION | typeof NAMESPACE | typeof ASSET, classes: string[] = [SELECTED, FADED, HOVERED, EXECUTIONS]): void {
cy.elements().removeClass(classes.join(" "));
if (subtype === EXECUTION) cy.edges().style(edgeColors());
}
@@ -197,7 +197,7 @@ export function fit(cy: cytoscape.Core, padding: number = 50): void {
* @param subtype - Determines how connected elements are highlighted (`FLOW`, `EXECUTION` or `NAMESPACE`).
* @param id - Optional explicit ID to assign to the ref (defaults to the nodes own ID).
*/
function selectHandler(cy: cytoscape.Core, node: cytoscape.NodeSingular, selected: Ref<Node["id"] | undefined>, subtype: typeof FLOW | typeof EXECUTION | typeof NAMESPACE, id?: Node["id"]): void {
function selectHandler(cy: cytoscape.Core, node: cytoscape.NodeSingular, selected: Ref<Node["id"] | undefined>, subtype: typeof FLOW | typeof EXECUTION | typeof NAMESPACE | typeof ASSET, id?: Node["id"]): void {
// Clear all existing classes
clearClasses(cy, subtype);
@@ -263,7 +263,17 @@ function hoverHandler(cy: cytoscape.Core): void {
* @returns An object with element getters, loading state, rendering state, selected node ID,
* selection helpers, and control handlers.
*/
export function useDependencies(container: Ref<HTMLElement | null>, subtype: typeof FLOW | typeof EXECUTION | typeof NAMESPACE = FLOW, initialNodeID: string, params: RouteParams, isTesting = false) {
export function useDependencies(
container: Ref<HTMLElement | null>,
subtype: typeof FLOW | typeof EXECUTION | typeof NAMESPACE | typeof ASSET = FLOW,
initialNodeID: string,
params: RouteParams,
isTesting = false,
fetchAssetDependencies?: () => Promise<{
data: Element[];
count: number;
}>
) {
const coreStore = useCoreStore();
const flowStore = useFlowStore();
const executionsStore = useExecutionsStore();
@@ -301,7 +311,13 @@ export function useDependencies(container: Ref<HTMLElement | null>, subtype: typ
}
};
const elements = ref<{ data: cytoscape.ElementDefinition[]; count: number; }>({data: [], count: 0});
const elements = ref<{
data: cytoscape.ElementDefinition[];
count: number;
}>({
data: [],
count: 0,
});
onMounted(async () => {
if (isTesting) {
if (!container.value) {
@@ -313,13 +329,32 @@ export function useDependencies(container: Ref<HTMLElement | null>, subtype: typ
isLoading.value = false;
} else {
try {
if (subtype === NAMESPACE) {
const {data} = await namespacesStore.loadDependencies({namespace: params.id as string});
if (fetchAssetDependencies) {
const result = await fetchAssetDependencies();
elements.value = {
data: result.data,
count: result.count
};
isLoading.value = false;
} else if (subtype === NAMESPACE) {
const {data} = await namespacesStore.loadDependencies({
namespace: params.id as string,
});
const nodes = data.nodes ?? [];
elements.value = {data: transformResponse(data, NAMESPACE), count: new Set(nodes.map((r: { uid: string }) => r.uid)).size};
elements.value = {
data: transformResponse(data, NAMESPACE),
count: new Set(nodes.map((r: { uid: string }) => r.uid)).size,
};
isLoading.value = false;
} else {
const result = await flowStore.loadDependencies({id: (subtype === FLOW ? params.id : params.flowId) as string, namespace: params.namespace as string, subtype}, false);
const result = await flowStore.loadDependencies(
{
id: (subtype === FLOW ? params.id : params.flowId) as string,
namespace: params.namespace as string,
subtype,
},
false
);
elements.value = {data: result.data ?? [], count: result.count};
isLoading.value = false;
}
@@ -448,8 +483,16 @@ export function useDependencies(container: Ref<HTMLElement | null>, subtype: typ
selectedNodeID,
selectNode,
handlers: {
zoomIn: () => cy.zoom({level: cy.zoom() + 0.1, renderedPosition: cy.getElementById(selectedNodeID.value!).renderedPosition()}),
zoomOut: () => cy.zoom({level: cy.zoom() - 0.1, renderedPosition: cy.getElementById(selectedNodeID.value!).renderedPosition()}),
zoomIn: () =>
cy.zoom({
level: cy.zoom() + 0.1,
renderedPosition: cy.getElementById(selectedNodeID.value!).renderedPosition(),
}),
zoomOut: () =>
cy.zoom({
level: cy.zoom() - 0.1,
renderedPosition: cy.getElementById(selectedNodeID.value!).renderedPosition(),
}),
clearSelection: () => {
clearClasses(cy, subtype);
selectedNodeID.value = undefined;
@@ -468,9 +511,23 @@ export function useDependencies(container: Ref<HTMLElement | null>, subtype: typ
* @param subtype - The node subtype, either `FLOW`, `EXECUTION`, or `NAMESPACE`.
* @returns An array of cytoscape elements with correctly typed nodes and edges.
*/
export function transformResponse(response: {nodes: { uid: string; namespace: string; id: string }[]; edges: { source: string; target: string }[]; }, subtype: typeof FLOW | typeof EXECUTION | typeof NAMESPACE): Element[] {
const nodes: Node[] = response.nodes.map((node) => ({id: node.uid, type: NODE, flow: node.id, namespace: node.namespace, metadata: {subtype}}));
const edges: Edge[] = response.edges.map((edge) => ({id: uuid(), type: EDGE, source: edge.source, target: edge.target}));
export function transformResponse(response: {nodes: { uid: string; namespace: string; id: string }[]; edges: { source: string; target: string }[];}, subtype: typeof FLOW | typeof EXECUTION | typeof NAMESPACE): Element[] {
const nodes: Node[] = response.nodes.map((node) => ({
id: node.uid,
type: NODE,
flow: node.id,
namespace: node.namespace,
metadata: {subtype},
}));
const edges: Edge[] = response.edges.map((edge) => ({
id: uuid(),
type: EDGE,
source: edge.source,
target: edge.target,
}));
return [...nodes.map((node) => ({data: node}) as Element), ...edges.map((edge) => ({data: edge}) as Element)];
return [
...nodes.map((node) => ({data: node}) as Element),
...edges.map((edge) => ({data: edge}) as Element),
];
}

View File

@@ -4,6 +4,7 @@ export const EDGE = "EDGE" as const;
export const FLOW = "FLOW" as const;
export const EXECUTION = "EXECUTION" as const;
export const NAMESPACE = "NAMESPACE" as const;
export const ASSET = "ASSET" as const;
type Flow = {
subtype: typeof FLOW;
@@ -19,12 +20,16 @@ type Namespace = {
subtype: typeof NAMESPACE;
};
type Asset = {
subtype: typeof ASSET;
};
export type Node = {
id: string;
type: "NODE";
flow: string;
namespace: string;
metadata: Flow | Execution | Namespace;
metadata: Flow | Execution | Namespace | Asset;
};
export type Edge = {

View File

@@ -25,21 +25,6 @@
<Row :rows="general" />
</div>
<el-divider />
<div class="labels">
<Row :rows="[{icon: LabelMultiple, label: $t('labels')}]">
<template #action>
<SetLabels :execution />
</template>
</Row>
<Labels :labels="execution.labels || []" />
</div>
<el-divider />
<div class="metadata">
<Row :rows="metadata" />
</div>
<el-divider />
<div class="actions">
<Row
@@ -60,6 +45,21 @@
</el-col>
</el-row>
</div>
<el-divider />
<div class="metadata">
<Row :rows="metadata" />
</div>
<el-divider />
<div class="labels">
<Row :rows="[{icon: LabelMultiple, label: $t('labels')}]">
<template #action>
<SetLabels :execution />
</template>
</Row>
<Labels :labels="execution.labels || []" />
</div>
</div>
</el-splitter-panel>
@@ -192,7 +192,7 @@
import ErrorAlert from "./components/main/ErrorAlert.vue";
import Id from "../../Id.vue";
import Cascader from "./components/main/cascaders/Cascader.vue";
import Cascader, {type Element} from "./components/main/cascaders/Cascader.vue";
import TimeSeries from "../../dashboard/sections/TimeSeries.vue";
import PrevNext from "./components/main/PrevNext.vue";
@@ -406,7 +406,7 @@
);
};
const cascaders = [
const cascaders: Element[] = [
{
title: t("variables"),
empty: t("no_variables"),

View File

@@ -83,13 +83,20 @@
children?: Node[];
}
const props = defineProps<{
type DebugTypes = "outputs" | "trigger";
export type Element = {
title: string;
empty: string;
elements?: Record<string, any>;
includeDebug?: "outputs" | "trigger";
execution: Execution;
}>();
includeDebug?: DebugTypes | undefined;
}
const props = defineProps<
Element & {
execution: Execution;
}
>();
const path = ref<string>("");

View File

@@ -152,6 +152,8 @@
font-size: 12px;
color: var(--ks-content-primary);
white-space: nowrap;
display: flex;
align-items: center;
}
.value {
font-weight: 700;

View File

@@ -86,7 +86,7 @@
);
const isKVPairFilter = computed(() =>
props.filterKey?.valueType === "key-value" || (props.filterKey?.key === "labels" && KV_COMPARATORS.includes(state.selectedComparator))
props.filterKey?.valueType === "key-value"
);
const valueComponent = computed(() => {

View File

@@ -14,7 +14,6 @@ import {
COMPARATOR_LABELS,
Comparators,
TEXT_COMPARATORS,
KV_COMPARATORS
} from "../utils/filterTypes";
import {usePreAppliedFilters} from "./usePreAppliedFilters";
import {useDefaultFilter} from "./useDefaultFilter";
@@ -67,11 +66,11 @@ export function useFilters(
};
const clearLegacyParams = (query: Record<string, any>) => {
configuration.keys?.forEach(({key}) => {
configuration.keys?.forEach(({key, valueType}) => {
delete query[key];
if (key === "details") {
if (valueType === "key-value") {
Object.keys(query).forEach(queryKey => {
if (queryKey.startsWith("details.")) delete query[queryKey];
if (queryKey.startsWith(`${key}.`)) delete query[queryKey];
});
}
});
@@ -85,10 +84,10 @@ export function useFilters(
*/
const buildLegacyQuery = (query: Record<string, any>) => {
getUniqueFilters(appliedFilters.value.filter(isValidFilter)).forEach(filter => {
if (filter.key === "details") {
if (configuration.keys?.find(k => k.key === filter.key)?.valueType === "key-value") {
(filter.value as string[]).forEach(item => {
const [k, v] = item.split(":");
query[`details.${k}`] = v;
query[`${filter.key}.${k}`] = v;
});
} else if (Array.isArray(filter.value)) {
filter.value.forEach(item =>
@@ -108,8 +107,6 @@ export function useFilters(
const query = {...route.query};
clearFilterQueryParams(query);
delete query.page;
if (legacyQuery) {
clearLegacyParams(query);
buildLegacyQuery(query);
@@ -119,6 +116,15 @@ export function useFilters(
}
updateSearchQuery(query);
if (
(appliedFilters.value.some(f => Array.isArray(f.value) && f.value.length > 0)
|| searchQuery.value.trim())
&& parseInt(String(query.page ?? "1")) > 1
) {
delete query.page;
}
router.push({query});
};
@@ -145,14 +151,13 @@ export function useFilters(
value: string | string[]
): AppliedFilter => {
const comparator = (config?.comparators?.[0] as Comparators) ?? Comparators.EQUALS;
const valueLabel = Array.isArray(value)
? key === "details" && value.length > 1
? `${value[0]} +${value.length - 1}`
return createAppliedFilter(key, config, comparator, value,
config?.valueType === "key-value" && Array.isArray(value)
? value.length > 1 ? `${value[0]} +${value.length - 1}` : value[0] ?? ""
: Array.isArray(value)
? value.join(", ")
: value[0]
: (value as string);
return createAppliedFilter(key, config, comparator, value, valueLabel, "EQUALS");
: value as string
, "EQUALS");
};
const createTimeRangeFilter = (
@@ -161,14 +166,13 @@ export function useFilters(
endDate: Date,
comparator = Comparators.EQUALS
): AppliedFilter => {
const valueLabel = `${startDate.toLocaleDateString()} - ${endDate.toLocaleDateString()}`;
return {
...createAppliedFilter(
"timeRange",
config,
comparator,
{startDate, endDate},
valueLabel,
`${startDate.toLocaleDateString()} - ${endDate.toLocaleDateString()}`,
keyOfComparator(comparator)
),
comparatorLabel: "Is Between"
@@ -181,34 +185,36 @@ export function useFilters(
*/
const parseLegacyFilters = (): AppliedFilter[] => {
const filtersMap = new Map<string, AppliedFilter>();
const details: string[] = [];
const keyValueFilters: Record<string, string[]> = {};
Object.entries(route.query).forEach(([key, value]) => {
if (["q", "search", "filters[q][EQUALS]"].includes(key)) return;
if (key.startsWith("details.")) {
details.push(`${key.split(".")[1]}:${value}`);
const kvConfig = configuration.keys?.find(k => key.startsWith(`${k.key}.`) && k.valueType === "key-value");
if (kvConfig) {
if (!keyValueFilters[kvConfig.key]) keyValueFilters[kvConfig.key] = [];
keyValueFilters[kvConfig.key].push(`${key.split(".")[1]}:${value}`);
return;
}
const config = configuration.keys?.find(k => k.key === key);
if (!config) return;
const processedValue = Array.isArray(value)
? (value as string[]).filter(v => v !== null)
: config?.valueType === "multi-select"
? ((value as string) ?? "").split(",")
: ((value as string) ?? "");
filtersMap.set(key, createFilter(key, config, processedValue));
filtersMap.set(key, createFilter(key, config,
Array.isArray(value)
? (value as string[]).filter(v => v !== null)
: config?.valueType === "multi-select"
? ((value as string) ?? "").split(",")
: ((value as string) ?? "")
));
});
if (details.length > 0) {
const config = configuration.keys?.find(k => k.key === "details");
Object.entries(keyValueFilters).forEach(([key, values]) => {
const config = configuration.keys?.find(k => k.key === key);
if (config) {
filtersMap.set("details", createFilter("details", config, details));
filtersMap.set(key, createFilter(key, config, values));
}
}
});
if (route.query.startDate && route.query.endDate) {
const timeRangeConfig = configuration.keys?.find(k => k.key === "timeRange");
@@ -227,13 +233,10 @@ export function useFilters(
return Array.from(filtersMap.values());
};
const isKVFilter = (field: string, comparator: Comparators) =>
field === "details" || (field === "labels" && KV_COMPARATORS.includes(comparator));
const processFieldValue = (config: any, params: any[], field: string, comparator: Comparators) => {
const processFieldValue = (config: any, params: any[], _field: string, comparator: Comparators) => {
const isTextOp = TEXT_COMPARATORS.includes(comparator);
if (isKVFilter(field, comparator)) {
if (config?.valueType === "key-value") {
const combinedValue = params.map(p => p?.value as string);
return {
value: combinedValue,
@@ -253,10 +256,9 @@ export function useFilters(
};
}
const param = params[0];
let value = Array.isArray(param?.value)
? param.value[0]
: (param?.value as string);
let value = Array.isArray(params[0]?.value)
? params[0].value[0]
: (params[0]?.value as string);
if (config?.valueType === "date" && typeof value === "string") {
value = new Date(value);

View File

@@ -127,7 +127,7 @@ export const useExecutionFilter = (): ComputedRef<FilterConfiguration> => {
label: t("filter.labels_execution.label"),
description: t("filter.labels_execution.description"),
comparators: [Comparators.EQUALS, Comparators.NOT_EQUALS],
valueType: "text",
valueType: "key-value",
},
{
key: "triggerExecutionId",

View File

@@ -74,7 +74,7 @@ export const useFlowExecutionFilter = (): ComputedRef<FilterConfiguration> => {
label: t("filter.labels_execution.label"),
description: t("filter.labels_execution.description"),
comparators: [Comparators.EQUALS, Comparators.NOT_EQUALS],
valueType: "text",
valueType: "key-value",
},
{
key: "triggerExecutionId",

View File

@@ -67,7 +67,7 @@ export const useFlowFilter = (): ComputedRef<FilterConfiguration> => {
label: t("filter.labels_flow.label"),
description: t("filter.labels_flow.description"),
comparators: [Comparators.EQUALS, Comparators.NOT_EQUALS],
valueType: "text",
valueType: "key-value",
},
]
};

View File

@@ -25,7 +25,7 @@ export const decodeSearchParams = (query: LocationQuery) =>
const [, field, operation, subKey] = match;
if (field === "labels" && subKey) {
if (subKey) {
return {
field,
value: `${subKey}:${decodeURIComponentSafely(value)}`,
@@ -57,30 +57,19 @@ export const encodeFiltersToQuery = (filters: Filter[], keyOfComparator: (compar
query[`filters[${key}][${comparatorKey}]`] = value?.toString() ?? "";
}
return query;
case "labels":
if (Array.isArray(value)) {
value.forEach((label: string) => {
const [k, v] = label.split(":", 2);
if (k && v) query[`filters[labels][${comparatorKey}][${k}]`] = v;
});
} else if (typeof value === "string") {
const [k, v] = value.split(":", 2);
if (k && v) {
query[`filters[labels][${comparatorKey}][${k}]`] = v;
} else {
query[`filters[${key}][${comparatorKey}]`] = value;
}
}
return query;
default: {
const processedValue = Array.isArray(value)
? value.join(",")
: typeof value === "object" && "startDate" in value
? `${value.startDate.toISOString()},${value.endDate.toISOString()}`
if (Array.isArray(value) && value.some(v => typeof v === "string" && v.includes(":"))) {
value.forEach((item: string) => {
const [k, v] = item.split(":", 2);
if (k && v) query[`filters[${key}][${comparatorKey}][${k}]`] = v;
});
} else {
query[`filters[${key}][${comparatorKey}]`] = Array.isArray(value)
? value.join(",")
: value instanceof Date
? value.toISOString()
: value;
query[`filters[${key}][${comparatorKey}]`] = processedValue?.toString() ?? "";
: value?.toString() ?? "";
}
return query;
}
}

View File

@@ -1,16 +1,13 @@
<template>
<el-tooltip placement="bottom" :content="t('playground.tooltip_persistence')">
<el-switch v-model="playgroundStore.enabled" :activeText="t('playground.toggle')" class="toggle" :class="{'is-active': playgroundStore.enabled}" />
<el-tooltip placement="bottom" :content="$t('playground.tooltip_persistence')">
<el-switch v-model="playgroundStore.enabled" :activeText="$t('playground.toggle')" class="toggle" :class="{'is-active': playgroundStore.enabled}" />
</el-tooltip>
</template>
<script setup lang="ts">
import {useI18n} from "vue-i18n";
import {usePlaygroundStore} from "../../stores/playground";
const {t} = useI18n();
const playgroundStore = usePlaygroundStore();
</script>
<style scoped lang="scss">

View File

@@ -6,7 +6,7 @@
<Keyboard />
</el-icon>
<span class="fs-6">
{{ t("editor_shortcuts.label") }}
{{ $t("editor_shortcuts.label") }}
</span>
</div>
</template>
@@ -27,7 +27,7 @@
</template>
</div>
<div class="text-break">
{{ t(command.description) }}
{{ $t(command.description) }}
</div>
</div>
</div>
@@ -35,11 +35,9 @@
</template>
<script setup lang="ts">
import {useI18n} from "vue-i18n";
import Keyboard from "vue-material-design-icons/Keyboard.vue";
import {useKeyShortcuts} from "../../utils/useKeyShortcuts";
const {t} = useI18n();
const {isKeyShortcutsDialogShown} = useKeyShortcuts();
const commands = [

View File

@@ -97,7 +97,6 @@
<script setup lang="ts">
import {
computed,
getCurrentInstance,
h,
inject,
onBeforeUnmount,
@@ -126,7 +125,7 @@
import uniqBy from "lodash/uniqBy";
import {useI18n} from "vue-i18n";
import {ElDatePicker} from "element-plus";
import {Moment} from "moment";
import moment, {Moment} from "moment";
import PlaceholderContentWidget from "../../composables/monaco/PlaceholderContentWidget";
import Utils from "../../utils/utils";
import {hashCode} from "../../utils/global";
@@ -137,7 +136,6 @@
import EditorType = editor.EditorType;
import {useRoute} from "vue-router";
const currentInstance = getCurrentInstance()!;
const {t} = useI18n();
const textAreaValue = computed({
@@ -371,8 +369,7 @@
}
}, {deep: true});
const nowMoment: Moment = currentInstance.appContext.config.globalProperties.$moment().startOf("day");
const nowMoment: Moment = moment().startOf("day");
function addedSuggestRows(mutations: MutationRecord[]) {
return mutations.flatMap(({addedNodes}) => {
const nodes = [...addedNodes];
@@ -461,7 +458,7 @@
endColumn: wordAtPosition?.endColumn ?? position?.column
},
// We don't use the selectedDate directly because if user modifies the input value directly it doesn't work otherwise
text: `${currentInstance.appContext.config.globalProperties.$moment(
text: `${moment(
datePicker.value!.$el.nextElementSibling.querySelector("input").value
).toISOString(true)} `,
forceMoveMarkers: true

View File

@@ -26,8 +26,16 @@
}
const props = withDefaults(
defineProps<{ labels: Label[]; readOnly?: boolean }>(),
{labels: () => [], readOnly: false},
defineProps<{
labels?: Label[];
readOnly?: boolean;
filterType?: "labels" | "metadata";
}>(),
{
labels: () => [],
readOnly: false,
filterType: "labels",
},
);
import {decodeSearchParams} from "../../components/filter/utils/helpers";
@@ -48,7 +56,7 @@
};
const updateLabel = (label: Label) => {
const getKey = (key: string) => `filters[labels][EQUALS][${key}]`;
const getKey = (key: string) => `filters[${props.filterType}][EQUALS][${key}]`;
if (isChecked(label)) {
const replacementQuery = {...route.query};

View File

@@ -1,11 +1,11 @@
<template>
<nav class="d-flex align-items-center w-100 gap-3 top-bar">
<SidebarToggleButton
v-if="layoutStore.sideMenuCollapsed"
@toggle="layoutStore.setSideMenuCollapsed(false)"
/>
<div class="d-flex flex-column flex-grow-1 flex-shrink-1 overflow-hidden top-title">
<div class="d-flex align-items-end gap-2">
<SidebarToggleButton
v-if="layoutStore.sideMenuCollapsed"
@toggle="layoutStore.setSideMenuCollapsed(false)"
/>
<div class="d-flex flex-column gap-2">
<el-breadcrumb v-if="breadcrumb">
<el-breadcrumb-item v-for="(item, x) in breadcrumb" :key="x" :class="{'pe-none': item.disabled}">
@@ -33,6 +33,11 @@
@click="onStarClick"
/>
</h1>
<div class="description">
<slot name="description">
{{ longDescription }}
</slot>
</div>
</div>
</div>
</div>
@@ -77,15 +82,20 @@
const props = defineProps<{
title: string;
description?: string;
breadcrumb?: { label: string; link?: RouterLinkTo; disabled?: boolean }[];
longDescription?: string;
breadcrumb?: {
label: string;
link?: RouterLinkTo;
disabled?: boolean;
}[];
beta?: boolean;
}>();
const logsStore = useLogsStore();
const bookmarksStore = useBookmarksStore();
const flowStore = useFlowStore();
const route = useRoute();
const logsStore = useLogsStore();
const flowStore = useFlowStore();
const layoutStore = useLayoutStore();
const bookmarksStore = useBookmarksStore();
const shouldDisplayDeleteButton = computed(() => {
@@ -182,6 +192,12 @@
align-items: center;
}
.description {
font-size: 0.875rem;
margin-top: -0.5rem;
color: var(--ks-content-secondary);
}
.icon {
border: none;
color: var(--ks-content-tertiary);

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

View File

@@ -8,6 +8,7 @@ import plugins from "./assets/visuals/plugins.png";
import triggers from "./assets/visuals/triggers.png";
import versionPlugin from "./assets/visuals/versionPlugin.png";
import panels from "./assets/visuals/panels.png";
import assets from "./assets/visuals/assets.png";
export const images: Record<string, string> = {
announcements,
@@ -18,8 +19,10 @@ export const images: Record<string, string> = {
"dependencies.FLOW": dependencies,
"dependencies.EXECUTION": dependencies,
"dependencies.NAMESPACE": dependencies,
"dependencies.ASSET": dependencies,
plugins,
triggers,
versionPlugin,
panels,
assets
};

View File

@@ -43,7 +43,9 @@
BLOCK_SCHEMA_PATH_INJECTION_KEY,
CLOSE_TASK_FUNCTION_INJECTION_KEY,
CREATE_TASK_FUNCTION_INJECTION_KEY,
CREATING_FLOW_INJECTION_KEY,
CREATING_TASK_INJECTION_KEY,
DEFAULT_NAMESPACE_INJECTION_KEY,
EDIT_TASK_FUNCTION_INJECTION_KEY,
EDITING_TASK_INJECTION_KEY,
FIELDNAME_INJECTION_KEY,
@@ -55,7 +57,7 @@
REF_PATH_INJECTION_KEY,
ROOT_SCHEMA_INJECTION_KEY,
SCHEMA_DEFINITIONS_INJECTION_KEY,
UPDATE_TASK_FUNCTION_INJECTION_KEY,
UPDATE_YAML_FUNCTION_INJECTION_KEY,
} from "./injectionKeys";
import {useFlowFields, SECTIONS_IDS} from "./utils/useFlowFields";
import debounce from "lodash/debounce";
@@ -65,6 +67,7 @@
import {useKeyboardSave} from "./utils/useKeyboardSave";
import {deepEqual} from "../../utils/utils";
import {useScrollMemory} from "../../composables/useScrollMemory";
import {defaultNamespace} from "../../composables/useNamespaces";
const props = defineProps<NoCodeProps>();
@@ -166,6 +169,8 @@
provide(REF_PATH_INJECTION_KEY, props.refPath);
provide(PANEL_INJECTION_KEY, panel)
provide(POSITION_INJECTION_KEY, props.position ?? "after");
provide(CREATING_FLOW_INJECTION_KEY, flowStore.isCreating ?? false);
provide(DEFAULT_NAMESPACE_INJECTION_KEY, computed(() => flowStore.flow?.namespace ?? defaultNamespace() ?? "company.team"));
provide(CREATING_TASK_INJECTION_KEY, props.creatingTask);
provide(EDITING_TASK_INJECTION_KEY, props.editingTask);
provide(FIELDNAME_INJECTION_KEY, props.fieldName);
@@ -184,7 +189,7 @@
emit("closeTask")
})
provide(UPDATE_TASK_FUNCTION_INJECTION_KEY, (yaml) => {
provide(UPDATE_YAML_FUNCTION_INJECTION_KEY, (yaml) => {
editorUpdate(yaml)
})

View File

@@ -44,7 +44,6 @@
<script setup lang="ts">
import {computed, inject, ref} from "vue";
import {BLOCK_SCHEMA_PATH_INJECTION_KEY} from "../../injectionKeys";
import {useFlowStore} from "../../../../stores/flow";
import Creation from "./taskList/buttons/Creation.vue";
import Element from "./taskList/Element.vue";
import * as YAML_UTILS from "@kestra-io/ui-libs/flow-yaml-utils";
@@ -53,7 +52,7 @@
import {
CREATING_TASK_INJECTION_KEY, FULL_SCHEMA_INJECTION_KEY, FULL_SOURCE_INJECTION_KEY,
PARENT_PATH_INJECTION_KEY, REF_PATH_INJECTION_KEY,
PARENT_PATH_INJECTION_KEY, REF_PATH_INJECTION_KEY, UPDATE_YAML_FUNCTION_INJECTION_KEY,
} from "../../injectionKeys";
import {SECTIONS_MAP} from "../../../../utils/constants";
import {getValueAtJsonPath} from "../../../../utils/utils";
@@ -83,8 +82,6 @@
inheritAttrs: false
});
const flowStore = useFlowStore();
interface Task {
id:string,
type:string
@@ -150,6 +147,8 @@
const movedIndex = ref(-1);
const updateYaml = inject(UPDATE_YAML_FUNCTION_INJECTION_KEY, () => {});
const moveElement = (
items: Record<string, any>[] | undefined,
elementID: string,
@@ -171,7 +170,7 @@
movedIndex.value = -1;
}, 200);
flowStore.flowYaml =
updateYaml(
YAML_UTILS.swapBlocks({
source:flow.value,
section: SECTIONS_MAP[section.value.toLowerCase() as keyof typeof SECTIONS_MAP],
@@ -179,6 +178,7 @@
key2:items[newIndex][keyName],
keyName,
})
);
};
const fullSchema = inject(FULL_SCHEMA_INJECTION_KEY, ref<Record<string, any>>({}));

View File

@@ -8,18 +8,17 @@
</template>
<script lang="ts" setup>
import {computed, onMounted} from "vue";
import {useFlowStore} from "../../../../stores/flow";
import {onMounted, inject, computed, provide} from "vue";
import NamespaceSelect from "../../../namespaces/components/NamespaceSelect.vue";
import {CREATING_FLOW_INJECTION_KEY, DEFAULT_NAMESPACE_INJECTION_KEY} from "../../injectionKeys";
const modelValue = defineModel<string>();
const flowStore = useFlowStore();
const isCreating = computed(() => flowStore.isCreating);
const isCreating = inject(CREATING_FLOW_INJECTION_KEY, false);
const defaultNamespace = inject(DEFAULT_NAMESPACE_INJECTION_KEY, computed(() => ""));
provide(DEFAULT_NAMESPACE_INJECTION_KEY, computed(() => modelValue.value || defaultNamespace.value));
onMounted(() => {
const flowNamespace = flowStore.flow?.namespace;
const flowNamespace = defaultNamespace.value;
if (!modelValue.value && flowNamespace) {
modelValue.value = flowNamespace;
}

View File

@@ -28,7 +28,15 @@
</Wrapper>
</template>
</el-collapse-item>
<el-collapse-item name="general" v-if="generalProperties?.length" :title="$t('no_code.sections.general')">
<template v-for="[fieldKey, fieldSchema] in generalProperties" :key="fieldKey">
<Wrapper>
<template #tasks>
<TaskObjectField v-bind="fieldProps(fieldKey, fieldSchema)" />
</template>
</Wrapper>
</template>
</el-collapse-item>
<el-collapse-item name="deprecated" v-if="deprecatedProperties?.length" :title="$t('no_code.sections.deprecated')">
<template v-for="[fieldKey, fieldSchema] in deprecatedProperties" :key="fieldKey">
<Wrapper>
@@ -125,6 +133,16 @@
return value?.$deprecated;
}
function isPartOfGroup(value: any, groups: string[]) {
if (value?.allOf) {
return value.allOf.some((item: any) => isPartOfGroup(item, groups));
}
if (value?.anyOf) {
return value.anyOf.some((item: any) => isPartOfGroup(item, groups));
}
return value?.$group && groups.includes(value.$group);
}
const filteredProperties = computed<Entry[]>(() => {
const propertiesProc = (props.properties ?? props.schema?.properties);
const isOutputsContext = props.root?.startsWith("outputs[") || false;
@@ -159,9 +177,17 @@
const protectedRequiredProperties = computed<Entry[]>(() => {
return requiredProperties.value.length ? requiredProperties.value : sortedProperties.value;
});
const connectionProperties = computed<Entry[]>(() => {
return props.merge ? [] : sortedProperties.value.filter(([p, v]) => v && !isRequired(p) && isPartOfGroup(v, ["connection"]));
});
const optionalProperties = computed<Entry[]>(() => {
return props.merge ? [] : sortedProperties.value.filter(([p, v]) => v && !isRequired(p) && !isDeprecated(v) && v.$group !== "connection");
return props.merge ? [] : sortedProperties.value.filter(([p, v]) => v && !isRequired(p) && !isDeprecated(v) && !isPartOfGroup(v, ["core","connection"]));
});
const generalProperties = computed<Entry[]>(() => {
return props.merge ? [] : sortedProperties.value.filter(([p, v]) => v && !isRequired(p) && !isDeprecated(v) && isPartOfGroup(v, ["core"]));
});
const deprecatedProperties = computed<Entry[]>(() => {
@@ -169,9 +195,6 @@
return props.merge ? [] : sortedProperties.value.filter(([k, v]) => v && isDeprecated(v) && obj[k] !== undefined);
});
const connectionProperties = computed<Entry[]>(() => {
return props.merge ? [] : sortedProperties.value.filter(([p, v]) => v && v.$group === "connection" && !isRequired(p));
});
function onInput(value: any) {
emit("update:modelValue", collapseEmptyValues(value));

View File

@@ -24,6 +24,7 @@ export const POSITION_INJECTION_KEY = Symbol("position-injection-key") as Inject
* NOTE: different from the `isCreating` flag coming from the store. `isCreating` refers to the Complete flow being in creation
*/
export const CREATING_TASK_INJECTION_KEY = Symbol("creating-injection-key") as InjectionKey<boolean>
export const CREATING_FLOW_INJECTION_KEY = Symbol("creating-flow-injection-key") as InjectionKey<boolean>
/**
* When creating anew task, allows to specify a field where the new task should be injected.
* @example
@@ -51,9 +52,9 @@ export const EDIT_TASK_FUNCTION_INJECTION_KEY = Symbol("edit-function-injection-
*/
export const CLOSE_TASK_FUNCTION_INJECTION_KEY = Symbol("close-function-injection-key") as InjectionKey<() => void>
/**
* We call this function when a task is changed, as soon as the first click or type is done
* Call this function to update the full Yaml content
*/
export const UPDATE_TASK_FUNCTION_INJECTION_KEY = Symbol("update-function-injection-key") as InjectionKey<(yaml: string) => void>
export const UPDATE_YAML_FUNCTION_INJECTION_KEY = Symbol("update-function-injection-key") as InjectionKey<(yaml: string) => void>
/**
* Set this to override the contents of the no-code editor with a component of your choice
* This is used to display the metadata edition inputs
@@ -92,4 +93,6 @@ export const SCHEMA_DEFINITIONS_INJECTION_KEY = Symbol("schema-definitions-injec
export const DATA_TYPES_MAP_INJECTION_KEY = Symbol("data-types-injection-key") as InjectionKey<ComputedRef<Record<string, string[] | undefined>>>
export const ON_TASK_EDITOR_CLICK_INJECTION_KEY = Symbol("on-task-editor-click-injection-key") as InjectionKey<(elt?: Partial<NoCodeElement>) => void>;
export const ON_TASK_EDITOR_CLICK_INJECTION_KEY = Symbol("on-task-editor-click-injection-key") as InjectionKey<(elt?: Partial<NoCodeElement>) => void>;
export const DEFAULT_NAMESPACE_INJECTION_KEY = Symbol("default-namespace-injection-key") as InjectionKey<ComputedRef<string>>;

View File

@@ -16,7 +16,7 @@
import {PLUGIN_DEFAULTS_SECTION, SECTIONS_MAP} from "../../../utils/constants";
import {
CLOSE_TASK_FUNCTION_INJECTION_KEY,
UPDATE_TASK_FUNCTION_INJECTION_KEY,
UPDATE_YAML_FUNCTION_INJECTION_KEY,
FULL_SOURCE_INJECTION_KEY, CREATING_TASK_INJECTION_KEY,
PARENT_PATH_INJECTION_KEY, POSITION_INJECTION_KEY,
REF_PATH_INJECTION_KEY, EDIT_TASK_FUNCTION_INJECTION_KEY,
@@ -37,7 +37,7 @@
const fieldName = inject(FIELDNAME_INJECTION_KEY, undefined);
const blockSchemaPath = inject(BLOCK_SCHEMA_PATH_INJECTION_KEY, ref(""));
const updateTask = inject(UPDATE_TASK_FUNCTION_INJECTION_KEY, () => {})
const updateTask = inject(UPDATE_YAML_FUNCTION_INJECTION_KEY, () => {})
const closeTaskAddition = inject(
CLOSE_TASK_FUNCTION_INJECTION_KEY,

View File

@@ -3,7 +3,7 @@
<template #additional-right>
<ul>
<li>
<el-button v-if="canCreate" tag="router-link" :to="{name: 'flows/create', query: {namespace: $route.query.namespace}}" :icon="Plus" type="primary">
<el-button v-if="canCreate" tag="router-link" :to="{name: 'flows/create', query: {namespace: $route.query.namespace}}" :icon="Plus" type="secondary">
{{ $t('create_flow') }}
</el-button>
</li>
@@ -30,7 +30,7 @@
<el-button
v-if="isOSS"
@click="startTour"
:icon="Plus"
:icon="Compass"
size="large"
type="primary"
class="px-3 p-4 section-1-link product-link"
@@ -39,7 +39,7 @@
</el-button>
<el-button
v-else
:icon="Plus"
:icon="Compass"
tag="router-link"
:to="{name: 'flows/create'}"
size="large"
@@ -74,6 +74,7 @@
import {useCoreStore} from "../../stores/core";
import {useI18n} from "vue-i18n";
import Plus from "vue-material-design-icons/Plus.vue";
import Compass from "vue-material-design-icons/Compass.vue";
import Play from "vue-material-design-icons/Play.vue";
import OnboardingBottom from "override/components/OnboardingBottom.vue";
import kestraWelcome from "../../assets/onboarding/kestra_welcome.svg";

View File

@@ -156,7 +156,7 @@
<el-form-item :label="$t('secret.key')" prop="key">
<el-input v-model="secret.key" :disabled="secret.update" required />
</el-form-item>
<el-form-item v-if="!secret.update" :label="$t('secret.name')" prop="value">
<el-form-item v-if="!secret.update" :label="$t('secret.name')" prop="value" required>
<MultilineSecret v-model="secret.value" :placeholder="secretModalTitle" />
</el-form-item>
<el-form-item v-if="secret.update" :label="$t('secret.name')" prop="value">

View File

@@ -1,3 +1,4 @@
import {computed, watch} from "vue";
import {useI18n} from "vue-i18n";
import {configureMonacoYaml} from "monaco-yaml";
import * as monaco from "monaco-editor/esm/vs/editor/editor.api";
@@ -21,6 +22,7 @@ import {
registerPebbleAutocompletion
} from "./pebbleLanguageConfigurator";
import {usePluginsStore} from "../../../stores/plugins";
import {useBlueprintsStore} from "../../../stores/blueprints";
import {languages} from "monaco-editor/esm/vs/editor/editor.api";
import CompletionItem = languages.CompletionItem;
@@ -34,11 +36,14 @@ export class YamlLanguageConfigurator extends AbstractLanguageConfigurator {
}
async configureLanguage(pluginsStore: ReturnType<typeof usePluginsStore>) {
const validateYAML = computed(() => useBlueprintsStore().validateYAML);
watch(validateYAML, (shouldValidate) => configureMonacoYaml(monaco, {validate: shouldValidate}));
configureMonacoYaml(monaco, {
enableSchemaRequest: true,
hover: localStorage.getItem("hoverTextEditor") === "true",
completion: true,
validate: true,
validate: validateYAML.value ?? true,
format: true,
schemas: yamlSchemas()
});

View File

@@ -1,5 +1,11 @@
<template>
<SideBar v-if="menu" :menu :showLink="showLink" @menu-collapse="onCollapse">
<SideBar
v-if="menu"
:menu
:showLink
@menu-collapse="onCollapse"
:class="{overlay: verticalLayout}"
>
<template #footer>
<Auth />
</template>
@@ -11,6 +17,9 @@
import SideBar from "../../components/layout/SideBar.vue";
import Auth from "../../override/components/auth/Auth.vue";
import {useBreakpoints, breakpointsElement} from "@vueuse/core";
const verticalLayout = useBreakpoints(breakpointsElement).smallerOrEqual("sm");
withDefaults(defineProps<{
showLink?: boolean
}>(), {

View File

@@ -56,7 +56,10 @@
<div v-if="!system && blueprint.tags?.length > 0" class="tags-section">
<span v-for="tag in processedTags(blueprint.tags)" :key="tag.original" class="tag-item">{{ tag.display }}</span>
</div>
<div class="text-section">
<div v-if="blueprint.template" class="tags-section">
<span class="tag-item">{{ $t('template') }}</span>
</div>
<div class="text-section">
<h3 class="title">
{{ blueprint.title ?? blueprint.id }}
</h3>
@@ -151,6 +154,7 @@
id: string;
tags: string[];
title?: string;
template?: Record<string, any>;
}[] | undefined>(undefined);
const error = ref(false);
const icon = {ContentCopy};

View File

@@ -2,7 +2,7 @@
<Navbar :title="routeInfo.title">
<template #additional-right>
<Action
v-if="canCreate"
v-if="!isOSS && canCreate"
:label="t('create')"
:to="{name: 'namespaces/create', params: {tab: 'edit'}}"
/>
@@ -142,6 +142,8 @@
const systemNamespace = computed(
() => miscStore.configs?.systemNamespace || "system",
);
const isOSS = computed(() => useMiscStore().configs?.edition === "OSS")
const namespacesHierarchy = computed(() => {
if (namespaces.value === undefined || namespaces.value.length === 0) {

Some files were not shown because too many files have changed in this diff Show More