Compare commits

...

66 Commits

Author SHA1 Message Date
YannC.
baa07dd02b fix: disabled flakky test shouldGetReport 2025-09-30 13:16:48 +02:00
github-actions[bot]
260cb50651 chore(version): update to version '1.0.3' 2025-09-30 07:07:34 +00:00
YannC
0a45325c69 fix(ui): avoid having a authentication dialog open when credentials are wrong (#11576) 2025-09-30 09:00:55 +02:00
Florian Hussonnois
c2522e2544 fix(triggers): do not resolve recoverMissedSchedule when enabling back a trigger
Add some refactoring to allow some methods to be overrided
2025-09-29 20:43:35 +02:00
Florian Hussonnois
27476279ae fix(triggers): handle RecoverMissedSchedules on trigger batch update
* Fix and clean code in TriggerController
* Remove duplicate code in Trigger class
2025-09-29 20:43:34 +02:00
YannC.
3cc6372cb5 fix: missing import due to backport 2025-09-29 18:09:25 +02:00
YannC
5f6e9dbe06 fix(dashboard): show startDate instead of duration in defaults, and avoid formatting date in JDBC if there is no aggregations (#11467)
close #5867
2025-09-29 17:51:36 +02:00
yuri1969
5078ce741d fix(core): enable runIf at execution updating tasks 2025-09-25 14:46:08 +02:00
github-actions[bot]
b7e17b7114 chore(version): update to version '1.0.2' 2025-09-24 08:03:43 +00:00
nKwiatkowski
acaee34b0e chore(version): update to version '1.0.1' 2025-09-24 10:03:23 +02:00
github-actions[bot]
1d78332505 chore(version): update to version '1.0.2' 2025-09-24 08:02:25 +00:00
nKwiatkowski
7249632510 fix(tests): disable flaky test that prevent the release 2025-09-24 10:01:43 +02:00
Sanjay Ramsinghani
4a66a08c3b chore(core): align toggle icon in failed execution collapse element (#11430)
Closes https://github.com/kestra-io/kestra/issues/11406.

Co-authored-by: Miloš Paunović <paun992@hotmail.com>
2025-09-23 14:20:10 +02:00
Antoine Gauthier
22fd6e97ea chore(logs): display copy button only on row hover (#11254)
Closes https://github.com/kestra-io/kestra/issues/11220.

Co-authored-by: Miloš Paunović <paun992@hotmail.com>
2025-09-23 14:18:34 +02:00
Jaem Dessources
9afd86d32b fix(core): align copy logs button to each row’s right edge (#11216)
Closes https://github.com/kestra-io/kestra/issues/10898.

Co-authored-by: Miloš Paunović <paun992@hotmail.com>
2025-09-23 14:18:28 +02:00
github-actions[bot]
797ea6c9e4 chore(version): update to version '1.0.2' 2025-09-23 12:10:01 +00:00
nKwiatkowski
07d5e815c4 chore(version): update to version '1.0.1' 2025-09-23 14:09:38 +02:00
github-actions[bot]
33ac9b1495 chore(version): update to version '1.0.2' 2025-09-23 09:22:01 +00:00
Bart Ledoux
4d5b95d040 chore: update package-lock 2025-09-23 11:17:48 +02:00
brian-mulier-p
667aca7345 fix(ai): avoid moving cursor twice after using AI Copilot (#11451)
closes #11314
2025-09-23 10:40:32 +02:00
brian.mulier
e05cc65202 fix(system): avoid trigger locking after scheduler restart
closes #11434
2025-09-22 18:40:22 +02:00
brian.mulier
71b606c27c fix(ci): same CI as develop 2025-09-22 18:40:19 +02:00
Florian Hussonnois
47f9f12ce8 chore(websever): make kvStore method in KVController protected
Related-to: kestra-io/kestra-ee#5055
2025-09-22 13:57:59 +02:00
Florian Hussonnois
01acae5e97 feat(core): add new findMetadataAndValue to KVStore
Related-to: kestra-io/kestra-ee#5055
2025-09-22 13:57:58 +02:00
Florian Hussonnois
e5878f08b7 fix(core): fix NPE in JackMapping.applyPatchesOnJsonNode method 2025-09-22 13:57:57 +02:00
brian-mulier-p
0bcb6b4e0d fix(tests): enforce closing consumers after each tests (#11399) 2025-09-19 16:35:23 +02:00
brian-mulier-p
3c2ecf4342 fix(core): avoid ClassCastException when doing secret decryption (#11393)
closes kestra-io/kestra-ee#5191
2025-09-19 11:32:27 +02:00
Piyush Bhaskar
3d4f66772e fix(core: webhook curl coomand needs tenant. 2025-09-19 14:17:00 +05:30
Sandip Mandal
e2afd4bcc3 fix(core: webhook curl coomand needs tenant. (#11391)
Co-authored-by: Piyush Bhaskar <102078527+Piyush-r-bhaskar@users.noreply.github.com>
Co-authored-by: Miloš Paunović <paun992@hotmail.com>
2025-09-19 14:10:36 +05:30
Loïc Mathieu
d143097f03 fix(executions): computing subflow outputs could fail when the executioin is failing or killing
Fixes https://github.com/kestra-io/kestra/issues/11379
2025-09-18 17:42:15 +02:00
Loïc Mathieu
72c0d91c1a fix(executions): concurrency limit should update the executioin
As if it's not updated in the database, it would not be detected as changed so that terminal actions (like purge) would not be done.

Fixes  #11022
Fixes #11025
Fixes #8143
2025-09-18 12:10:36 +02:00
Loïc Mathieu
1d692e56b0 fix(executions): the Exit task was not correctly ends parent tasks
Fixes https://github.com/kestra-io/kestra-ee/issues/5168
2025-09-18 11:39:16 +02:00
Miloš Paunović
0352d617ac chore(core): improve coloring scheme for dependencies graph (#11306) 2025-09-18 09:22:27 +02:00
Miloš Paunović
b41aa4e0b9 fix(core): adjust positioning of default tour elements (#11286)
The problem occurred when `No Code` was selected as the `Default Editor Type` in `Settings`. This `PR` resolves the issue.

Closes https://github.com/kestra-io/kestra/issues/9556.
2025-09-18 09:21:40 +02:00
Miloš Paunović
d811dc030b chore(core): ensure editor suggestion widget renders above other elements (#11258)
Closes https://github.com/kestra-io/kestra/issues/10702.
Closes https://github.com/kestra-io/kestra/issues/11033.
2025-09-18 09:21:18 +02:00
Miloš Paunović
105e62eee1 fix(namespaces): open details page at top (#11221)
Closes https://github.com/kestra-io/kestra/issues/10536.
2025-09-18 09:20:55 +02:00
Loïc Mathieu
28796862a4 fix(executions): possible NPE on dynamic taskrun
Fixes https://github.com/kestra-io/kestra-ee/issues/5166
2025-09-17 15:56:28 +02:00
brian.mulier
637cd794a4 fix(core): filters weren't applying anymore 2025-09-17 12:57:47 +02:00
Miloš Paunović
fdd5c6e63d chore(core): remove unused decompress library (#11346) 2025-09-17 11:15:43 +02:00
brian.mulier
eda2483ec9 fix(core): avoid filters from overlapping on other pages when changing query params 2025-09-17 10:37:58 +02:00
brian.mulier
7b3c296489 fix(core): avoid clearing filters when reclicking on current left menu item
closes #9476
2025-09-17 10:37:56 +02:00
brian.mulier
fe6f8b4ed9 fix(core): avoid undefined error on refresh chart 2025-09-17 10:37:04 +02:00
Roman Acevedo
17ff539690 ci: fix some non-release workflows were not using develop 2025-09-16 14:43:24 +02:00
Roman Acevedo
bbd0dda47e ci: readd back workflow-publish-docker.yml needed for release 2025-09-16 12:16:15 +02:00
github-actions[bot]
27a8e8b5a7 chore(version): update to version '1.0.1' 2025-09-16 10:00:39 +00:00
Roman Acevedo
d6620a34cd ci: try to use develop CI workflows 2025-09-16 11:38:34 +02:00
Loïc Mathieu
6f8b3c5cfd fix(flows): properly coompute flow dependencies with preconditions
When both upstream flows and where are set, it should be a AND between the two as dependencies must match the upstream flows.

Fixes #11164
2025-09-16 10:44:26 +02:00
Florian Hussonnois
6da6cbab60 fix(executions): add missing CrudEvent on purge execution
Related-to: kestra-io/kestra-ee#5061
2025-09-16 10:30:53 +02:00
Loïc Mathieu
a899e16178 fix(system): allow flattening a map with duplicated keys 2025-09-16 10:25:25 +02:00
Florian Hussonnois
568cd0b0c7 fix(core): fix CrudEvent model for DELETE operation
Refactor XxxRepository class to use new factory methods
from the CrudEvent class

Related-to: kestra-io/kestra-ee#4727
2025-09-15 18:51:36 +02:00
Loïc Mathieu
92e1dcb6eb fix(executions): truncate the execution_running table as in 0.24 there was an issue in the purge
This table contains executions for flows that have a concurrency that are currently running.
It has been added in 0.24 but in that release there was a bug that may prevent some records to being correctly removed from this table.
To fix that, we truncate it once.
2025-09-15 17:30:08 +02:00
brian-mulier-p
499e040cd0 fix(test): add tenant-in-path storage test (#11292)
part of kestra-io/storage-s3#166
2025-09-15 16:53:56 +02:00
brian-mulier-p
5916831d62 fix(security): enhance basic auth security (#11285)
closes kestra-io/kestra-ee#5111
2025-09-15 16:28:16 +02:00
Bart Ledoux
0b1b55957e fix: remove last uses of vuex as a store 2025-09-12 16:23:25 +02:00
Bart Ledoux
7ee40d376a flows: clear tasks list when last task is deleted 2025-09-12 16:15:36 +02:00
Florian Hussonnois
e2c9b3e256 fix(core): make CRC32 for plugin JARs lazy
Make CRC32 calculation for lazy plugin JAR files
to avoid excessive startup time and performance impact.

Avoid byte buffer reallocation while computing CRC32.
2025-09-12 14:02:23 +02:00
brian-mulier-p
556730777b fix(core): add ability to remap sort keys (#11233)
part of kestra-io/kestra-ee#5075
2025-09-12 09:44:32 +02:00
brian.mulier
c1a75a431f fix(ai): increase maxOutputToken default 2025-09-11 18:24:21 +02:00
brian-mulier-p
4a5b91667a fix(flows): avoid failing flow dependencies with dynamic defaults (#11166)
closes #11117
2025-09-10 16:15:04 +02:00
Roman Acevedo
f7b2af16a1 fix(flows): topology would not load when having many flows and cyclic relations
- this will probably fix https://github.com/kestra-io/kestra-ee/issues/4980

the issue was recursiveFlowTopology was returning a lot of duplicates, it was aggravated when having many Flows and multiple Flow triggers
2025-09-10 16:14:41 +02:00
Loïc Mathieu
9351cb22e0 fixsystem): always load netty from the app classloader
As Netty is used in core and a lot of plugins, and we already load project reactor from the app classloader that depends in Netty.

Fixes https://github.com/kestra-io/kestra-ee/issues/5038
2025-09-10 10:51:31 +02:00
brian-mulier-p
b1ecb82fdc fix(namespaces): avoid adding 'company.team' as default ns (#11174)
closes #11168
2025-09-09 17:14:27 +02:00
Miloš Paunović
c6d56151eb chore(flows): display correct flow dependency count (#11169)
Closes https://github.com/kestra-io/kestra/issues/11127.
2025-09-09 13:57:00 +02:00
François Delbrayelle
ed4398467a fix(outputs): open external file was not working (#11154) 2025-09-09 09:46:02 +02:00
brian-mulier-p
c51947419a chore(ci): add LTS tagging (#11131) 2025-09-08 14:10:53 +02:00
github-actions[bot]
ccb6a1f4a7 chore(version): update to version 'v1.0.0'. 2025-09-08 08:00:59 +00:00
123 changed files with 1930 additions and 1633 deletions

View File

@@ -26,7 +26,7 @@ jobs:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: "3.x"
@@ -39,7 +39,7 @@ jobs:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
- name: Set up Node
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: "20.x"

View File

@@ -37,7 +37,7 @@ jobs:
path: kestra
# Setup build
- uses: kestra-io/actions/.github/actions/setup-build@main
- uses: kestra-io/actions/composite/setup-build@main
name: Setup - Build
id: build
with:

View File

@@ -25,21 +25,13 @@ jobs:
with:
fetch-depth: 0
# Checkout GitHub Actions
- uses: actions/checkout@v5
with:
repository: kestra-io/actions
path: actions
ref: main
# Setup build
- uses: ./actions/.github/actions/setup-build
- uses: kestra-io/actions/composite/setup-build@main
id: build
with:
java-enabled: true
node-enabled: true
python-enabled: true
caches-enabled: true
# Get Plugins List
- name: Get Plugins List
@@ -60,7 +52,7 @@ jobs:
GITHUB_PAT: ${{ secrets.GH_PERSONAL_TOKEN }}
run: |
chmod +x ./dev-tools/release-plugins.sh;
./dev-tools/release-plugins.sh \
--release-version=${{github.event.inputs.releaseVersion}} \
--next-version=${{github.event.inputs.nextVersion}} \
@@ -73,7 +65,7 @@ jobs:
GITHUB_PAT: ${{ secrets.GH_PERSONAL_TOKEN }}
run: |
chmod +x ./dev-tools/release-plugins.sh;
./dev-tools/release-plugins.sh \
--release-version=${{github.event.inputs.releaseVersion}} \
--next-version=${{github.event.inputs.nextVersion}} \

View File

@@ -38,15 +38,8 @@ jobs:
fetch-depth: 0
path: kestra
# Checkout GitHub Actions
- uses: actions/checkout@v5
with:
repository: kestra-io/actions
path: actions
ref: main
# Setup build
- uses: ./actions/.github/actions/setup-build
- uses: kestra-io/actions/composite/setup-build@main
id: build
with:
java-enabled: true

View File

@@ -59,8 +59,6 @@ jobs:
needs:
- release
if: always()
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
steps:
- name: Trigger EE Workflow
uses: peter-evans/repository-dispatch@v3
@@ -70,14 +68,9 @@ jobs:
repository: kestra-io/kestra-ee
event-type: "oss-updated"
# Slack
- name: Slack - Notification
uses: Gamesight/slack-workflow-status@master
if: ${{ always() && env.SLACK_WEBHOOK_URL != 0 }}
if: ${{ failure() && env.SLACK_WEBHOOK_URL != 0 && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop') }}
uses: kestra-io/actions/composite/slack-status@main
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }}
name: GitHub Actions
icon_emoji: ":github-actions:"
channel: "C02DQ1A7JLR" # _int_git channel
webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}

View File

@@ -60,19 +60,3 @@ jobs:
name: E2E - Tests
uses: ./.github/workflows/e2e.yml
end:
name: End
runs-on: ubuntu-latest
if: always()
needs: [frontend, backend]
steps:
# Slack
- name: Slack notification
uses: Gamesight/slack-workflow-status@master
if: ${{ always() && env.SLACK_WEBHOOK_URL != 0 }}
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }}
name: GitHub Actions
icon_emoji: ":github-actions:"
channel: "C02DQ1A7JLR"

View File

@@ -21,13 +21,6 @@ jobs:
with:
fetch-depth: 0
# Checkout GitHub Actions
- uses: actions/checkout@v5
with:
repository: kestra-io/actions
path: actions
ref: main
# Setup build
- uses: ./actions/.github/actions/setup-build
id: build
@@ -70,15 +63,8 @@ jobs:
with:
fetch-depth: 0
# Checkout GitHub Actions
- uses: actions/checkout@v5
with:
repository: kestra-io/actions
path: actions
ref: main
# Setup build
- uses: ./actions/.github/actions/setup-build
- uses: kestra-io/actions/composite/setup-build@main
id: build
with:
java-enabled: false
@@ -87,7 +73,7 @@ jobs:
# Run Trivy image scan for Docker vulnerabilities, see https://github.com/aquasecurity/trivy-action
- name: Docker Vulnerabilities Check
uses: aquasecurity/trivy-action@0.33.0
uses: aquasecurity/trivy-action@0.33.1
with:
image-ref: kestra/kestra:develop
format: 'template'
@@ -115,24 +101,16 @@ jobs:
with:
fetch-depth: 0
# Checkout GitHub Actions
- uses: actions/checkout@v5
with:
repository: kestra-io/actions
path: actions
ref: main
# Setup build
- uses: ./actions/.github/actions/setup-build
- uses: kestra-io/actions/composite/setup-build@main
id: build
with:
java-enabled: false
node-enabled: false
caches-enabled: true
# Run Trivy image scan for Docker vulnerabilities, see https://github.com/aquasecurity/trivy-action
- name: Docker Vulnerabilities Check
uses: aquasecurity/trivy-action@0.33.0
uses: aquasecurity/trivy-action@0.33.1
with:
image-ref: kestra/kestra:latest
format: table

View File

@@ -20,6 +20,7 @@ permissions:
contents: write
checks: write
actions: read
pull-requests: write
jobs:
test:
@@ -35,7 +36,7 @@ jobs:
fetch-depth: 0
# Setup build
- uses: kestra-io/actions/.github/actions/setup-build@main
- uses: kestra-io/actions/composite/setup-build@main
name: Setup - Build
id: build
with:
@@ -59,84 +60,15 @@ jobs:
export GOOGLE_APPLICATION_CREDENTIALS=$HOME/.gcp-service-account.json
./gradlew check javadoc --parallel
# report test
- name: Test - Publish Test Results
uses: dorny/test-reporter@v2
if: always()
with:
name: Java Tests Report
reporter: java-junit
path: '**/build/test-results/test/TEST-*.xml'
list-suites: 'failed'
list-tests: 'failed'
fail-on-error: 'false'
token: ${{ secrets.GITHUB_AUTH_TOKEN }}
# Sonar
- name: Test - Analyze with Sonar
if: env.SONAR_TOKEN != ''
- name: comment PR with test report
if: ${{ !cancelled() && github.event_name == 'pull_request' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_AUTH_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
shell: bash
run: ./gradlew sonar --info
run: npx --yes @kestra-io/kestra-devtools generateTestReportSummary --only-errors --ci $(pwd)
# GCP
- name: GCP - Auth with unit test account
id: auth
if: always() && env.GOOGLE_SERVICE_ACCOUNT != ''
continue-on-error: true
uses: "google-github-actions/auth@v3"
with:
credentials_json: "${{ secrets.GOOGLE_SERVICE_ACCOUNT }}"
- name: GCP - Setup Cloud SDK
if: env.GOOGLE_SERVICE_ACCOUNT != ''
uses: "google-github-actions/setup-gcloud@v3"
# Allure check
- uses: rlespinasse/github-slug-action@v5
name: Allure - Generate slug variables
- name: Allure - Publish report
uses: andrcuns/allure-publish-action@v2.9.0
if: always() && env.GOOGLE_SERVICE_ACCOUNT != ''
continue-on-error: true
env:
GITHUB_AUTH_TOKEN: ${{ secrets.GITHUB_AUTH_TOKEN }}
JAVA_HOME: /usr/lib/jvm/default-jvm/
with:
storageType: gcs
resultsGlob: "**/build/allure-results"
bucket: internal-kestra-host
baseUrl: "https://internal.dev.kestra.io"
prefix: ${{ format('{0}/{1}', github.repository, 'allure/java') }}
copyLatest: true
ignoreMissingResults: true
# Jacoco
- name: Jacoco - Copy reports
if: env.GOOGLE_SERVICE_ACCOUNT != ''
continue-on-error: true
shell: bash
run: |
mv build/reports/jacoco/testCodeCoverageReport build/reports/jacoco/test/
mv build/reports/jacoco/test/testCodeCoverageReport.xml build/reports/jacoco/test/jacocoTestReport.xml
gsutil -m rsync -d -r build/reports/jacoco/test/ gs://internal-kestra-host/${{ format('{0}/{1}', github.repository, 'jacoco') }}
# Codecov
- name: Codecov - Upload coverage reports
uses: codecov/codecov-action@v5
# Report Java
- name: Report - Java
uses: kestra-io/actions/composite/report-java@main
if: ${{ !cancelled() }}
continue-on-error: true
with:
token: ${{ secrets.CODECOV_TOKEN }}
flags: backend
- name: Codecov - Upload test results
uses: codecov/test-results-action@v1
if: ${{ !cancelled() }}
continue-on-error: true
with:
token: ${{ secrets.CODECOV_TOKEN }}
flags: backend
secrets: ${{ toJSON(secrets) }}

View File

@@ -26,7 +26,7 @@ jobs:
run: npm ci
# Setup build
- uses: kestra-io/actions/.github/actions/setup-build@main
- uses: kestra-io/actions/composite/setup-build@main
name: Setup - Build
id: build
with:

View File

@@ -25,15 +25,6 @@ jobs:
fetch-depth: 0
submodules: true
# Checkout GitHub Actions
- name: Checkout - Actions
uses: actions/checkout@v5
with:
repository: kestra-io/actions
sparse-checkout-cone-mode: true
path: actions
sparse-checkout: |
.github/actions
# Download Exec
# Must be done after checkout actions
@@ -59,7 +50,7 @@ jobs:
# GitHub Release
- name: Create GitHub release
uses: ./actions/.github/actions/github-release
uses: kestra-io/actions/composite/github-release@main
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
env:
MAKE_LATEST: ${{ steps.is_latest.outputs.latest }}
@@ -82,7 +73,7 @@ jobs:
- name: Merge Release Notes
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
uses: ./actions/.github/actions/github-release-note-merge
uses: kestra-io/actions/composite/github-release-note-merge@main
env:
GITHUB_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
RELEASE_TAG: ${{ github.ref_name }}

View File

@@ -11,6 +11,14 @@ on:
options:
- "true"
- "false"
retag-lts:
description: 'Retag LTS Docker images'
required: true
type: choice
default: "false"
options:
- "true"
- "false"
release-tag:
description: 'Kestra Release Tag (by default, deduced with the ref)'
required: false
@@ -179,6 +187,11 @@ jobs:
run: |
regctl image copy ${{ format('kestra/kestra:{0}{1}', steps.vars.outputs.tag, matrix.image.name) }} ${{ format('kestra/kestra:latest{0}', matrix.image.name) }}
- name: Retag to LTS
if: startsWith(github.ref, 'refs/tags/v') && inputs.retag-lts == 'true'
run: |
regctl image copy ${{ format('kestra/kestra:{0}{1}', steps.vars.outputs.tag, matrix.image.name) }} ${{ format('kestra/kestra:latest-lts{0}', matrix.image.name) }}
end:
runs-on: ubuntu-latest
needs:
@@ -187,14 +200,9 @@ jobs:
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
steps:
# Slack
- name: Slack notification
uses: Gamesight/slack-workflow-status@master
if: ${{ always() && env.SLACK_WEBHOOK_URL != 0 }}
if: ${{ failure() && env.SLACK_WEBHOOK_URL != 0 }}
uses: kestra-io/actions/composite/slack-status@main
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }}
name: GitHub Actions
icon_emoji: ':github-actions:'
channel: 'C02DQ1A7JLR' # _int_git channel
webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}

View File

@@ -29,7 +29,7 @@ jobs:
# Setup build
- name: Setup - Build
uses: kestra-io/actions/.github/actions/setup-build@main
uses: kestra-io/actions/composite/setup-build@main
id: build
with:
java-enabled: true

View File

@@ -7,7 +7,7 @@ on:
jobs:
publish:
name: Pull Request - Delete Docker
if: github.repository == github.event.pull_request.head.repo.full_name # prevent running on forks
if: github.repository == 'kestra-io/kestra' # prevent running on forks
runs-on: ubuntu-latest
steps:
- uses: dataaxiom/ghcr-cleanup-action@v1

View File

@@ -8,12 +8,12 @@ on:
jobs:
build-artifacts:
name: Build Artifacts
if: github.repository == github.event.pull_request.head.repo.full_name # prevent running on forks
if: github.repository == 'kestra-io/kestra' # prevent running on forks
uses: ./.github/workflows/workflow-build-artifacts.yml
publish:
name: Publish Docker
if: github.repository == github.event.pull_request.head.repo.full_name # prevent running on forks
if: github.repository == 'kestra-io/kestra' # prevent running on forks
runs-on: ubuntu-latest
needs: build-artifacts
env:
@@ -62,7 +62,7 @@ jobs:
# Add comment on pull request
- name: Add comment to PR
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |

View File

@@ -84,14 +84,12 @@ jobs:
name: Notify - Slack
runs-on: ubuntu-latest
needs: [ frontend, backend ]
if: github.event_name == 'schedule'
steps:
- name: Notify failed CI
id: send-ci-failed
if: |
always() && (needs.frontend.result != 'success' ||
needs.backend.result != 'success')
uses: kestra-io/actions/.github/actions/send-ci-failed@main
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
always() &&
(needs.frontend.result != 'success' || needs.backend.result != 'success') &&
(github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop')
uses: kestra-io/actions/composite/slack-status@main
with:
webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}

View File

@@ -167,6 +167,9 @@ kestra:
open-urls:
- "/ping"
- "/api/v1/executions/webhook/"
- "/api/v1/main/executions/webhook/"
- "/api/v1/*/executions/webhook/"
- "/api/v1/basicAuthValidationErrors"
preview:
initial-rows: 100

View File

@@ -3,30 +3,88 @@ package io.kestra.core.events;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.context.ServerRequestContext;
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
import java.util.Objects;
@Getter
public class CrudEvent<T> {
T model;
private final T model;
@Nullable
T previousModel;
CrudEventType type;
HttpRequest<?> request;
private final T previousModel;
private final CrudEventType type;
private final HttpRequest<?> request;
/**
* Static helper method for creating a new {@link CrudEventType#UPDATE} CrudEvent.
*
* @param model the new created model.
* @param <T> type of the model.
* @return the new {@link CrudEvent}.
*/
public static <T> CrudEvent<T> create(T model) {
Objects.requireNonNull(model, "Can't create CREATE event with a null model");
return new CrudEvent<>(model, null, CrudEventType.CREATE);
}
/**
* Static helper method for creating a new {@link CrudEventType#DELETE} CrudEvent.
*
* @param model the deleted model.
* @param <T> type of the model.
* @return the new {@link CrudEvent}.
*/
public static <T> CrudEvent<T> delete(T model) {
Objects.requireNonNull(model, "Can't create DELETE event with a null model");
return new CrudEvent<>(null, model, CrudEventType.DELETE);
}
/**
* Static helper method for creating a new CrudEvent.
*
* @param before the model before the update.
* @param after the model after the update.
* @param <T> type of the model.
* @return the new {@link CrudEvent}.
*/
public static <T> CrudEvent<T> of(T before, T after) {
if (before == null && after == null) {
throw new IllegalArgumentException("Both before and after cannot be null");
}
if (before == null) {
return create(after);
}
if (after == null) {
return delete(before);
}
return new CrudEvent<>(after, before, CrudEventType.UPDATE);
}
/**
* @deprecated use the static factory methods.
*/
@Deprecated
public CrudEvent(T model, CrudEventType type) {
this.model = model;
this.type = type;
this.previousModel = null;
this.request = ServerRequestContext.currentRequest().orElse(null);
this(
CrudEventType.DELETE.equals(type) ? null : model,
CrudEventType.DELETE.equals(type) ? model : null,
type,
ServerRequestContext.currentRequest().orElse(null)
);
}
public CrudEvent(T model, T previousModel, CrudEventType type) {
this(model, previousModel, type, ServerRequestContext.currentRequest().orElse(null));
}
public CrudEvent(T model, T previousModel, CrudEventType type, HttpRequest<?> request) {
this.model = model;
this.previousModel = previousModel;
this.type = type;
this.request = ServerRequestContext.currentRequest().orElse(null);
this.request = request;
}
}

View File

@@ -185,34 +185,6 @@ public class Trigger extends TriggerContext implements HasUID {
.build();
}
public static Trigger update(Trigger currentTrigger, Trigger newTrigger, ZonedDateTime nextExecutionDate) throws Exception {
Trigger updated = currentTrigger;
// If a backfill is created, we update the currentTrigger
// and set the nextExecutionDate() as the previous one
if (newTrigger.getBackfill() != null) {
updated = currentTrigger.toBuilder()
.backfill(
newTrigger
.getBackfill()
.toBuilder()
.end(newTrigger.getBackfill().getEnd() != null ? newTrigger.getBackfill().getEnd() : ZonedDateTime.now())
.currentDate(
newTrigger.getBackfill().getStart()
)
.previousNextExecutionDate(
currentTrigger.getNextExecutionDate())
.build())
.build();
}
return updated.toBuilder()
.nextExecutionDate(newTrigger.getDisabled() ?
null : nextExecutionDate)
.disabled(newTrigger.getDisabled())
.build();
}
public Trigger resetExecution(Flow flow, Execution execution, ConditionContext conditionContext) {
boolean disabled = this.getStopAfter() != null ? this.getStopAfter().contains(execution.getState().getCurrent()) : this.getDisabled();
if (!disabled) {
@@ -276,27 +248,22 @@ public class Trigger extends TriggerContext implements HasUID {
.build();
}
public Trigger initBackfill(Trigger newTrigger) {
// If a backfill is created, we update the currentTrigger
public Trigger withBackfill(final Backfill backfill) {
Trigger updated = this;
// If a backfill is created, we update the trigger
// and set the nextExecutionDate() as the previous one
if (newTrigger.getBackfill() != null) {
return this.toBuilder()
if (backfill != null) {
updated = this.toBuilder()
.backfill(
newTrigger
.getBackfill()
backfill
.toBuilder()
.end(newTrigger.getBackfill().getEnd() != null ? newTrigger.getBackfill().getEnd() : ZonedDateTime.now())
.currentDate(
newTrigger.getBackfill().getStart()
)
.previousNextExecutionDate(
this.getNextExecutionDate())
.end(backfill.getEnd() != null ? backfill.getEnd() : ZonedDateTime.now())
.currentDate(backfill.getStart())
.previousNextExecutionDate(this.getNextExecutionDate())
.build())
.build();
}
return this;
return updated;
}
// if the next date is after the backfill end, we remove the backfill

View File

@@ -6,6 +6,12 @@ import lombok.Getter;
import lombok.ToString;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Enumeration;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.zip.CRC32;
@AllArgsConstructor
@Getter
@@ -14,5 +20,59 @@ import java.net.URL;
public class ExternalPlugin {
private final URL location;
private final URL[] resources;
private final long crc32;
private volatile Long crc32; // lazy-val
public ExternalPlugin(URL location, URL[] resources) {
this.location = location;
this.resources = resources;
}
public Long getCrc32() {
if (this.crc32 == null) {
synchronized (this) {
if (this.crc32 == null) {
this.crc32 = computeJarCrc32(location);
}
}
}
return crc32;
}
/**
* Compute a CRC32 of the JAR File without reading the whole file
*
* @param location of the JAR File.
* @return the CRC32 of {@code -1} if the checksum can't be computed.
*/
private static long computeJarCrc32(final URL location) {
CRC32 crc = new CRC32();
try (JarFile jar = new JarFile(location.toURI().getPath(), false)) {
Enumeration<JarEntry> entries = jar.entries();
byte[] buffer = new byte[Long.BYTES]; // reusable buffer to avoid re-allocation
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
crc.update(entry.getName().getBytes(StandardCharsets.UTF_8));
updateCrc32WithLong(crc, buffer, entry.getSize());
updateCrc32WithLong(crc, buffer, entry.getCrc());
}
return crc.getValue();
} catch (Exception e) {
return -1;
}
}
private static void updateCrc32WithLong(CRC32 crc32, byte[] reusable, long val) {
// fast long -> byte conversion
reusable[0] = (byte) (val >>> 56);
reusable[1] = (byte) (val >>> 48);
reusable[2] = (byte) (val >>> 40);
reusable[3] = (byte) (val >>> 32);
reusable[4] = (byte) (val >>> 24);
reusable[5] = (byte) (val >>> 16);
reusable[6] = (byte) (val >>> 8);
reusable[7] = (byte) val;
crc32.update(reusable);;
}
}

View File

@@ -46,6 +46,7 @@ public class PluginClassLoader extends URLClassLoader {
+ "|dev.failsafe"
+ "|reactor"
+ "|io.opentelemetry"
+ "|io.netty"
+ ")\\..*$");
private final ClassLoader parent;

View File

@@ -51,8 +51,7 @@ public class PluginResolver {
final List<URL> resources = resolveUrlsForPluginPath(path);
plugins.add(new ExternalPlugin(
path.toUri().toURL(),
resources.toArray(new URL[0]),
computeJarCrc32(path)
resources.toArray(new URL[0])
));
}
} catch (final InvalidPathException | MalformedURLException e) {
@@ -124,33 +123,5 @@ public class PluginResolver {
return urls;
}
/**
* Compute a CRC32 of the JAR File without reading the whole file
*
* @param location of the JAR File.
* @return the CRC32 of {@code -1} if the checksum can't be computed.
*/
private static long computeJarCrc32(final Path location) {
CRC32 crc = new CRC32();
try (JarFile jar = new JarFile(location.toFile(), false)) {
Enumeration<JarEntry> entries = jar.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
crc.update(entry.getName().getBytes());
crc.update(longToBytes(entry.getSize()));
crc.update(longToBytes(entry.getCrc()));
}
} catch (Exception e) {
return -1;
}
return crc.getValue();
}
private static byte[] longToBytes(long x) {
ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES);
buffer.putLong(x);
return buffer.array();
}
}

View File

@@ -10,6 +10,7 @@ import io.kestra.core.models.flows.FlowInterface;
import io.kestra.core.models.flows.Input;
import io.kestra.core.models.flows.State;
import io.kestra.core.models.flows.input.SecretInput;
import io.kestra.core.models.property.Property;
import io.kestra.core.models.property.PropertyContext;
import io.kestra.core.models.tasks.Task;
import io.kestra.core.models.triggers.AbstractTrigger;
@@ -282,15 +283,15 @@ public final class RunVariables {
if (flow != null && flow.getInputs() != null) {
// we add default inputs value from the flow if not already set, this will be useful for triggers
flow.getInputs().stream()
.filter(input -> input.getDefaults() != null && !inputs.containsKey(input.getId()))
.forEach(input -> {
try {
inputs.put(input.getId(), FlowInputOutput.resolveDefaultValue(input, propertyContext));
} catch (IllegalVariableEvaluationException e) {
throw new RuntimeException("Unable to inject default value for input '" + input.getId() + "'", e);
}
});
flow.getInputs().stream()
.filter(input -> input.getDefaults() != null && !inputs.containsKey(input.getId()))
.forEach(input -> {
try {
inputs.put(input.getId(), FlowInputOutput.resolveDefaultValue(input, propertyContext));
} catch (IllegalVariableEvaluationException e) {
// Silent catch, if an input depends on another input, or a variable that is populated at runtime / input filling time, we can't resolve it here.
}
});
}
if (!inputs.isEmpty()) {

View File

@@ -45,7 +45,7 @@ final class Secret {
for (var entry: data.entrySet()) {
if (entry.getValue() instanceof Map map) {
// if some value are of type EncryptedString we decode them and replace the object
if (EncryptedString.TYPE.equalsIgnoreCase((String)map.get("type"))) {
if (map.get("type") instanceof String typeStr && EncryptedString.TYPE.equalsIgnoreCase(typeStr)) {
try {
String decoded = decrypt((String) map.get("value"));
decryptedMap.put(entry.getKey(), decoded);

View File

@@ -163,31 +163,28 @@ public final class JacksonMapper {
.build();
}
public static Pair<JsonNode, JsonNode> getBiDirectionalDiffs(Object previous, Object current) {
JsonNode previousJson = MAPPER.valueToTree(previous);
JsonNode newJson = MAPPER.valueToTree(current);
public static Pair<JsonNode, JsonNode> getBiDirectionalDiffs(Object before, Object after) {
JsonNode beforeNode = MAPPER.valueToTree(before);
JsonNode afterNode = MAPPER.valueToTree(after);
JsonNode patchPrevToNew = JsonDiff.asJson(previousJson, newJson);
JsonNode patchNewToPrev = JsonDiff.asJson(newJson, previousJson);
JsonNode patch = JsonDiff.asJson(beforeNode, afterNode);
JsonNode revert = JsonDiff.asJson(afterNode, beforeNode);
return Pair.of(patchPrevToNew, patchNewToPrev);
return Pair.of(patch, revert);
}
public static String applyPatches(Object object, List<JsonNode> patches) throws JsonProcessingException {
public static JsonNode applyPatchesOnJsonNode(JsonNode jsonObject, List<JsonNode> patches) {
for (JsonNode patch : patches) {
try {
// Required for ES
if (patch.findValue("value") == null) {
((ObjectNode) patch.get(0)).set("value", (JsonNode) null);
if (patch.findValue("value") == null && !patch.isEmpty()) {
((ObjectNode) patch.get(0)).set("value", null);
}
JsonNode current = MAPPER.valueToTree(object);
object = JsonPatch.fromJson(patch).apply(current);
jsonObject = JsonPatch.fromJson(patch).apply(jsonObject);
} catch (IOException | JsonPatchException e) {
throw new RuntimeException(e);
}
}
return MAPPER.writeValueAsString(object);
return jsonObject;
}
}

View File

@@ -3,12 +3,7 @@ package io.kestra.core.services;
import com.fasterxml.jackson.core.JsonProcessingException;
import io.kestra.core.exceptions.FlowProcessingException;
import io.kestra.core.models.executions.Execution;
import io.kestra.core.models.flows.Flow;
import io.kestra.core.models.flows.FlowId;
import io.kestra.core.models.flows.FlowInterface;
import io.kestra.core.models.flows.FlowWithException;
import io.kestra.core.models.flows.FlowWithSource;
import io.kestra.core.models.flows.GenericFlow;
import io.kestra.core.models.flows.*;
import io.kestra.core.models.tasks.RunnableTask;
import io.kestra.core.models.topologies.FlowTopology;
import io.kestra.core.models.triggers.AbstractTrigger;
@@ -30,16 +25,7 @@ import org.apache.commons.lang3.builder.EqualsBuilder;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@@ -551,23 +537,24 @@ public class FlowService {
return expandAll ? recursiveFlowTopology(new ArrayList<>(), tenant, namespace, id, destinationOnly) : flowTopologyRepository.get().findByFlow(tenant, namespace, id, destinationOnly).stream();
}
private Stream<FlowTopology> recursiveFlowTopology(List<FlowId> flowIds, String tenantId, String namespace, String id, boolean destinationOnly) {
private Stream<FlowTopology> recursiveFlowTopology(List<String> visitedTopologies, String tenantId, String namespace, String id, boolean destinationOnly) {
if (flowTopologyRepository.isEmpty()) {
throw noRepositoryException();
}
List<FlowTopology> flowTopologies = flowTopologyRepository.get().findByFlow(tenantId, namespace, id, destinationOnly);
FlowId flowId = FlowId.of(tenantId, namespace, id, null);
if (flowIds.contains(flowId)) {
return flowTopologies.stream();
}
flowIds.add(flowId);
var flowTopologies = flowTopologyRepository.get().findByFlow(tenantId, namespace, id, destinationOnly);
return flowTopologies.stream()
.flatMap(topology -> Stream.of(topology.getDestination(), topology.getSource()))
// recursively fetch child nodes
.flatMap(node -> recursiveFlowTopology(flowIds, node.getTenantId(), node.getNamespace(), node.getId(), destinationOnly));
// ignore already visited topologies
.filter(x -> !visitedTopologies.contains(x.uid()))
.flatMap(topology -> {
visitedTopologies.add(topology.uid());
Stream<FlowTopology> subTopologies = Stream
.of(topology.getDestination(), topology.getSource())
// recursively visit children and parents nodes
.flatMap(relationNode -> recursiveFlowTopology(visitedTopologies, relationNode.getTenantId(), relationNode.getNamespace(), relationNode.getId(), destinationOnly));
return Stream.concat(Stream.of(topology), subTopologies);
});
}
private IllegalStateException noRepositoryException() {

View File

@@ -1,5 +1,6 @@
package io.kestra.core.storages.kv;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import java.time.Duration;
@@ -9,6 +10,7 @@ import java.util.Map;
import java.util.Optional;
@Getter
@EqualsAndHashCode
public class KVMetadata {
private String description;
private Instant expirationDate;
@@ -17,14 +19,18 @@ public class KVMetadata {
if (ttl != null && ttl.isNegative()) {
throw new IllegalArgumentException("ttl cannot be negative");
}
this.description = description;
if (ttl != null) {
this.expirationDate = Instant.now().plus(ttl);
}
}
public KVMetadata(String description, Instant expirationDate) {
this.description = description;
this.expirationDate = expirationDate;
}
public KVMetadata(Map<String, String> metadata) {
if (metadata == null) {
return;
@@ -46,4 +52,9 @@ public class KVMetadata {
}
return map;
}
@Override
public String toString() {
return "[description=" + description + ", expirationDate=" + expirationDate + "]";
}
}

View File

@@ -4,7 +4,9 @@ import io.kestra.core.exceptions.ResourceExpiredException;
import io.kestra.core.storages.StorageContext;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URI;
import java.time.Duration;
import java.util.List;
import java.util.Optional;
import java.util.regex.Pattern;
@@ -104,8 +106,33 @@ public interface KVStore {
default boolean exists(String key) throws IOException {
return list().stream().anyMatch(kvEntry -> kvEntry.key().equals(key));
}
/**
* Finds a KV entry with associated metadata for a given key.
*
* @param key the KV entry key.
* @return an optional of {@link KVValueAndMetadata}.
*
* @throws UncheckedIOException if an error occurred while executing the operation on the K/V store.
*/
default Optional<KVValueAndMetadata> findMetadataAndValue(final String key) throws UncheckedIOException {
try {
return get(key).flatMap(entry ->
{
try {
return getValue(entry.key()).map(current -> new KVValueAndMetadata(new KVMetadata(entry.description(), entry.expirationDate()), current.value()));
} catch (IOException e) {
throw new UncheckedIOException(e);
} catch (ResourceExpiredException e) {
return Optional.empty();
}
}
);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
Pattern KEY_VALIDATOR_PATTERN = Pattern.compile("[a-zA-Z0-9][a-zA-Z0-9._-]*");
/**

View File

@@ -18,6 +18,7 @@ import io.kestra.core.repositories.FlowRepositoryInterface;
import io.kestra.core.repositories.FlowTopologyRepositoryInterface;
import io.kestra.core.services.ConditionService;
import io.kestra.core.utils.ListUtils;
import io.kestra.core.utils.MapUtils;
import io.kestra.plugin.core.condition.*;
import io.micronaut.core.annotation.Nullable;
import jakarta.inject.Inject;
@@ -175,9 +176,6 @@ public class FlowTopologyService {
protected boolean isTriggerChild(Flow parent, Flow child) {
List<AbstractTrigger> triggers = ListUtils.emptyOnNull(child.getTriggers());
// simulated execution: we add a "simulated" label so conditions can know that the evaluation is for a simulated execution
Execution execution = Execution.newExecution(parent, (f, e) -> null, List.of(SIMULATED_EXECUTION), Optional.empty());
// keep only flow trigger
List<io.kestra.plugin.core.trigger.Flow> flowTriggers = triggers
.stream()
@@ -189,13 +187,16 @@ public class FlowTopologyService {
return false;
}
// simulated execution: we add a "simulated" label so conditions can know that the evaluation is for a simulated execution
Execution execution = Execution.newExecution(parent, (f, e) -> null, List.of(SIMULATED_EXECUTION), Optional.empty());
boolean conditionMatch = flowTriggers
.stream()
.flatMap(flow -> ListUtils.emptyOnNull(flow.getConditions()).stream())
.allMatch(condition -> validateCondition(condition, parent, execution));
boolean preconditionMatch = flowTriggers.stream()
.anyMatch(flow -> flow.getPreconditions() == null || validateMultipleConditions(flow.getPreconditions().getConditions(), parent, execution));
.anyMatch(flow -> flow.getPreconditions() == null || validatePreconditions(flow.getPreconditions(), parent, execution));
return conditionMatch && preconditionMatch;
}
@@ -239,11 +240,24 @@ public class FlowTopologyService {
}
private boolean isMandatoryMultipleCondition(Condition condition) {
return Stream
.of(
Expression.class
)
.anyMatch(aClass -> condition.getClass().isAssignableFrom(aClass));
return condition.getClass().isAssignableFrom(Expression.class);
}
private boolean validatePreconditions(io.kestra.plugin.core.trigger.Flow.Preconditions preconditions, FlowInterface child, Execution execution) {
boolean upstreamFlowMatched = MapUtils.emptyOnNull(preconditions.getUpstreamFlowsConditions())
.values()
.stream()
.filter(c -> !isFilterCondition(c))
.anyMatch(c -> validateCondition(c, child, execution));
boolean whereMatched = MapUtils.emptyOnNull(preconditions.getWhereConditions())
.values()
.stream()
.filter(c -> !isFilterCondition(c))
.allMatch(c -> validateCondition(c, child, execution));
// to be a dependency, if upstream flow is set it must be either inside it so it's a AND between upstream flow and where
return upstreamFlowMatched && whereMatched;
}
private boolean isFilterCondition(Condition condition) {

View File

@@ -206,22 +206,17 @@ public class MapUtils {
/**
* Utility method that flatten a nested map.
* <p>
* NOTE: for simplicity, this method didn't allow to flatten maps with conflicting keys that would end up in different flatten keys,
* this could be related later if needed by flattening {k1: k2: {k3: v1}, k1: {k4: v2}} to {k1.k2.k3: v1, k1.k4: v2} is prohibited for now.
*
* @param nestedMap the nested map.
* @return the flattened map.
*
* @throws IllegalArgumentException if any entry contains a map of more than one element.
*/
public static Map<String, Object> nestedToFlattenMap(@NotNull Map<String, Object> nestedMap) {
Map<String, Object> result = new TreeMap<>();
for (Map.Entry<String, Object> entry : nestedMap.entrySet()) {
if (entry.getValue() instanceof Map<?, ?> map) {
Map.Entry<String, Object> flatten = flattenEntry(entry.getKey(), (Map<String, Object>) map);
result.put(flatten.getKey(), flatten.getValue());
Map<String, Object> flatten = flattenEntry(entry.getKey(), (Map<String, Object>) map);
result.putAll(flatten);
} else {
result.put(entry.getKey(), entry.getValue());
}
@@ -229,18 +224,19 @@ public class MapUtils {
return result;
}
private static Map.Entry<String, Object> flattenEntry(String key, Map<String, Object> value) {
if (value.size() > 1) {
throw new IllegalArgumentException("You cannot flatten a map with an entry that is a map of more than one element, conflicting key: " + key);
private static Map<String, Object> flattenEntry(String key, Map<String, Object> value) {
Map<String, Object> result = new TreeMap<>();
for (Map.Entry<String, Object> entry : value.entrySet()) {
String newKey = key + "." + entry.getKey();
Object newValue = entry.getValue();
if (newValue instanceof Map<?, ?> map) {
result.putAll(flattenEntry(newKey, (Map<String, Object>) map));
} else {
result.put(newKey, newValue);
}
}
Map.Entry<String, Object> entry = value.entrySet().iterator().next();
String newKey = key + "." + entry.getKey();
Object newValue = entry.getValue();
if (newValue instanceof Map<?, ?> map) {
return flattenEntry(newKey, (Map<String, Object>) map);
} else {
return Map.entry(newKey, newValue);
}
return result;
}
}

View File

@@ -21,6 +21,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.*;
import lombok.experimental.SuperBuilder;
import lombok.extern.slf4j.Slf4j;
import java.util.Optional;
@@ -68,6 +69,7 @@ import java.util.Optional;
)
}
)
@Slf4j
public class Exit extends Task implements ExecutionUpdatableTask {
@NotNull
@Schema(
@@ -104,12 +106,13 @@ public class Exit extends Task implements ExecutionUpdatableTask {
// ends all parents
while (newTaskRun.getParentTaskRunId() != null) {
newTaskRun = newExecution.findTaskRunByTaskRunId(newTaskRun.getParentTaskRunId()).withState(exitState);
newExecution = execution.withTaskRun(newTaskRun);
newExecution = newExecution.withTaskRun(newTaskRun);
}
return newExecution;
} catch (InternalException e) {
// in case we cannot update the last not terminated task run, we ignore it
return execution;
log.warn("Unable to update the taskrun state", e);
return execution.withState(exitState);
}
})
.orElse(execution)

View File

@@ -216,49 +216,46 @@ public class Subflow extends Task implements ExecutableTask<Subflow.Output>, Chi
VariablesService variablesService = ((DefaultRunContext) runContext).getApplicationContext().getBean(VariablesService.class);
if (this.wait) { // we only compute outputs if we wait for the subflow
boolean isOutputsAllowed = runContext
.<Boolean>pluginConfiguration(PLUGIN_FLOW_OUTPUTS_ENABLED)
.orElse(true);
List<io.kestra.core.models.flows.Output> subflowOutputs = flow.getOutputs();
// region [deprecated] Subflow outputs feature
if (subflowOutputs == null && isOutputsAllowed && this.getOutputs() != null) {
subflowOutputs = this.getOutputs().entrySet().stream()
.<io.kestra.core.models.flows.Output>map(entry -> io.kestra.core.models.flows.Output
.builder()
.id(entry.getKey())
.value(entry.getValue())
.required(true)
.build()
)
.toList();
if (subflowOutputs == null && this.getOutputs() != null) {
boolean isOutputsAllowed = runContext
.<Boolean>pluginConfiguration(PLUGIN_FLOW_OUTPUTS_ENABLED)
.orElse(true);
if (isOutputsAllowed) {
try {
subflowOutputs = this.getOutputs().entrySet().stream()
.<io.kestra.core.models.flows.Output>map(entry -> io.kestra.core.models.flows.Output
.builder()
.id(entry.getKey())
.value(entry.getValue())
.required(true)
.build()
)
.toList();
} catch (Exception e) {
Variables variables = variablesService.of(StorageContext.forTask(taskRun), builder.build());
return failSubflowDueToOutput(runContext, taskRun, execution, e, variables);
}
} else {
runContext.logger().warn("Defining outputs inside the Subflow task is not allowed.");
}
}
//endregion
if (subflowOutputs != null && !subflowOutputs.isEmpty()) {
try {
Map<String, Object> outputs = FlowInputOutput.renderFlowOutputs(subflowOutputs, runContext);
Map<String, Object> rOutputs = FlowInputOutput.renderFlowOutputs(subflowOutputs, runContext);
FlowInputOutput flowInputOutput = ((DefaultRunContext)runContext).getApplicationContext().getBean(FlowInputOutput.class); // this is hacking
if (flow.getOutputs() != null && flowInputOutput != null) {
outputs = flowInputOutput.typedOutputs(flow, execution, outputs);
rOutputs = flowInputOutput.typedOutputs(flow, execution, rOutputs);
}
builder.outputs(outputs);
builder.outputs(rOutputs);
} catch (Exception e) {
runContext.logger().warn("Failed to extract outputs with the error: '{}'", e.getLocalizedMessage(), e);
var state = State.Type.fail(this);
Variables variables = variablesService.of(StorageContext.forTask(taskRun), builder.build());
taskRun = taskRun
.withState(state)
.withAttempts(Collections.singletonList(TaskRunAttempt.builder().state(new State().withState(state)).build()))
.withOutputs(variables);
return Optional.of(SubflowExecutionResult.builder()
.executionId(execution.getId())
.state(State.Type.FAILED)
.parentTaskRun(taskRun)
.build());
return failSubflowDueToOutput(runContext, taskRun, execution, e, variables);
}
}
}
@@ -282,6 +279,21 @@ public class Subflow extends Task implements ExecutableTask<Subflow.Output>, Chi
return Optional.of(ExecutableUtils.subflowExecutionResult(taskRun, execution));
}
private Optional<SubflowExecutionResult> failSubflowDueToOutput(RunContext runContext, TaskRun taskRun, Execution execution, Exception e, Variables outputs) {
runContext.logger().error("Failed to extract outputs with the error: '{}'", e.getLocalizedMessage(), e);
var state = State.Type.fail(this);
taskRun = taskRun
.withState(state)
.withAttempts(Collections.singletonList(TaskRunAttempt.builder().state(new State().withState(state)).build()))
.withOutputs(outputs);
return Optional.of(SubflowExecutionResult.builder()
.executionId(execution.getId())
.state(State.Type.FAILED)
.parentTaskRun(taskRun)
.build());
}
@Override
public boolean waitForExecution() {
return this.wait;

View File

@@ -202,7 +202,7 @@ import static io.kestra.core.utils.Rethrow.throwPredicate;
code = """
id: sentry_execution_example
namespace: company.team
tasks:
- id: send_alert
type: io.kestra.plugin.notifications.sentry.SentryExecution
@@ -221,7 +221,7 @@ import static io.kestra.core.utils.Rethrow.throwPredicate;
- WARNING
- type: io.kestra.plugin.core.condition.ExecutionNamespace
namespace: company.payroll
prefix: false"""
prefix: false"""
)
},
@@ -405,6 +405,28 @@ public class Flow extends AbstractTrigger implements TriggerOutput<Flow.Output>
return conditions;
}
@JsonIgnore
public Map<String, Condition> getUpstreamFlowsConditions() {
AtomicInteger conditionId = new AtomicInteger();
return ListUtils.emptyOnNull(flows).stream()
.map(upstreamFlow -> Map.entry(
"condition_" + conditionId.incrementAndGet(),
new UpstreamFlowCondition(upstreamFlow)
))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
@JsonIgnore
public Map<String, Condition> getWhereConditions() {
AtomicInteger conditionId = new AtomicInteger();
return ListUtils.emptyOnNull(where).stream()
.map(filter -> Map.entry(
"condition_" + conditionId.incrementAndGet() + "_" + filter.getId(),
new FilterCondition(filter)
))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
@Override
public Logger logger() {
return log;

View File

@@ -0,0 +1,121 @@
package io.kestra.core.events;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
class CrudEventTest {
@Test
void shouldReturnCreateEventWhenModelIsProvided() {
// Given
String model = "testModel";
// When
CrudEvent<String> event = CrudEvent.create(model);
// Then
assertThat(event.getModel()).isEqualTo(model);
assertThat(event.getPreviousModel()).isNull();
assertThat(event.getType()).isEqualTo(CrudEventType.CREATE);
assertThat(event.getRequest()).isNull();
}
@Test
void shouldThrowExceptionWhenCreateEventWithNullModel() {
// Given
String model = null;
// When / Then
assertThatThrownBy(() -> CrudEvent.create(model))
.isInstanceOf(NullPointerException.class)
.hasMessage("Can't create CREATE event with a null model");
}
@Test
void shouldReturnDeleteEventWhenModelIsProvided() {
// Given
String model = "testModel";
// When
CrudEvent<String> event = CrudEvent.delete(model);
// Then
assertThat(event.getModel()).isNull();
assertThat(event.getPreviousModel()).isEqualTo(model);
assertThat(event.getType()).isEqualTo(CrudEventType.DELETE);
assertThat(event.getRequest()).isNull();
}
@Test
void shouldThrowExceptionWhenDeleteEventWithNullModel() {
// Given
String model = null;
// When / Then
assertThatThrownBy(() -> CrudEvent.delete(model))
.isInstanceOf(NullPointerException.class)
.hasMessage("Can't create DELETE event with a null model");
}
@Test
void shouldReturnUpdateEventWhenBeforeAndAfterAreProvided() {
// Given
String before = "oldModel";
String after = "newModel";
// When
CrudEvent<String> event = CrudEvent.of(before, after);
// Then
assertThat(event.getModel()).isEqualTo(after);
assertThat(event.getPreviousModel()).isEqualTo(before);
assertThat(event.getType()).isEqualTo(CrudEventType.UPDATE);
assertThat(event.getRequest()).isNull();
}
@Test
void shouldReturnCreateEventWhenBeforeIsNullAndAfterIsProvided() {
// Given
String before = null;
String after = "newModel";
// When
CrudEvent<String> event = CrudEvent.of(before, after);
// Then
assertThat(event.getModel()).isEqualTo(after);
assertThat(event.getPreviousModel()).isNull();
assertThat(event.getType()).isEqualTo(CrudEventType.CREATE);
assertThat(event.getRequest()).isNull();
}
@Test
void shouldReturnDeleteEventWhenAfterIsNullAndBeforeIsProvided() {
// Given
String before = "oldModel";
String after = null;
// When
CrudEvent<String> event = CrudEvent.of(before, after);
// Then
assertThat(event.getModel()).isNull();
assertThat(event.getPreviousModel()).isEqualTo(before);
assertThat(event.getType()).isEqualTo(CrudEventType.DELETE);
assertThat(event.getRequest()).isNull();
}
@Test
void shouldThrowExceptionWhenBothBeforeAndAfterAreNull() {
// Given
String before = null;
String after = null;
// When / Then
assertThatThrownBy(() -> CrudEvent.of(before, after))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Both before and after cannot be null");
}
}

View File

@@ -10,6 +10,7 @@ import io.kestra.core.server.ServiceType;
import io.kestra.core.utils.IdUtils;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.time.Duration;
@@ -30,6 +31,7 @@ public abstract class AbstractServiceUsageReportTest {
ServiceInstanceRepositoryInterface serviceInstanceRepository;
@Test
@Disabled
public void shouldGetReport() {
// Given
final LocalDate start = LocalDate.now().withDayOfMonth(1);

View File

@@ -453,6 +453,12 @@ public abstract class AbstractRunnerTest {
flowConcurrencyCaseTest.flowConcurrencyQueueAfterExecution();
}
@Test
@LoadFlows({"flows/valids/flow-concurrency-subflow.yml", "flows/valids/flow-concurrency-cancel.yml"})
void flowConcurrencySubflow() throws Exception {
flowConcurrencyCaseTest.flowConcurrencySubflow();
}
@Test
@ExecuteFlow("flows/valids/executable-fail.yml")
void badExecutable(Execution execution) {

View File

@@ -7,6 +7,7 @@ import io.kestra.core.models.flows.State.Type;
import io.kestra.core.queues.QueueException;
import io.kestra.core.queues.QueueFactoryInterface;
import io.kestra.core.queues.QueueInterface;
import io.kestra.core.reporter.model.Count;
import io.kestra.core.repositories.FlowRepositoryInterface;
import io.kestra.core.services.ExecutionService;
import io.kestra.core.storages.StorageInterface;
@@ -391,6 +392,52 @@ public class FlowConcurrencyCaseTest {
assertThat(executionResult2.get().getState().getHistories().get(2).getState()).isEqualTo(State.Type.RUNNING);
}
public void flowConcurrencySubflow() throws TimeoutException, QueueException, InterruptedException {
CountDownLatch successLatch = new CountDownLatch(1);
CountDownLatch canceledLatch = new CountDownLatch(1);
Flux<Execution> receive = TestsUtils.receive(executionQueue, e -> {
if (e.getLeft().getFlowId().equals("flow-concurrency-cancel")) {
if (e.getLeft().getState().getCurrent() == State.Type.SUCCESS) {
successLatch.countDown();
}
if (e.getLeft().getState().getCurrent() == Type.CANCELLED) {
canceledLatch.countDown();
}
}
// FIXME we should fail if we receive the cancel execution again but on Kafka it happens
});
Execution execution1 = runnerUtils.runOneUntilRunning(MAIN_TENANT, "io.kestra.tests", "flow-concurrency-subflow", null, null, Duration.ofSeconds(30));
Execution execution2 = runnerUtils.runOne(MAIN_TENANT, "io.kestra.tests", "flow-concurrency-subflow");
assertThat(execution1.getState().isRunning()).isTrue();
assertThat(execution2.getState().getCurrent()).isEqualTo(Type.SUCCESS);
// assert we have one canceled subflow and one in success
assertTrue(canceledLatch.await(1, TimeUnit.MINUTES));
assertTrue(successLatch.await(1, TimeUnit.MINUTES));
receive.blockLast();
// run another execution to be sure that everything work (purge is correctly done)
CountDownLatch newSuccessLatch = new CountDownLatch(1);
Flux<Execution> secondReceive = TestsUtils.receive(executionQueue, e -> {
if (e.getLeft().getFlowId().equals("flow-concurrency-cancel")) {
if (e.getLeft().getState().getCurrent() == State.Type.SUCCESS) {
newSuccessLatch.countDown();
}
}
// FIXME we should fail if we receive the cancel execution again but on Kafka it happens
});
Execution execution3 = runnerUtils.runOne(MAIN_TENANT, "io.kestra.tests", "flow-concurrency-subflow");
assertThat(execution3.getState().getCurrent()).isEqualTo(Type.SUCCESS);
// assert we have two successful subflow
assertTrue(newSuccessLatch.await(1, TimeUnit.MINUTES));
secondReceive.blockLast();
}
private URI storageUpload() throws URISyntaxException, IOException {
File tempFile = File.createTempFile("file", ".txt");

View File

@@ -1,13 +1,24 @@
package io.kestra.core.runners;
import io.kestra.core.exceptions.IllegalVariableEvaluationException;
import io.kestra.core.models.executions.Execution;
import io.kestra.core.models.flows.DependsOn;
import io.kestra.core.models.flows.Flow;
import io.kestra.core.models.flows.Type;
import io.kestra.core.models.flows.input.BoolInput;
import io.kestra.core.models.property.Property;
import io.kestra.core.models.property.PropertyContext;
import io.kestra.core.models.tasks.Task;
import io.kestra.core.models.triggers.AbstractTrigger;
import io.kestra.core.runners.pebble.functions.SecretFunction;
import io.kestra.core.utils.IdUtils;
import io.micronaut.context.ApplicationContext;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
@@ -112,4 +123,25 @@ class RunVariablesTest {
assertThat(kestra.get("environment")).isEqualTo("test");
assertThat(kestra.get("url")).isEqualTo("http://localhost:8080");
}
}
@Test
void nonResolvableDynamicInputsShouldBeSkipped() throws IllegalVariableEvaluationException {
Map<String, Object> variables = new RunVariables.DefaultBuilder()
.withFlow(Flow
.builder()
.namespace("a.b")
.id("c")
.inputs(List.of(
BoolInput.builder().id("a").type(Type.BOOL).defaults(Property.ofValue(true)).build(),
BoolInput.builder().id("b").type(Type.BOOL).dependsOn(new DependsOn(List.of("a"), null)).defaults(Property.ofExpression("{{inputs.a == true}}")).build()
))
.build()
)
.withExecution(Execution.builder().id(IdUtils.create()).build())
.build(new RunContextLogger(), PropertyContext.create(new VariableRenderer(Mockito.mock(ApplicationContext.class), Mockito.mock(VariableRenderer.VariableConfiguration.class), Collections.emptyList())));
Assertions.assertEquals(Map.of(
"a", true
), variables.get("inputs"));
}
}

View File

@@ -6,6 +6,8 @@ import io.kestra.core.models.executions.Execution;
import io.kestra.core.models.flows.State;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@KestraTest(startRunner = true)
@@ -31,4 +33,15 @@ class TaskWithRunIfTest {
assertThat(execution.findTaskRunsByTaskId("log_test").getFirst().getState().getCurrent()).isEqualTo(State.Type.SKIPPED);
}
@Test
@ExecuteFlow("flows/valids/task-runif-executionupdating.yml")
void executionUpdatingTask(Execution execution) {
assertThat(execution.getState().getCurrent()).isEqualTo(State.Type.SUCCESS);
assertThat(execution.getTaskRunList()).hasSize(5);
assertThat(execution.findTaskRunsByTaskId("skipSetVariables").getFirst().getState().getCurrent()).isEqualTo(State.Type.SKIPPED);
assertThat(execution.findTaskRunsByTaskId("skipUnsetVariables").getFirst().getState().getCurrent()).isEqualTo(State.Type.SKIPPED);
assertThat(execution.findTaskRunsByTaskId("unsetVariables").getFirst().getState().getCurrent()).isEqualTo(State.Type.SUCCESS);
assertThat(execution.findTaskRunsByTaskId("setVariables").getFirst().getState().getCurrent()).isEqualTo(State.Type.SUCCESS);
assertThat(execution.getVariables()).containsEntry("list", List.of(42));
}
}

View File

@@ -1,13 +1,14 @@
package io.kestra.core.serializers;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.tuple.Pair;
import org.junit.jupiter.api.Test;
import org.junitpioneer.jupiter.DefaultTimeZone;
import org.junitpioneer.jupiter.RetryingTest;
import java.io.IOException;
import java.time.Instant;
@@ -86,6 +87,36 @@ class JacksonMapperTest {
assertThat(deserialize.getZonedDateTime().toEpochSecond()).isEqualTo(original.getZonedDateTime().toEpochSecond());
assertThat(deserialize.getZonedDateTime().getOffset()).isEqualTo(original.getZonedDateTime().getOffset());
}
@Test
void shouldComputeDiffGivenCreatedObject() {
Pair<JsonNode, JsonNode> value = JacksonMapper.getBiDirectionalDiffs(null, new DummyObject("value"));
// patch
assertThat(value.getLeft().toString()).isEqualTo("[{\"op\":\"replace\",\"path\":\"\",\"value\":{\"value\":\"value\"}}]");
// Revert
assertThat(value.getRight().toString()).isEqualTo("[{\"op\":\"replace\",\"path\":\"\",\"value\":null}]");
}
@Test
void shouldComputeDiffGivenUpdatedObject() {
Pair<JsonNode, JsonNode> value = JacksonMapper.getBiDirectionalDiffs(new DummyObject("before"), new DummyObject("after"));
// patch
assertThat(value.getLeft().toString()).isEqualTo("[{\"op\":\"replace\",\"path\":\"/value\",\"value\":\"after\"}]");
// Revert
assertThat(value.getRight().toString()).isEqualTo("[{\"op\":\"replace\",\"path\":\"/value\",\"value\":\"before\"}]");
}
@Test
void shouldComputeDiffGivenDeletedObject() {
Pair<JsonNode, JsonNode> value = JacksonMapper.getBiDirectionalDiffs(new DummyObject("value"), null);
// Patch
assertThat(value.getLeft().toString()).isEqualTo("[{\"op\":\"replace\",\"path\":\"\",\"value\":null}]");
// Revert
assertThat(value.getRight().toString()).isEqualTo("[{\"op\":\"replace\",\"path\":\"\",\"value\":{\"value\":\"value\"}}]");
}
private record DummyObject(String value){}
@Getter
@NoArgsConstructor

View File

@@ -21,9 +21,11 @@ import java.nio.file.Path;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalUnit;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -91,7 +93,7 @@ class InternalKVStoreTest {
String description = "myDescription";
kv.put(TEST_KV_KEY, new KVValueAndMetadata(new KVMetadata(description, Duration.ofMinutes(5)), complexValue));
kv.put("key-without-expiration", new KVValueAndMetadata(new KVMetadata(null, null), complexValue));
kv.put("key-without-expiration", new KVValueAndMetadata(new KVMetadata(null, (Duration) null), complexValue));
kv.put("expired-key", new KVValueAndMetadata(new KVMetadata(null, Duration.ofMillis(1)), complexValue));
List<KVEntry> list = kv.listAll();
@@ -213,6 +215,22 @@ class InternalKVStoreTest {
// When
Assertions.assertThrows(ResourceExpiredException.class, () -> kv.getValue(TEST_KV_KEY));
}
@Test
void shouldGetKVValueAndMetadata() throws IOException {
// Given
final InternalKVStore kv = kv();
KVValueAndMetadata val = new KVValueAndMetadata(new KVMetadata(null, Duration.ofMinutes(5)), complexValue);
kv.put(TEST_KV_KEY, val);
// When
Optional<KVValueAndMetadata> result = kv.findMetadataAndValue(TEST_KV_KEY);
// Then
Assertions.assertEquals(val.value(), result.get().value());
Assertions.assertEquals(val.metadata().getDescription(), result.get().metadata().getDescription());
Assertions.assertEquals(val.metadata().getExpirationDate().truncatedTo(ChronoUnit.MILLIS), result.get().metadata().getExpirationDate().truncatedTo(ChronoUnit.MILLIS));
}
@Test
void illegalKey() {

View File

@@ -204,6 +204,10 @@ class FlowTopologyServiceTest {
io.kestra.plugin.core.trigger.Flow.UpstreamFlow.builder().namespace("io.kestra.ee").flowId("parent").build(),
io.kestra.plugin.core.trigger.Flow.UpstreamFlow.builder().namespace("io.kestra.others").flowId("invalid").build()
))
// add an always true condition to validate that it's an AND between 'flows' and 'where'
.where(List.of(io.kestra.plugin.core.trigger.Flow.ExecutionFilter.builder()
.filters(List.of(io.kestra.plugin.core.trigger.Flow.Filter.builder().field(io.kestra.plugin.core.trigger.Flow.Field.EXPRESSION).type(io.kestra.plugin.core.trigger.Flow.Type.IS_NOT_NULL).value("something").build()))
.build()))
.build()
)
.build()

View File

@@ -0,0 +1,304 @@
package io.kestra.core.topologies;
import io.kestra.core.exceptions.FlowProcessingException;
import io.kestra.core.junit.annotations.KestraTest;
import io.kestra.core.models.flows.FlowWithSource;
import io.kestra.core.models.topologies.FlowTopology;
import io.kestra.core.repositories.FlowRepositoryInterface;
import io.kestra.core.repositories.FlowTopologyRepositoryInterface;
import io.kestra.core.services.FlowService;
import io.kestra.core.utils.IdUtils;
import jakarta.inject.Inject;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@KestraTest
public class FlowTopologyTest {
@Inject
private FlowService flowService;
@Inject
private FlowTopologyService flowTopologyService;
@Inject
private FlowTopologyRepositoryInterface flowTopologyRepository;
@Test
void should_findDependencies_simpleCase() throws FlowProcessingException {
// Given
var tenantId = randomTenantId();
var child = flowService.importFlow(tenantId,
"""
id: child
namespace: io.kestra.unittest
tasks:
- id: download
type: io.kestra.plugin.core.http.Download
""");
var parent = flowService.importFlow(tenantId, """
id: parent
namespace: io.kestra.unittest
tasks:
- id: subflow
type: io.kestra.core.tasks.flows.Flow
flowId: child
namespace: io.kestra.unittest
""");
var unrelatedFlow = flowService.importFlow(tenantId, """
id: unrelated_flow
namespace: io.kestra.unittest
tasks:
- id: download
type: io.kestra.plugin.core.http.Download
""");
// When
computeAndSaveTopologies(List.of(child, parent, unrelatedFlow));
System.out.println();
flowTopologyRepository.findAll(tenantId).forEach(topology -> {
System.out.println(FlowTopologyTestData.of(topology));
});
var dependencies = flowService.findDependencies(tenantId, "io.kestra.unittest", parent.getId(), false, true);
flowTopologyRepository.findAll(tenantId).forEach(topology -> {
System.out.println(FlowTopologyTestData.of(topology));
});
// Then
assertThat(dependencies.map(FlowTopologyTestData::of))
.containsExactlyInAnyOrder(
new FlowTopologyTestData(parent, child)
);
}
@Test
void should_findDependencies_subchildAndSuperParent() throws FlowProcessingException {
// Given
var tenantId = randomTenantId();
var subChild = flowService.importFlow(tenantId,
"""
id: sub_child
namespace: io.kestra.unittest
tasks:
- id: download
type: io.kestra.plugin.core.http.Download
""");
var child = flowService.importFlow(tenantId,
"""
id: child
namespace: io.kestra.unittest
tasks:
- id: subflow
type: io.kestra.core.tasks.flows.Flow
flowId: sub_child
namespace: io.kestra.unittest
""");
var superParent = flowService.importFlow(tenantId, """
id: super_parent
namespace: io.kestra.unittest
tasks:
- id: subflow
type: io.kestra.core.tasks.flows.Flow
flowId: parent
namespace: io.kestra.unittest
""");
var parent = flowService.importFlow(tenantId, """
id: parent
namespace: io.kestra.unittest
tasks:
- id: subflow
type: io.kestra.core.tasks.flows.Flow
flowId: child
namespace: io.kestra.unittest
""");
var unrelatedFlow = flowService.importFlow(tenantId, """
id: unrelated_flow
namespace: io.kestra.unittest
tasks:
- id: download
type: io.kestra.plugin.core.http.Download
""");
// When
computeAndSaveTopologies(List.of(subChild, child, superParent, parent, unrelatedFlow));
System.out.println();
flowTopologyRepository.findAll(tenantId).forEach(topology -> {
System.out.println(FlowTopologyTestData.of(topology));
});
System.out.println();
var dependencies = flowService.findDependencies(tenantId, "io.kestra.unittest", parent.getId(), false, true);
flowTopologyRepository.findAll(tenantId).forEach(topology -> {
System.out.println(FlowTopologyTestData.of(topology));
});
// Then
assertThat(dependencies.map(FlowTopologyTestData::of))
.containsExactlyInAnyOrder(
new FlowTopologyTestData(superParent, parent),
new FlowTopologyTestData(parent, child),
new FlowTopologyTestData(child, subChild)
);
}
@Test
void should_findDependencies_cyclicTriggers() throws FlowProcessingException {
// Given
var tenantId = randomTenantId();
var triggeredFlowOne = flowService.importFlow(tenantId,
"""
id: triggered_flow_one
namespace: io.kestra.unittest
tasks:
- id: download
type: io.kestra.plugin.core.http.Download
triggers:
- id: listen
type: io.kestra.plugin.core.trigger.Flow
conditions:
- type: io.kestra.plugin.core.condition.ExecutionStatus
in:
- FAILED
""");
var triggeredFlowTwo = flowService.importFlow(tenantId, """
id: triggered_flow_two
namespace: io.kestra.unittest
tasks:
- id: download
type: io.kestra.plugin.core.http.Download
triggers:
- id: listen
type: io.kestra.plugin.core.trigger.Flow
conditions:
- type: io.kestra.plugin.core.condition.ExecutionStatus
in:
- FAILED
""");
// When
computeAndSaveTopologies(List.of(triggeredFlowOne, triggeredFlowTwo));
flowTopologyRepository.findAll(tenantId).forEach(topology -> {
System.out.println(FlowTopologyTestData.of(topology));
});
var dependencies = flowService.findDependencies(tenantId, "io.kestra.unittest", triggeredFlowTwo.getId(), false, true).toList();
flowTopologyRepository.findAll(tenantId).forEach(topology -> {
System.out.println(FlowTopologyTestData.of(topology));
});
// Then
assertThat(dependencies.stream().map(FlowTopologyTestData::of))
.containsExactlyInAnyOrder(
new FlowTopologyTestData(triggeredFlowTwo, triggeredFlowOne),
new FlowTopologyTestData(triggeredFlowOne, triggeredFlowTwo)
);
}
@Test
void flowTriggerWithTargetFlow() throws FlowProcessingException {
// Given
var tenantId = randomTenantId();
var parent = flowService.importFlow(tenantId,
"""
id: parent
namespace: io.kestra.unittest
inputs:
- id: a
type: BOOL
defaults: true
- id: b
type: BOOL
defaults: "{{ inputs.a == true }}"
dependsOn:
inputs:
- a
tasks:
- id: helloA
type: io.kestra.plugin.core.log.Log
message: Hello A
""");
var child = flowService.importFlow(tenantId, """
id: child
namespace: io.kestra.unittest
tasks:
- id: helloB
type: io.kestra.plugin.core.log.Log
message: Hello B
triggers:
- id: release
type: io.kestra.plugin.core.trigger.Flow
states:
- SUCCESS
preconditions:
id: flows
flows:
- namespace: io.kestra.unittest
flowId: parent
""");
var unrelatedFlow = flowService.importFlow(tenantId, """
id: unrelated_flow
namespace: io.kestra.unittest
tasks:
- id: download
type: io.kestra.plugin.core.http.Download
""");
// When
computeAndSaveTopologies(List.of(child, parent, unrelatedFlow));
System.out.println();
flowTopologyRepository.findAll(tenantId).forEach(topology -> {
System.out.println(FlowTopologyTestData.of(topology));
});
var dependencies = flowService.findDependencies(tenantId, "io.kestra.unittest", parent.getId(), false, true);
flowTopologyRepository.findAll(tenantId).forEach(topology -> {
System.out.println(FlowTopologyTestData.of(topology));
});
// Then
assertThat(dependencies.map(FlowTopologyTestData::of))
.containsExactlyInAnyOrder(
new FlowTopologyTestData(parent, child)
);
}
/**
* this function mimics the production behaviour
*/
private void computeAndSaveTopologies(List<@NotNull FlowWithSource> flows) {
flows.forEach(flow ->
flowTopologyService
.topology(
flow,
flows
).distinct()
.forEach(topology -> flowTopologyRepository.save(topology))
);
}
private static String randomTenantId() {
return FlowTopologyTest.class + IdUtils.create();
}
record FlowTopologyTestData(String sourceUid, String destinationUid) {
public FlowTopologyTestData(FlowWithSource parent, FlowWithSource child) {
this(parent.uidWithoutRevision(), child.uidWithoutRevision());
}
public static FlowTopologyTestData of(FlowTopology flowTopology) {
return new FlowTopologyTestData(flowTopology.getSource().getUid(), flowTopology.getDestination().getUid());
}
@Override
public String toString() {
return sourceUid + " -> " + destinationUid;
}
}
}

View File

@@ -9,7 +9,6 @@ import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
class MapUtilsTest {
@SuppressWarnings("unchecked")
@@ -208,10 +207,13 @@ class MapUtilsTest {
}
@Test
void shouldThrowIfNestedMapContainsMultipleEntries() {
var exception = assertThrows(IllegalArgumentException.class,
() -> MapUtils.nestedToFlattenMap(Map.of("k1", Map.of("k2", Map.of("k3", "v1"), "k4", "v2")))
);
assertThat(exception.getMessage()).isEqualTo("You cannot flatten a map with an entry that is a map of more than one element, conflicting key: k1");
void shouldFlattenANestedMapWithDuplicateKeys() {
Map<String, Object> results = MapUtils.nestedToFlattenMap(Map.of("k1", Map.of("k2", Map.of("k3", "v1"), "k4", "v2")));
assertThat(results).hasSize(2);
assertThat(results).containsAllEntriesOf(Map.of(
"k1.k2.k3", "v1",
"k1.k4", "v2"
));
}
}

View File

@@ -38,7 +38,7 @@ class ExitTest {
@ExecuteFlow("flows/valids/exit.yaml")
void shouldExitTheExecution(Execution execution) {
assertThat(execution.getState().getCurrent()).isEqualTo(State.Type.WARNING);
assertThat(execution.getTaskRunList().size()).isEqualTo(2);
assertThat(execution.getTaskRunList()).hasSize(2);
assertThat(execution.getTaskRunList().getFirst().getState().getCurrent()).isEqualTo(State.Type.WARNING);
}
@@ -68,4 +68,14 @@ class ExitTest {
assertThat(killedExecution.get().getTaskRunList().get(1).getState().getCurrent()).isEqualTo(State.Type.KILLED);
receive.blockLast();
}
@Test
@ExecuteFlow("flows/valids/exit-nested.yaml")
void shouldExitAndFailNestedIf(Execution execution) {
assertThat(execution.getState().getCurrent()).isEqualTo(State.Type.FAILED);
assertThat(execution.getTaskRunList()).hasSize(4);
assertThat(execution.findTaskRunsByTaskId("if_some_bool").getFirst().getState().getCurrent()).isEqualTo(State.Type.FAILED);
assertThat(execution.findTaskRunsByTaskId("nested_bool_check").getFirst().getState().getCurrent()).isEqualTo(State.Type.FAILED);
assertThat(execution.findTaskRunsByTaskId("nested_was_false").getFirst().getState().getCurrent()).isEqualTo(State.Type.FAILED);
}
}

View File

@@ -126,7 +126,7 @@ public class PurgeKVTest {
KVStore kvStore2 = runContext.namespaceKv(namespace2);
kvStore2.put(KEY_EXPIRED, new KVValueAndMetadata(new KVMetadata("unused", Duration.ofMillis(1L)), "unused"));
kvStore2.put(KEY, new KVValueAndMetadata(new KVMetadata("unused", Duration.ofMinutes(1L)), "unused"));
kvStore2.put(KEY2_NEVER_EXPIRING, new KVValueAndMetadata(new KVMetadata("unused", null), "unused"));
kvStore2.put(KEY2_NEVER_EXPIRING, new KVValueAndMetadata(new KVMetadata("unused", (Duration) null), "unused"));
kvStore2.put(KEY3_NEVER_EXPIRING, new KVValueAndMetadata(null, "unused"));
PurgeKV purgeKV = PurgeKV.builder()
@@ -152,7 +152,7 @@ public class PurgeKVTest {
KVStore kvStore1 = runContext.namespaceKv(namespace);
kvStore1.put(KEY_EXPIRED, new KVValueAndMetadata(new KVMetadata("unused", Duration.ofMillis(1L)), "unused"));
kvStore1.put(KEY, new KVValueAndMetadata(new KVMetadata("unused", Duration.ofMinutes(1L)), "unused"));
kvStore1.put(KEY2_NEVER_EXPIRING, new KVValueAndMetadata(new KVMetadata("unused", null), "unused"));
kvStore1.put(KEY2_NEVER_EXPIRING, new KVValueAndMetadata(new KVMetadata("unused",(Duration) null), "unused"));
kvStore1.put(KEY3_NEVER_EXPIRING, new KVValueAndMetadata(null, "unused"));
PurgeKV purgeKV = PurgeKV.builder()

View File

@@ -0,0 +1,36 @@
id: exit-nested
namespace: io.kestra.tests
inputs:
- id: someBool
type: BOOL
defaults: true
- id: secondBool
type: BOOL
defaults: false
tasks:
- id: if_some_bool
type: io.kestra.plugin.core.flow.If
condition: "{{ inputs.someBool }}"
then:
- id: was_true
type: io.kestra.plugin.core.log.Log
message: The value was true
- id: nested_bool_check
type: io.kestra.plugin.core.flow.If
condition: "{{ inputs.secondBool }}"
then:
- id: was_also_true
type: io.kestra.plugin.core.log.Log
message: Was also true
else:
- id: nested_was_false
type: io.kestra.plugin.core.execution.Exit
state: FAILED
else:
- id: was_false
type: io.kestra.plugin.core.execution.Exit
state: FAILED

View File

@@ -0,0 +1,8 @@
id: flow-concurrency-subflow
namespace: io.kestra.tests
tasks:
- id: subflow
type: io.kestra.plugin.core.flow.Subflow
namespace: io.kestra.tests
flowId: flow-concurrency-cancel

View File

@@ -0,0 +1,35 @@
id: task-runif-executionupdating
namespace: io.kestra.tests
variables:
list: []
tasks:
- id: output
type: io.kestra.plugin.core.output.OutputValues
values:
taskrun_data: 1
- id: unsetVariables
type: io.kestra.plugin.core.execution.UnsetVariables
runIf: "true"
variables:
- list
- id: setVariables
type: io.kestra.plugin.core.execution.SetVariables
runIf: "{{ outputs.output['values']['taskrun_data'] == 1 }}"
variables:
list: [42]
- id: skipSetVariables
type: io.kestra.plugin.core.execution.SetVariables
runIf: "false"
variables:
list: [1]
- id: skipUnsetVariables
type: io.kestra.plugin.core.execution.UnsetVariables
runIf: "{{ outputs.output['values']['taskrun_data'] == 2 }}"
variables:
- list

View File

@@ -1072,6 +1072,17 @@ public class ExecutorService {
var executionUpdatingTask = (ExecutionUpdatableTask) workerTask.getTask();
try {
// handle runIf
if (!TruthUtils.isTruthy(workerTask.getRunContext().render(workerTask.getTask().getRunIf()))) {
executor.withExecution(
executor
.getExecution()
.withTaskRun(workerTask.getTaskRun().withState(State.Type.SKIPPED)),
"handleExecutionUpdatingTaskSkipped"
);
return false;
}
executor.withExecution(
executionUpdatingTask.update(executor.getExecution(), workerTask.getRunContext())
.withTaskRun(workerTask.getTaskRun().withState(State.Type.RUNNING)),
@@ -1171,7 +1182,7 @@ public class ExecutorService {
}
}
return taskRuns.size() > execution.getTaskRunList().size() ? execution.withTaskRunList(taskRuns) : null;
return taskRuns.size() > ListUtils.emptyOnNull(execution.getTaskRunList()).size() ? execution.withTaskRunList(taskRuns) : null;
}
public boolean canBePurged(final Executor executor) {

View File

@@ -1,4 +1,4 @@
version=1.0.0-SNAPSHOT
version=1.0.3
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError
org.gradle.parallel=true

View File

@@ -0,0 +1,2 @@
-- We must truncate the table as in 0.24 there was a bug that lead to records not purged in this table
truncate table execution_running;

View File

@@ -0,0 +1,2 @@
-- We must truncate the table as in 0.24 there was a bug that lead to records not purged in this table
truncate table execution_running;

View File

@@ -0,0 +1,2 @@
-- We must truncate the table as in 0.24 there was a bug that lead to records not purged in this table
truncate table execution_running;

View File

@@ -150,13 +150,8 @@ public abstract class AbstractJdbcDashboardRepository extends AbstractJdbcReposi
fields.put(field("source_code"), source);
this.jdbcRepository.persist(dashboard, fields);
if (previousDashboard == null) {
eventPublisher.publishEvent(new CrudEvent<>(dashboard, CrudEventType.CREATE));
} else {
eventPublisher.publishEvent(new CrudEvent<>(dashboard, previousDashboard, CrudEventType.UPDATE));
}
this.eventPublisher.publishEvent(CrudEvent.of(previousDashboard, dashboard));
return dashboard;
}
@@ -174,8 +169,7 @@ public abstract class AbstractJdbcDashboardRepository extends AbstractJdbcReposi
fields.put(field("source_code"), deleted.getSourceCode());
this.jdbcRepository.persist(deleted, fields);
eventPublisher.publishEvent(new CrudEvent<>(dashboard.get(), CrudEventType.DELETE));
this.eventPublisher.publishEvent(CrudEvent.delete(dashboard.get()));
return deleted;
}

View File

@@ -1,7 +1,6 @@
package io.kestra.jdbc.repository;
import io.kestra.core.events.CrudEvent;
import io.kestra.core.events.CrudEventType;
import io.kestra.core.models.Label;
import io.kestra.core.models.QueryFilter;
import io.kestra.core.models.QueryFilter.Resource;
@@ -10,8 +9,10 @@ import io.kestra.core.models.dashboards.DataFilter;
import io.kestra.core.models.dashboards.DataFilterKPI;
import io.kestra.core.models.dashboards.filters.*;
import io.kestra.core.models.executions.Execution;
import io.kestra.core.models.executions.TaskRun;
import io.kestra.core.models.executions.statistics.*;
import io.kestra.core.models.executions.statistics.DailyExecutionStatistics;
import io.kestra.core.models.executions.statistics.ExecutionCount;
import io.kestra.core.models.executions.statistics.ExecutionStatistics;
import io.kestra.core.models.executions.statistics.Flow;
import io.kestra.core.models.flows.FlowScope;
import io.kestra.core.models.flows.State;
import io.kestra.core.queues.QueueFactoryInterface;
@@ -40,6 +41,7 @@ import org.jooq.Record;
import org.jooq.impl.DSL;
import reactor.core.publisher.Flux;
import reactor.core.publisher.FluxSink;
import io.kestra.core.models.executions.TaskRun;
import java.time.Duration;
import java.time.Instant;
@@ -969,14 +971,16 @@ public abstract class AbstractJdbcExecutionRepository extends AbstractJdbcReposi
executionQueue().emit(deleted);
eventPublisher.publishEvent(new CrudEvent<>(deleted, CrudEventType.DELETE));
eventPublisher.publishEvent(CrudEvent.delete(deleted));
return deleted;
}
@Override
public Integer purge(Execution execution) {
return this.jdbcRepository.delete(execution);
int delete = this.jdbcRepository.delete(execution);
eventPublisher.publishEvent(CrudEvent.delete(execution));
return delete;
}
public Executor lock(String executionId, Function<Pair<Execution, ExecutorState>, Pair<Executor, ExecutorState>> function) {
@@ -1045,8 +1049,10 @@ public abstract class AbstractJdbcExecutionRepository extends AbstractJdbcReposi
.filter(entry -> entry.getValue().getField() == null || !dateFields().contains(entry.getValue().getField()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
boolean hasAgg = descriptors.getColumns().entrySet().stream().anyMatch(col -> col.getValue().getAgg() != null);
// Generate custom fields for date as they probably need formatting
List<Field<Date>> dateFields = generateDateFields(descriptors, fieldsMapping, startDate, endDate, dateFields());
// If they don't have aggs, we format datetime to minutes
List<Field<Date>> dateFields = generateDateFields(descriptors, fieldsMapping, startDate, endDate, dateFields(), hasAgg ? null : DateUtils.GroupType.MINUTE);
// Init request
SelectConditionStep<Record> selectConditionStep = select(

View File

@@ -700,12 +700,7 @@ public abstract class AbstractJdbcFlowRepository extends AbstractJdbcRepository
this.jdbcRepository.persist(flow, fields);
flowQueue.emit(flow);
if (nullOrExisting != null) {
eventPublisher.publishEvent(new CrudEvent<>(flow, nullOrExisting, crudEventType));
} else {
eventPublisher.publishEvent(new CrudEvent<>(flow, crudEventType));
}
eventPublisher.publishEvent(new CrudEvent<>(flow, nullOrExisting, crudEventType));
return flowWithSource.toBuilder().revision(revision).build();
}
@@ -735,8 +730,7 @@ public abstract class AbstractJdbcFlowRepository extends AbstractJdbcRepository
this.jdbcRepository.persist(deleted, fields);
flowQueue.emit(deleted);
eventPublisher.publishEvent(new CrudEvent<>(flow, CrudEventType.DELETE));
eventPublisher.publishEvent(CrudEvent.delete(flow));
return deleted;
}

View File

@@ -545,8 +545,10 @@ public abstract class AbstractJdbcLogRepository extends AbstractJdbcRepository i
.filter(entry -> entry.getValue().getField() == null || !dateFields().contains(entry.getValue().getField()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
boolean hasAgg = descriptors.getColumns().entrySet().stream().anyMatch(col -> col.getValue().getAgg() != null);
// Generate custom fields for date as they probably need formatting
List<Field<Date>> dateFields = generateDateFields(descriptors, getFieldsMapping(), startDate, endDate, dateFields());
// If they don't have aggs, we format datetime to minutes
List<Field<Date>> dateFields = generateDateFields(descriptors, getFieldsMapping(), startDate, endDate, dateFields(), hasAgg ? null : DateUtils.GroupType.MINUTE);
// Init request
SelectConditionStep<Record> selectConditionStep = select(

View File

@@ -429,8 +429,10 @@ public abstract class AbstractJdbcMetricRepository extends AbstractJdbcRepositor
.filter(entry -> entry.getValue().getField() == null || !dateFields().contains(entry.getValue().getField()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
boolean hasAgg = descriptors.getColumns().entrySet().stream().anyMatch(col -> col.getValue().getAgg() != null);
// Generate custom fields for date as they probably need formatting
List<Field<Date>> dateFields = generateDateFields(descriptors, fieldsMapping, startDate, endDate, dateFields());
// If they don't have aggs, we format datetime to minutes
List<Field<Date>> dateFields = generateDateFields(descriptors, fieldsMapping, startDate, endDate, dateFields(), hasAgg ? null : DateUtils.GroupType.MINUTE);
// Init request
SelectConditionStep<Record> selectConditionStep = select(

View File

@@ -442,13 +442,15 @@ public abstract class AbstractJdbcRepository {
Map<F, String> fieldsMapping,
ZonedDateTime startDate,
ZonedDateTime endDate,
Set<F> dateFields
Set<F> dateFields,
@Nullable DateUtils.GroupType groupType
) {
return descriptors.getColumns().entrySet().stream()
.filter(entry -> entry.getValue().getAgg() == null && dateFields.contains(entry.getValue().getField()))
.map(entry -> {
Duration duration = Duration.between(startDate, endDate == null ? ZonedDateTime.now() : endDate);
return formatDateField(fieldsMapping.get(entry.getValue().getField()), DateUtils.groupByType(duration)).as(entry.getKey());
DateUtils.GroupType effectiveGroupType = groupType != null ? groupType : DateUtils.groupByType(duration);
return formatDateField(fieldsMapping.get(entry.getValue().getField()), effectiveGroupType).as(entry.getKey());
})
.toList();

View File

@@ -67,8 +67,7 @@ public abstract class AbstractJdbcSettingRepository extends AbstractJdbcReposito
public Setting save(Setting setting) {
Map<Field<Object>, Object> fields = this.jdbcRepository.persistFields(setting);
this.jdbcRepository.persist(setting, fields);
eventPublisher.publishEvent(new CrudEvent<>(setting, CrudEventType.UPDATE));
this.eventPublisher.publishEvent(new CrudEvent<>(setting, CrudEventType.UPDATE));
return setting;
}
@@ -82,8 +81,7 @@ public abstract class AbstractJdbcSettingRepository extends AbstractJdbcReposito
}
this.jdbcRepository.delete(setting);
eventPublisher.publishEvent(new CrudEvent<>(setting, CrudEventType.DELETE));
this.eventPublisher.publishEvent(CrudEvent.delete(setting));
return setting;
}

View File

@@ -174,7 +174,7 @@ public abstract class AbstractJdbcTemplateRepository extends AbstractJdbcReposit
try {
templateQueue.emit(template);
eventPublisher.publishEvent(new CrudEvent<>(template, CrudEventType.CREATE));
eventPublisher.publishEvent(CrudEvent.create(template));
return template;
} catch (QueueException e) {
@@ -217,7 +217,7 @@ public abstract class AbstractJdbcTemplateRepository extends AbstractJdbcReposit
try {
templateQueue.emit(deleted);
eventPublisher.publishEvent(new CrudEvent<>(deleted, CrudEventType.DELETE));
eventPublisher.publishEvent(CrudEvent.delete(deleted));
} catch (QueueException e) {
throw new RuntimeException(e);
}

View File

@@ -405,8 +405,10 @@ public abstract class AbstractJdbcTriggerRepository extends AbstractJdbcReposito
.filter(entry -> entry.getValue().getField() == null || !dateFields().contains(entry.getValue().getField()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
boolean hasAgg = descriptors.getColumns().entrySet().stream().anyMatch(col -> col.getValue().getAgg() != null);
// Generate custom fields for date as they probably need formatting
List<Field<Date>> dateFields = generateDateFields(descriptors, fieldsMapping, startDate, endDate, dateFields());
// If they don't have aggs, we format datetime to minutes
List<Field<Date>> dateFields = generateDateFields(descriptors, fieldsMapping, startDate, endDate, dateFields(), hasAgg ? null : DateUtils.GroupType.MINUTE);
// Init request
SelectConditionStep<Record> selectConditionStep = select(

View File

@@ -991,32 +991,28 @@ public class JdbcExecutor implements ExecutorInterface {
}
ExecutionRunning executionRunning = either.getLeft();
FlowInterface flow = flowMetaStore.findByExecution(executionRunning.getExecution()).orElseThrow();
ExecutionRunning processed = executionRunningStorage.countThenProcess(flow, (dslContext, count) -> {
ExecutionRunning computed = executorService.processExecutionRunning(flow, count, executionRunning);
if (computed.getConcurrencyState() == ExecutionRunning.ConcurrencyState.RUNNING && !computed.getExecution().getState().isTerminated()) {
executionRunningStorage.save(dslContext, computed);
} else if (computed.getConcurrencyState() == ExecutionRunning.ConcurrencyState.QUEUED) {
executionQueuedStorage.save(dslContext, ExecutionQueued.fromExecutionRunning(computed));
}
return computed;
});
// we need to update the execution after applying concurrency limit so we use the lock for that
Executor executor = executionRepository.lock(executionRunning.getExecution().getId(), pair -> {
Execution execution = pair.getLeft();
Executor newExecutor = new Executor(execution, null);
FlowInterface flow = flowMetaStore.findByExecution(execution).orElseThrow();
ExecutionRunning processed = executionRunningStorage.countThenProcess(flow, (dslContext, count) -> {
ExecutionRunning computed = executorService.processExecutionRunning(flow, count, executionRunning.withExecution(execution)); // be sure that the execution running contains the latest value of the execution
if (computed.getConcurrencyState() == ExecutionRunning.ConcurrencyState.RUNNING && !computed.getExecution().getState().isTerminated()) {
executionRunningStorage.save(dslContext, computed);
} else if (computed.getConcurrencyState() == ExecutionRunning.ConcurrencyState.QUEUED) {
executionQueuedStorage.save(dslContext, ExecutionQueued.fromExecutionRunning(computed));
}
return computed;
});
try {
executionQueue.emit(processed.getExecution());
// process flow triggers to allow listening on QUEUED and RUNNING state for concurrency limit
flowTriggerService.computeExecutionsFromFlowTriggers(processed.getExecution(), allFlows, Optional.of(multipleConditionStorage))
.forEach(throwConsumer(executionFromFlowTrigger -> this.executionQueue.emit(executionFromFlowTrigger)));
} catch (QueueException e) {
try {
this.executionQueue.emit(
processed.getExecution().failedExecutionFromExecutor(e).getExecution().withState(State.Type.FAILED)
return Pair.of(
newExecutor.withExecution(processed.getExecution(), "handleExecutionRunning"),
pair.getRight()
);
} catch (QueueException ex) {
log.error("Unable to emit the execution {}", processed.getExecution().getId(), ex);
}
}
});
toExecution(executor);
}
private Executor killingOrAfterKillState(final String executionId, Optional<State.Type> afterKillState) {

View File

@@ -301,6 +301,8 @@ public abstract class AbstractScheduler implements Scheduler {
// Initialized local trigger state,
// and if some flows were created outside the box, for example from the CLI,
// then we may have some triggers that are not created yet.
/* FIXME: There is a race between Kafka stream consumption & initializedTriggers: we can override a trigger update coming from a stream consumption with an old one because stream consumption is not waiting for trigger initialization
* Example: we see a SUCCESS execution so we reset the trigger's executionId but then the initializedTriggers resubmits an old trigger state for some reasons (evaluationDate for eg.) */
private void initializedTriggers(List<FlowWithSource> flows) {
record FlowAndTrigger(FlowWithSource flow, AbstractTrigger trigger) {
@Override
@@ -371,10 +373,13 @@ public abstract class AbstractScheduler implements Scheduler {
this.triggerState.update(lastUpdate);
}
} else if (recoverMissedSchedules == RecoverMissedSchedules.NONE) {
lastUpdate = trigger.get().toBuilder().nextExecutionDate(schedule.nextEvaluationDate()).build();
} else {
ZonedDateTime nextEvaluationDate = schedule.nextEvaluationDate();
if (recoverMissedSchedules == RecoverMissedSchedules.NONE && !Objects.equals(trigger.get().getNextExecutionDate(), nextEvaluationDate)) {
lastUpdate = trigger.get().toBuilder().nextExecutionDate(nextEvaluationDate).build();
this.triggerState.update(lastUpdate);
this.triggerState.update(lastUpdate);
}
}
// Used for schedulableNextDate
FlowWithWorkerTrigger flowWithWorkerTrigger = FlowWithWorkerTrigger.builder()

View File

@@ -30,6 +30,7 @@ import jakarta.inject.Inject;
import jakarta.inject.Named;
import lombok.*;
import lombok.experimental.SuperBuilder;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
@@ -88,6 +89,7 @@ public class SchedulerTriggerChangeTest extends AbstractSchedulerTest {
return FlowWithSource.of(flow, flow.getSource());
}
@Disabled("Way too flaky on the CI")
@Test
void run() throws Exception {
CountDownLatch executionQueueCount = new CountDownLatch(1);

View File

@@ -2,6 +2,7 @@ package io.kestra.core.junit.extensions;
import io.kestra.core.junit.annotations.KestraTest;
import io.kestra.core.runners.TestRunner;
import io.kestra.core.utils.TestsUtils;
import io.micronaut.test.annotation.MicronautTestValue;
import io.micronaut.test.extensions.junit5.MicronautJunit5Extension;
import org.junit.jupiter.api.extension.ExtensionContext;
@@ -55,4 +56,11 @@ public class KestraTestExtension extends MicronautJunit5Extension {
}
}
}
@Override
public void afterTestExecution(ExtensionContext context) throws Exception {
super.afterTestExecution(context);
TestsUtils.queueConsumersCleanup();
}
}

View File

@@ -663,6 +663,14 @@ public abstract class StorageTestSuite {
put(tenantId, prefix);
}
@Test
void put_PathWithTenantStringInIt() throws Exception {
String tenantId = IdUtils.create();
String prefix = tenantId + "/" + IdUtils.create();
put(tenantId, prefix);
}
@Test
void putFromAnotherFile() throws Exception {
String prefix = IdUtils.create();
@@ -982,6 +990,14 @@ public abstract class StorageTestSuite {
deleteByPrefix(prefix, tenantId);
}
@Test
void deleteByPrefix_PathWithTenantStringInIt() throws Exception {
String tenantId = IdUtils.create();
String prefix = tenantId + "/" + IdUtils.create();
deleteByPrefix(prefix, tenantId);
}
@Test
void deleteByPrefixNotFound() throws URISyntaxException, IOException {
String prefix = IdUtils.create();

View File

@@ -41,8 +41,15 @@ import java.util.stream.Collectors;
import static io.kestra.core.tenant.TenantService.MAIN_TENANT;
abstract public class TestsUtils {
private static final ThreadLocal<List<Runnable>> queueConsumersCancellations = ThreadLocal.withInitial(ArrayList::new);
private static final ObjectMapper mapper = JacksonMapper.ofYaml();
public static void queueConsumersCleanup() {
queueConsumersCancellations.get().forEach(Runnable::run);
queueConsumersCancellations.get().clear();
}
public static <T> T map(String path, Class<T> cls) throws IOException {
URL resource = TestsUtils.class.getClassLoader().getResource(path);
assert resource != null;
@@ -214,6 +221,7 @@ abstract public class TestsUtils {
}
};
Runnable receiveCancellation = queueType == null ? queue.receive(consumerGroup, eitherConsumer, false) : queue.receive(consumerGroup, queueType, eitherConsumer, false);
queueConsumersCancellations.get().add(receiveCancellation);
return Flux.<T>create(sink -> {
DeserializationException exception = exceptionRef.get();

View File

@@ -12,6 +12,7 @@
<link rel="stylesheet" href="/loader.css" />
<meta name="html-head" content="replace">
<meta name="robots" content="noindex,nofollow">
<meta name="referrer" content="no-referrer-when-downgrade">
<script>
window.KESTRA_GOOGLE_ANALYTICS = null;
window.KESTRA_UI_PATH = "./";

666
ui/package-lock.json generated
View File

@@ -10,7 +10,7 @@
"hasInstallScript": true,
"dependencies": {
"@js-joda/core": "^5.6.5",
"@kestra-io/ui-libs": "^0.0.245",
"@kestra-io/ui-libs": "^0.0.250",
"@vue-flow/background": "^1.3.2",
"@vue-flow/controls": "^1.1.2",
"@vue-flow/core": "^1.46.2",
@@ -95,9 +95,8 @@
"@vueuse/router": "^13.9.0",
"change-case": "5.4.4",
"cross-env": "^10.0.0",
"decompress": "^4.2.1",
"eslint": "^9.34.0",
"eslint-plugin-storybook": "^9.1.4",
"eslint": "^9.35.0",
"eslint-plugin-storybook": "^9.1.5",
"eslint-plugin-vue": "^9.33.0",
"globals": "^16.3.0",
"husky": "^9.1.7",
@@ -1618,9 +1617,9 @@
}
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
"integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==",
"version": "4.9.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
"integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1793,9 +1792,9 @@
}
},
"node_modules/@eslint/js": {
"version": "9.34.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.34.0.tgz",
"integrity": "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==",
"version": "9.36.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.36.0.tgz",
"integrity": "sha512-uhCbYtYynH30iZErszX78U+nR3pJU3RHGQ57NXy5QupD4SBVwDeU8TNBy+MjMngc1UyIW9noKqsRqfjQTBU2dw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3220,9 +3219,9 @@
"license": "BSD-3-Clause"
},
"node_modules/@kestra-io/ui-libs": {
"version": "0.0.245",
"resolved": "https://registry.npmjs.org/@kestra-io/ui-libs/-/ui-libs-0.0.245.tgz",
"integrity": "sha512-nJOq5gG5SxbsGtX7LuWR32YNq5eiCaObhPNW5rQ7071dJAc11aB7qAdycOPlG1uVMj9AUhlsMaBtjdLsKb4IMw==",
"version": "0.0.250",
"resolved": "https://registry.npmjs.org/@kestra-io/ui-libs/-/ui-libs-0.0.250.tgz",
"integrity": "sha512-Y0ANjGn91f+3G6ZeH0niorf0ZCNe/BPWfur+yHni4AKHbyNUZjrE8UN9ETvOlYe5c2qQSyQdM9yK/LdG1Thtzw==",
"dependencies": {
"@nuxtjs/mdc": "^0.16.1",
"@popperjs/core": "^2.11.8",
@@ -7748,26 +7747,10 @@
"node": ">= 4.0.0"
}
},
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
"integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"possible-typed-array-names": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/axios": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
"integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
@@ -8045,57 +8028,6 @@
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/bl": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz",
"integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==",
"dev": true,
"license": "MIT",
"dependencies": {
"readable-stream": "^2.3.5",
"safe-buffer": "^5.1.1"
}
},
"node_modules/bl/node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"dev": true,
"license": "MIT"
},
"node_modules/bl/node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"dev": true,
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/bl/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true,
"license": "MIT"
},
"node_modules/bl/node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
@@ -8211,41 +8143,6 @@
"ieee754": "^1.2.1"
}
},
"node_modules/buffer-alloc": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz",
"integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==",
"dev": true,
"license": "MIT",
"dependencies": {
"buffer-alloc-unsafe": "^1.1.0",
"buffer-fill": "^1.0.0"
}
},
"node_modules/buffer-alloc-unsafe": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz",
"integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==",
"dev": true,
"license": "MIT"
},
"node_modules/buffer-crc32": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/buffer-fill": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz",
"integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==",
"dev": true,
"license": "MIT"
},
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@@ -8930,13 +8827,6 @@
"url": "https://opencollective.com/core-js"
}
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"dev": true,
"license": "MIT"
},
"node_modules/cose-base": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz",
@@ -9715,109 +9605,6 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/decompress": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.1.tgz",
"integrity": "sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"decompress-tar": "^4.0.0",
"decompress-tarbz2": "^4.0.0",
"decompress-targz": "^4.0.0",
"decompress-unzip": "^4.0.1",
"graceful-fs": "^4.1.10",
"make-dir": "^1.0.0",
"pify": "^2.3.0",
"strip-dirs": "^2.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/decompress-tar": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz",
"integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"file-type": "^5.2.0",
"is-stream": "^1.1.0",
"tar-stream": "^1.5.2"
},
"engines": {
"node": ">=4"
}
},
"node_modules/decompress-tarbz2": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz",
"integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==",
"dev": true,
"license": "MIT",
"dependencies": {
"decompress-tar": "^4.1.0",
"file-type": "^6.1.0",
"is-stream": "^1.1.0",
"seek-bzip": "^1.0.5",
"unbzip2-stream": "^1.0.9"
},
"engines": {
"node": ">=4"
}
},
"node_modules/decompress-tarbz2/node_modules/file-type": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz",
"integrity": "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/decompress-targz": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz",
"integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==",
"dev": true,
"license": "MIT",
"dependencies": {
"decompress-tar": "^4.1.1",
"file-type": "^5.2.0",
"is-stream": "^1.1.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/decompress-unzip": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz",
"integrity": "sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw==",
"dev": true,
"license": "MIT",
"dependencies": {
"file-type": "^3.8.0",
"get-stream": "^2.2.0",
"pify": "^2.3.0",
"yauzl": "^2.4.2"
},
"engines": {
"node": ">=4"
}
},
"node_modules/decompress-unzip/node_modules/file-type": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz",
"integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/dedent": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz",
@@ -10333,16 +10120,6 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/end-of-stream": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
"dev": true,
"license": "MIT",
"dependencies": {
"once": "^1.4.0"
}
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
@@ -10568,19 +10345,19 @@
}
},
"node_modules/eslint": {
"version": "9.34.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.34.0.tgz",
"integrity": "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==",
"version": "9.36.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.36.0.tgz",
"integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
"@eslint/config-array": "^0.21.0",
"@eslint/config-helpers": "^0.3.1",
"@eslint/core": "^0.15.2",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "9.34.0",
"@eslint/js": "9.36.0",
"@eslint/plugin-kit": "^0.3.5",
"@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1",
@@ -10676,9 +10453,9 @@
}
},
"node_modules/eslint-plugin-storybook": {
"version": "9.1.4",
"resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-9.1.4.tgz",
"integrity": "sha512-IiIqGFo524PDELajyDLMtceikHpDUKBF6QlH5oJECy+xV3e0DHJkcuyokwxWveb1yg7tHfTLimCKNix2ftRETg==",
"version": "9.1.7",
"resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-9.1.7.tgz",
"integrity": "sha512-Bq9VNutFGX7T0jw7luWt5eEyRFInIsE0+FSaXdayqBNW6NPaGuE+hoBhhTowvohNqEqn5DXwIkPHiI1GhONE9g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -10689,7 +10466,7 @@
},
"peerDependencies": {
"eslint": ">=8",
"storybook": "^9.1.4"
"storybook": "^9.1.7"
}
},
"node_modules/eslint-plugin-vue": {
@@ -11145,16 +10922,6 @@
"bser": "2.1.1"
}
},
"node_modules/fd-slicer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
"integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
"dev": true,
"license": "MIT",
"dependencies": {
"pend": "~1.2.0"
}
},
"node_modules/fflate": {
"version": "0.4.8",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz",
@@ -11174,16 +10941,6 @@
"node": ">=16.0.0"
}
},
"node_modules/file-type": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz",
"integrity": "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -11370,22 +11127,6 @@
}
}
},
"node_modules/for-each": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
"integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-callable": "^1.2.7"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/foreground-child": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz",
@@ -11437,13 +11178,6 @@
],
"license": "MIT"
},
"node_modules/fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
"dev": true,
"license": "MIT"
},
"node_modules/fs-exists-sync": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz",
@@ -11581,20 +11315,6 @@
"node": ">= 0.4"
}
},
"node_modules/get-stream": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz",
"integrity": "sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA==",
"dev": true,
"license": "MIT",
"dependencies": {
"object-assign": "^4.0.1",
"pinkie-promise": "^2.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/giget": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz",
@@ -12572,19 +12292,6 @@
"integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
"license": "MIT"
},
"node_modules/is-callable": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
"integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-core-module": {
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
@@ -12707,13 +12414,6 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/is-natural-number": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz",
"integrity": "sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ==",
"dev": true,
"license": "MIT"
},
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@@ -12779,32 +12479,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-stream": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
"integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-typed-array": {
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
"integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"which-typed-array": "^1.1.16"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-typedarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
@@ -15442,29 +15116,6 @@
"node": ">=0.10"
}
},
"node_modules/make-dir": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz",
"integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"pify": "^3.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/make-dir/node_modules/pify": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
"integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/make-error": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
@@ -17812,13 +17463,6 @@
"@napi-rs/canvas": "^0.1.77"
}
},
"node_modules/pend": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
"dev": true,
"license": "MIT"
},
"node_modules/perfect-debounce": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
@@ -17857,16 +17501,6 @@
"node": ">=0.10"
}
},
"node_modules/pify": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/pinia": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.3.tgz",
@@ -17888,29 +17522,6 @@
}
}
},
"node_modules/pinkie": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz",
"integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/pinkie-promise": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz",
"integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==",
"dev": true,
"license": "MIT",
"dependencies": {
"pinkie": "^2.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/pirates": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
@@ -18064,16 +17675,6 @@
"points-on-curve": "0.2.0"
}
},
"node_modules/possible-typed-array-names": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
"integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
@@ -18256,13 +17857,6 @@
"node": ">= 0.6.0"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"dev": true,
"license": "MIT"
},
"node_modules/process-on-spawn": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.1.0.tgz",
@@ -19489,27 +19083,6 @@
"integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==",
"license": "MIT"
},
"node_modules/seek-bzip": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz",
"integrity": "sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"commander": "^2.8.1"
},
"bin": {
"seek-bunzip": "bin/seek-bunzip",
"seek-table": "bin/seek-bzip-table"
}
},
"node_modules/seek-bzip/node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true,
"license": "MIT"
},
"node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
@@ -20105,16 +19678,6 @@
"node": ">=8"
}
},
"node_modules/strip-dirs": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz",
"integrity": "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-natural-number": "^4.0.1"
}
},
"node_modules/strip-final-newline": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
@@ -20262,65 +19825,6 @@
"url": "https://opencollective.com/synckit"
}
},
"node_modules/tar-stream": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz",
"integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==",
"dev": true,
"license": "MIT",
"dependencies": {
"bl": "^1.0.0",
"buffer-alloc": "^1.2.0",
"end-of-stream": "^1.0.0",
"fs-constants": "^1.0.0",
"readable-stream": "^2.3.0",
"to-buffer": "^1.1.1",
"xtend": "^4.0.0"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/tar-stream/node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"dev": true,
"license": "MIT"
},
"node_modules/tar-stream/node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"dev": true,
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/tar-stream/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true,
"license": "MIT"
},
"node_modules/tar-stream/node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/test-exclude": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz",
@@ -20396,13 +19900,6 @@
"node": ">=12.22"
}
},
"node_modules/through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
"integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
"dev": true,
"license": "MIT"
},
"node_modules/tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
@@ -20538,21 +20035,6 @@
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/to-buffer": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.1.tgz",
"integrity": "sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"isarray": "^2.0.5",
"safe-buffer": "^5.2.1",
"typed-array-buffer": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -20845,21 +20327,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/typed-array-buffer": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
"integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.3",
"es-errors": "^1.3.0",
"is-typed-array": "^1.1.14"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/typedarray-to-buffer": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
@@ -20929,42 +20396,6 @@
"integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==",
"license": "MIT"
},
"node_modules/unbzip2-stream": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz",
"integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==",
"dev": true,
"license": "MIT",
"dependencies": {
"buffer": "^5.2.1",
"through": "^2.3.8"
}
},
"node_modules/unbzip2-stream/node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"node_modules/unctx": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/unctx/-/unctx-2.4.1.tgz",
@@ -21459,9 +20890,9 @@
}
},
"node_modules/vite": {
"version": "6.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
"version": "6.3.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz",
"integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -22415,28 +21846,6 @@
"dev": true,
"license": "ISC"
},
"node_modules/which-typed-array": {
"version": "1.1.19",
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz",
"integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==",
"dev": true,
"license": "MIT",
"dependencies": {
"available-typed-arrays": "^1.0.7",
"call-bind": "^1.0.8",
"call-bound": "^1.0.4",
"for-each": "^0.3.5",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-tostringtag": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/why-is-node-running": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
@@ -22689,16 +22098,6 @@
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"license": "MIT"
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.4"
}
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
@@ -22789,17 +22188,6 @@
"node": ">=8"
}
},
"node_modules/yauzl": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
"integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
"dev": true,
"license": "MIT",
"dependencies": {
"buffer-crc32": "~0.2.3",
"fd-slicer": "~1.1.0"
}
},
"node_modules/yn": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",

View File

@@ -24,7 +24,7 @@
},
"dependencies": {
"@js-joda/core": "^5.6.5",
"@kestra-io/ui-libs": "^0.0.245",
"@kestra-io/ui-libs": "^0.0.250",
"@vue-flow/background": "^1.3.2",
"@vue-flow/controls": "^1.1.2",
"@vue-flow/core": "^1.46.2",
@@ -109,9 +109,8 @@
"@vueuse/router": "^13.9.0",
"change-case": "5.4.4",
"cross-env": "^10.0.0",
"decompress": "^4.2.1",
"eslint": "^9.34.0",
"eslint-plugin-storybook": "^9.1.4",
"eslint": "^9.35.0",
"eslint-plugin-storybook": "^9.1.5",
"eslint-plugin-vue": "^9.33.0",
"globals": "^16.3.0",
"husky": "^9.1.7",

View File

@@ -203,7 +203,7 @@
}
if (error?.response?.status === 401) {
await loadAuthConfigErrors()
await loadAuthConfigErrors(true)
} else if (error?.response?.status === 404) {
router.push({name: "setup"})
} else {

View File

@@ -45,6 +45,10 @@
function removeItem(yaml: string, index: number){
flowStore.flowYaml = yaml;
if(items.value.length <= 1 && index === 0){
emits("update:modelValue", undefined);
return;
}
let localItems = [...items.value]
localItems.splice(index, 1)

View File

@@ -74,7 +74,7 @@
const dashboardComponent = useTemplateRef("dashboardComponent");
const refreshCharts = () => {
dashboardComponent.value!.refreshCharts();
dashboardComponent.value?.refreshCharts?.();
};
const load = async (id = "default", defaultYAML = YAML_MAIN) => {

View File

@@ -154,9 +154,9 @@ charts:
flow:
field: FLOW_ID
displayName: Flow
duration:
field: DURATION
displayName: Duration
start_date:
field: START_DATE
displayName: Start Date
state:
field: STATE
displayName: State

View File

@@ -144,9 +144,9 @@ charts:
flow:
field: FLOW_ID
displayName: Flow
duration:
field: DURATION
displayName: Duration
start_date:
field: START_DATE
displayName: Start Date
state:
field: STATE
displayName: State

View File

@@ -142,9 +142,9 @@ charts:
flow:
field: FLOW_ID
displayName: Flow
duration:
field: DURATION
displayName: Duration
start_date:
field: START_DATE
displayName: Start Date
state:
field: STATE
displayName: State

View File

@@ -75,9 +75,8 @@
import type {Dashboard, Chart} from "../composables/useDashboards";
import {TYPES, isKPIChart, isTableChart, getChartTitle} from "../composables/useDashboards";
import {useRoute, useRouter} from "vue-router";
import {useRoute} from "vue-router";
const route = useRoute();
const router = useRouter();
import {useDashboardStore} from "../../../stores/dashboard";
const dashboardStore = useDashboardStore();
@@ -116,13 +115,6 @@
const filters = ref<{ field: string; operation: string; value: string | string[] }[]>([]);
onMounted(() => {
const dateTimeKeys = ["startDate", "endDate", "timeRange"];
// Default to the last 7 days if no time range is set
if (route.name !== "flows/list" && !Object.keys(route.query).some((key) => dateTimeKeys.some((dateTimeKey) => key.includes(dateTimeKey)))) {
router.push({query: {...route.query, "filters[timeRange][EQUALS]": "PT168H"}});
}
if (route.name === "flows/update") {
filters.value.push({field: "namespace", operation: "EQUALS", value: route.params.namespace});
filters.value.push({field: "flowId", operation: "EQUALS", value: route.params.id});

View File

@@ -2,29 +2,27 @@ import type cytoscape from "cytoscape";
import {cssVariable} from "@kestra-io/ui-libs";
const VARIABLES = {
import {States} from "./types";
const VARIABLES: {node: { background: States; border: States }; edge: States;} = {
node: {
default: {
background: "--ks-dependencies-node-background-default",
border: "--ks-dependencies-node-border-default",
background: {
default: "--ks-dependencies-node-background-default",
faded: "--ks-dependencies-node-background-faded",
selected: "--ks-dependencies-node-background-selected",
hovered: "--ks-dependencies-node-background-hovered",
},
faded: {
background: "--ks-dependencies-node-background-faded",
border: "--ks-dependencies-node-border-faded",
},
selected: {
background: "--ks-dependencies-node-background-selected",
border: "--ks-dependencies-node-border-selected",
},
hovered: {
background: "--ks-dependencies-node-background-hovered",
border: "--ks-dependencies-node-border-hovered",
border: {
default: "--ks-dependencies-node-border-default",
faded: "--ks-dependencies-node-border-faded",
selected: "--ks-dependencies-node-border-selected",
hovered: "--ks-dependencies-node-border-hovered",
},
},
edge: {
default: "--ks-dependencies-edge-default",
faded: "--ks-dependencies-edge-faded",
selected: "--ks-dependencies-node-background-selected",
selected: "--ks-dependencies-edge-selected",
hovered: "--ks-dependencies-edge-hovered",
},
};
@@ -51,14 +49,14 @@ const edgeAnimated: cytoscape.Css.Edge = {
"line-dash-pattern": [3, 5],
};
function nodeColors(type: keyof typeof VARIABLES.node = "default"): Partial<cytoscape.Css.Node> {
function nodeColors(type: keyof States = "default"): Partial<cytoscape.Css.Node> {
return {
"background-color": cssVariable(VARIABLES.node[type].background)!,
"border-color": cssVariable(VARIABLES.node[type].border)!,
"background-color": cssVariable(VARIABLES.node.background[type])!,
"border-color": cssVariable(VARIABLES.node.border[type])!,
};
}
export function edgeColors(type: keyof typeof VARIABLES.edge = "default"): Partial<cytoscape.Css.Edge> {
export function edgeColors(type: keyof States = "default"): Partial<cytoscape.Css.Edge> {
return {
"line-color": cssVariable(VARIABLES.edge[type])!,
"target-arrow-color": cssVariable(VARIABLES.edge[type])!,

View File

@@ -35,3 +35,10 @@ export type Edge = {
};
export type Element = { data: Node } | { data: Edge };
export type States = {
default: string;
faded: string;
selected: string;
hovered: string;
};

View File

@@ -482,6 +482,8 @@
import {useAuthStore} from "override/stores/auth.ts";
import {useFlowStore} from "../../stores/flow.ts";
import {defaultNamespace} from "../../composables/useNamespaces";
export default {
mixins: [RouteContext, RestoreUrl, DataTableActions, SelectTableActions],
components: {
@@ -705,15 +707,12 @@
}
},
beforeRouteEnter(to, _, next) {
const defaultNamespace = localStorage.getItem(
storageKeys.DEFAULT_NAMESPACE,
);
const query = {...to.query};
let queryHasChanged = false;
const queryKeys = Object.keys(query);
if (this?.namespace === undefined && defaultNamespace && !queryKeys.some(key => key.startsWith("filters[namespace]"))) {
query["filters[namespace][PREFIX]"] = defaultNamespace;
if (this?.namespace === undefined && defaultNamespace() && !queryKeys.some(key => key.startsWith("filters[namespace]"))) {
query["filters[namespace][PREFIX]"] = defaultNamespace();
queryHasChanged = true;
}

View File

@@ -605,7 +605,7 @@
.toggle-icon {
position: absolute;
color: var(--ks-content-alert);
right: 1rem;
right: 1.5rem;
width: 1rem;
height: 1rem;
font-size: 1.75rem;

View File

@@ -19,6 +19,7 @@
<el-button-group v-else-if="isURI(value)">
<el-button
type="primary"
tag="a"
size="small"
:href="value"
target="_blank"

View File

@@ -436,6 +436,13 @@
const monacoEditor = ref<typeof MonacoEditor>();
let initialRouteName = ref();
watch(() => route.name, (newVal) => {
if (initialRouteName.value === undefined) {
initialRouteName.value = newVal;
}
}, {immediate: true})
const updateQuery = () => {
const newQuery = {
...Object.fromEntries(queryParamsToKeep.value.map(key => {
@@ -451,9 +458,11 @@
return; // Skip if the query hasn't changed
}
skipRouteWatcherOnce.value = true;
router.push({
query: newQuery
});
if (route.name === initialRouteName.value) {
router.push({
query: newQuery
});
}
};
const editorDidMount = (mountedEditor: monaco.editor.IStandaloneCodeEditor) => {

View File

@@ -11,7 +11,6 @@
import RouteContext from "../../mixins/routeContext";
import TopNavBar from "../../components/layout/TopNavBar.vue";
import MultiPanelFlowEditorView from "./MultiPanelFlowEditorView.vue";
import {storageKeys} from "../../utils/constants";
import {useBlueprintsStore} from "../../stores/blueprints";
import {useCoreStore} from "../../stores/core";
import {editorViewTypes} from "../../utils/constants";
@@ -19,6 +18,7 @@
import {getRandomID} from "../../../scripts/id";
import {useEditorStore} from "../../stores/editor";
import {useFlowStore} from "../../stores/flow";
import {defaultNamespace} from "../../composables/useNamespaces";
export default {
mixins: [RouteContext],
@@ -50,8 +50,7 @@
} else if (blueprintId && blueprintSource) {
flowYaml = await this.blueprintsStore.getBlueprintSource({type: blueprintSource, kind: "flow", id: blueprintId});
} else {
const defaultNamespace = localStorage.getItem(storageKeys.DEFAULT_NAMESPACE);
const selectedNamespace = this.$route.query.namespace || defaultNamespace || "company.team";
const selectedNamespace = this.$route.query.namespace || defaultNamespace() || "company.team";
flowYaml = `id: ${getRandomID()}
namespace: ${selectedNamespace}

View File

@@ -75,7 +75,7 @@
setTimeout(() => {
this.flowStore
.loadDependencies({namespace: flow.namespace, id: flow.id}, true)
.then(({count}) => this.dependenciesCount = count);
.then(({count}) => this.dependenciesCount = count > 0 ? (count - 1) : 0);
}, 1000);
}
},

View File

@@ -295,7 +295,6 @@
</script>
<script>
import {mapState} from "vuex";
import {mapStores} from "pinia";
import {useExecutionsStore} from "../../stores/executions";
import _merge from "lodash/merge";
@@ -314,7 +313,7 @@
import MarkdownTooltip from "../layout/MarkdownTooltip.vue";
import Kicon from "../Kicon.vue";
import Labels from "../layout/Labels.vue";
import {storageKeys} from "../../utils/constants";
import {defaultNamespace} from "../../composables/useNamespaces";
import * as YAML_UTILS from "@kestra-io/ui-libs/flow-yaml-utils";
import YAML_CHART from "../dashboard/assets/executions_timeseries_chart.yaml?raw";
import {useAuthStore} from "override/stores/auth.ts";
@@ -431,7 +430,6 @@
};
},
computed: {
...mapState("auth", ["user"]),
...mapStores(useExecutionsStore, useFlowStore, useAuthStore),
user() {
return this.authStore.user;
@@ -486,14 +484,11 @@
}
},
beforeRouteEnter(to, _, next) {
const defaultNamespace = localStorage.getItem(
storageKeys.DEFAULT_NAMESPACE,
);
const query = {...to.query};
let queryHasChanged = false;
const queryKeys = Object.keys(query);
if (defaultNamespace && !queryKeys.some(key => key.startsWith("filters[namespace]"))) {
query["filters[namespace][PREFIX]"] = defaultNamespace;
if (defaultNamespace() && !queryKeys.some(key => key.startsWith("filters[namespace]"))) {
query["filters[namespace][PREFIX]"] = defaultNamespace();
queryHasChanged = true;
}

View File

@@ -164,12 +164,14 @@
const noCodeHandlers = useNoCodeHandlers(openTabs, focusTab, tempActions)
const TABS = isTourRunning.value ? DEFAULT_TOUR_TABS.flatMap(t => t.tabs) : DEFAULT_ACTIVE_TABS;
const panels = useStorage<Panel[]>(
`el-fl-${flowStore.flow?.namespace}-${flowStore.flow?.id}`,
DEFAULT_ACTIVE_TABS
TABS
.map((t) => ({
...staticGetPanelFromValue(t).panel,
size: 100 / DEFAULT_ACTIVE_TABS.length
size: 100 / TABS.length
})),
undefined,
{

View File

@@ -35,7 +35,7 @@
import {useI18n} from "vue-i18n";
import CopyToClipboard from "../layout/CopyToClipboard.vue";
import Editor from "../inputs/Editor.vue";
import {baseUrl, basePathWithoutTenant, apiUrlWithoutTenants} from "../../override/utils/route";
import {baseUrl, basePath, apiUrl} from "../../override/utils/route";
import {useFlowStore} from "../../stores/flow";
interface Flow {
@@ -73,7 +73,7 @@
});
const generateWebhookUrl = (trigger: Trigger): string => {
const origin = baseUrl ? apiUrlWithoutTenants() : `${location.origin}${basePathWithoutTenant()}`;
const origin = baseUrl ? apiUrl() : `${location.origin}${basePath()}`;
return `${origin}/executions/webhook/${props.flow.namespace}/${props.flow.id}/${trigger.key}`;
};

View File

@@ -1,6 +1,6 @@
<template>
<div class="ks-editor edit-flow-editor">
<nav v-if="original === undefined && navbar" class="top-nav">
<nav v-if="!isDiff && navbar" class="top-nav">
<slot name="nav">
<div class="text-nowrap">
<el-button-group>
@@ -43,11 +43,12 @@
<div ref="editorContainer" class="editor-wrapper position-relative">
<MonacoEditor
ref="monacoEditor"
:key="isDiff.toString()"
:path="path"
:theme="themeComputed"
:value="modelValue"
:options="options"
:diff-editor="original !== undefined"
:diff-editor="isDiff"
:original="original"
:language="lang"
:extension="extension"
@@ -230,8 +231,8 @@
} else {
options.scrollbar = {
vertical: props.original !== undefined ? "hidden" : "auto",
verticalScrollbarSize: props.original !== undefined ? 0 : 10,
vertical: isDiff.value ? "hidden" : "auto",
verticalScrollbarSize: isDiff.value ? 0 : 10,
alwaysConsumeMouseWheel: false,
};
options.renderSideBySide = props.diffSideBySide;
@@ -282,6 +283,7 @@
let lastTimeout: number | undefined = undefined
let decorations: monaco.editor.IEditorDecorationsCollection | undefined = undefined
const isDiff = computed(() => props.original !== undefined);
function isCodeEditor(editor?: monaco.editor.IStandaloneCodeEditor | monaco.editor.IStandaloneDiffEditor): editor is monaco.editor.IStandaloneCodeEditor{
return editor?.getEditorType() === monacoEditor.value?.monaco.editor.EditorType.ICodeEditor
@@ -310,7 +312,7 @@
return
}
if (!props.original) {
if (!isDiff.value) {
editor.onDidBlurEditorWidget?.(() => {
emit("focusout", isCodeEditor(editor)
? editor.getValue()
@@ -396,7 +398,7 @@
}
}
if (props.original === undefined && props.navbar && props.fullHeight) {
if (!isDiff.value && props.navbar && props.fullHeight) {
editor.addAction({
id: "fold-multiline",
label: t("fold_all_multi_lines"),
@@ -445,7 +447,7 @@
});
}
if (!props.original) {
if (!isDiff.value) {
editor.onDidContentSizeChange((_) => {
highlightPebble();
});
@@ -652,197 +654,197 @@
</script>
<style scoped lang="scss">
@import "../code/styles/code.scss";
@import "../code/styles/code.scss";
</style>
<style lang="scss">
@import "@kestra-io/ui-libs/src/scss/color-palette.scss";
@import "../../styles/layout/root-dark.scss";
@import "@kestra-io/ui-libs/src/scss/color-palette.scss";
@import "../../styles/layout/root-dark.scss";
.highlight-lines{
background-color: rgba($base-blue-400, .2);
}
.editor-content-widget-content{
display: flex;
align-items: center;
justify-content: center;
.el-button-group {
display: inline-flex;
}
}
:not(.namespace-defaults, .el-drawer__body) > .ks-editor {
flex-direction: column;
height: 100%;
}
.el-form .ks-editor {
display: flex;
width: 100%;
}
.ks-editor {
display: flex;
.top-nav {
background-color: var(--ks-background-card);
padding: 0.5rem;
border-radius: var(--bs-border-radius-lg);
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
html.dark & {
background-color: var(--bs-gray-100);
}
.highlight-lines{
background-color: rgba($base-blue-400, .2);
}
.editor-absolute-container {
position: absolute;
top: 8px;
right: 20px;
z-index: 10;
color: var(--ks-content-secondary);
cursor: pointer;
}
.editor-absolute-container > * {
pointer-events: auto;
}
.editor-container {
.editor-content-widget-content{
display: flex;
flex-grow: 1;
align-items: center;
justify-content: center;
&.single-line {
min-height: var(--el-component-size);
padding: 1px 11px;
background-color: var(
--el-input-bg-color,
var(--el-fill-color-blank)
);
border-radius: var(
--el-input-border-radius,
var(--el-border-radius-base)
);
transition: var(--el-transition-box-shadow);
box-shadow: 0 0 0 1px var(--ks-border-primary) inset;
padding-top: 7px;
.el-button-group {
display: inline-flex;
}
}
&.custom-dark-vs-theme {
background-color: var(--ks-background-input);
}
:not(.namespace-defaults, .el-drawer__body) > .ks-editor {
flex-direction: column;
height: 100%;
}
&.theme-light {
background-color: $base-white;
.el-form .ks-editor {
display: flex;
width: 100%;
}
.ks-editor {
display: flex;
.top-nav {
background-color: var(--ks-background-card);
padding: 0.5rem;
border-radius: var(--bs-border-radius-lg);
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
html.dark & {
background-color: var(--bs-gray-100);
}
}
.placeholder {
.editor-absolute-container {
position: absolute;
top: -3px;
overflow: hidden;
padding-left: inherit;
padding-right: inherit;
cursor: text;
user-select: none;
color: var(--ks-content-inactive);
top: 8px;
right: 20px;
z-index: 10;
color: var(--ks-content-secondary);
cursor: pointer;
}
.editor-wrapper {
min-width: 75%;
width: 100%;
.editor-absolute-container > * {
pointer-events: auto;
}
.monaco-hover-content {
h4 {
font-size: var(--font-size-base);
font-weight: bold;
line-height: var(--bs-body-line-height);
.editor-container {
display: flex;
flex-grow: 1;
&.single-line {
min-height: var(--el-component-size);
padding: 1px 11px;
background-color: var(
--el-input-bg-color,
var(--el-fill-color-blank)
);
border-radius: var(
--el-input-border-radius,
var(--el-border-radius-base)
);
transition: var(--el-transition-box-shadow);
box-shadow: 0 0 0 1px var(--ks-border-primary) inset;
padding-top: 7px;
&.custom-dark-vs-theme {
background-color: var(--ks-background-input);
}
p {
margin-bottom: 0.5rem;
&.theme-light {
background-color: $base-white;
}
}
&:last-child {
display: none;
.placeholder {
position: absolute;
top: -3px;
overflow: hidden;
padding-left: inherit;
padding-right: inherit;
cursor: text;
user-select: none;
color: var(--ks-content-inactive);
}
.editor-wrapper {
min-width: 75%;
width: 100%;
.monaco-hover-content {
h4 {
font-size: var(--font-size-base);
font-weight: bold;
line-height: var(--bs-body-line-height);
}
p {
margin-bottom: 0.5rem;
&:last-child {
display: none;
}
}
*:nth-last-child(2n) {
margin-bottom: 0;
}
}
}
*:nth-last-child(2n) {
margin-bottom: 0;
.bottom-right {
bottom: 0px;
right: 0px;
ul {
display: flex;
list-style: none;
padding: 0;
margin: 0;
//gap: .5rem;
}
}
}
}
.bottom-right {
bottom: 0px;
right: 0px;
.custom-dark-vs-theme {
.monaco-editor,
.monaco-editor-background {
outline: none;
background-color: var(--ks-background-input);
--vscode-editor-background: var(--ks-background-input);
--vscode-breadcrumb-background: var(--ks-background-input);
--vscode-editorGutter-background: var(--ks-background-input);
}
ul {
display: flex;
list-style: none;
padding: 0;
margin: 0;
//gap: .5rem;
.monaco-editor .margin {
background-color: var(--ks-background-input);
--vscode-editorGutter-background: var(--ks-background-input);
--vscode-editorLineNumber-activeForeground: var(--ks-content-secondary);
--vscode-editorLineNumber-foreground: var(--ks-content-secondary);
--vscode-editorLineNumber-rangeHighlightBackground: var(--ks-content-secondary);
}
}
.highlight-text {
cursor: pointer;
font-weight: 700;
box-shadow: 0 19px 44px rgba(157, 29, 236, 0.31);
html.dark & {
background-color: rgba(255, 255, 255, 0.2);
}
}
.highlight-pebble {
color: #977100 !important;
html.dark & {
color: #ffca16 !important;
}
}
.disable-text {
color: var(--ks-content-inactive) !important;
}
div.img {
min-height: 130px;
height: 100%;
&.get-started {
background: url("../../assets/onboarding/onboarding-doc-light.svg")
no-repeat center;
html.dark & {
background: url("../../assets/onboarding/onboarding-doc-dark.svg")
no-repeat center;
}
}
}
}
.custom-dark-vs-theme {
.monaco-editor,
.monaco-editor-background {
outline: none;
background-color: var(--ks-background-input);
--vscode-editor-background: var(--ks-background-input);
--vscode-breadcrumb-background: var(--ks-background-input);
--vscode-editorGutter-background: var(--ks-background-input);
}
.monaco-editor .margin {
background-color: var(--ks-background-input);
--vscode-editorGutter-background: var(--ks-background-input);
--vscode-editorLineNumber-activeForeground: var(--ks-content-secondary);
--vscode-editorLineNumber-foreground: var(--ks-content-secondary);
--vscode-editorLineNumber-rangeHighlightBackground: var(--ks-content-secondary);
}
}
.highlight-text {
cursor: pointer;
font-weight: 700;
box-shadow: 0 19px 44px rgba(157, 29, 236, 0.31);
html.dark & {
background-color: rgba(255, 255, 255, 0.2);
}
}
.highlight-pebble {
color: #977100 !important;
html.dark & {
color: #ffca16 !important;
}
}
.disable-text {
color: var(--ks-content-inactive) !important;
}
div.img {
min-height: 130px;
height: 100%;
&.get-started {
background: url("../../assets/onboarding/onboarding-doc-light.svg")
no-repeat center;
html.dark & {
background: url("../../assets/onboarding/onboarding-doc-dark.svg")
no-repeat center;
}
}
}
</style>

View File

@@ -4,7 +4,7 @@
id="editorWrapper"
ref="editorRefElement"
class="flex-1"
:model-value="draftSource === undefined ? source : draftSource"
:model-value="hasDraft ? draftSource : source"
:schema-type="isCurrentTabFlow ? 'flow': undefined"
:lang="extension === undefined ? 'yaml' : undefined"
:extension="extension"
@@ -19,7 +19,7 @@
@execute="execute"
@mouse-move="(e) => highlightHoveredTask(e.target?.position?.lineNumber)"
@mouse-leave="() => highlightHoveredTask(-1)"
:original="draftSource === undefined ? undefined : source"
:original="hasDraft ? source : undefined"
:diff-side-by-side="false"
>
<template #absolute>
@@ -45,7 +45,7 @@
/>
</Transition>
<AcceptDecline
v-if="draftSource !== undefined"
v-if="hasDraft"
@accept="acceptDraft"
@reject="declineDraft"
/>
@@ -120,7 +120,7 @@
const content = await namespacesStore.readFile({namespace: fileNamespace.toString(), path: props.path ?? ""})
editorStore.setTabContent({path: props.path, content})
}
onMounted(() => {
loadPluginsHash();
@@ -162,13 +162,13 @@
hash.value = config.pluginsHash;
});
}
function editorUpdate(newValue: string){
if (editorContent.value === newValue) {
return;
}
if (isCurrentTabFlow.value) {
if (draftSource.value !== undefined) {
if (hasDraft.value) {
draftSource.value = newValue;
} else {
flowStore.flowYaml = newValue;
@@ -292,6 +292,8 @@
aiAgentOpened.value = true;
}
const hasDraft = computed(() => draftSource.value !== undefined);
const {
playgroundStore,
highlightHoveredTask,

View File

@@ -727,7 +727,10 @@
});
if (editorRef.value) {
localEditor.value = monaco.editor.create(editorRef.value, options);
localEditor.value = monaco.editor.create(editorRef.value, {
...options,
fixedOverflowWidgets: true // Helps suggestion widget render above other elements
});
if (props.suggestionsOnFocus) {
localEditor.value.onMouseDown(() => {

View File

@@ -41,12 +41,12 @@
import ArrowRight from "vue-material-design-icons/ArrowRight.vue";
const router = useRouter();
const {generateMenu} = useLeftMenu()
const {menu} = useLeftMenu()
const filter = ref("");
const navItems = computed(() => {
return generateMenu().flatMap(item => {
return menu.value.flatMap(item => {
if(item.hidden) {
return [];
}

View File

@@ -3,7 +3,7 @@
ref="sideBarRef"
data-component="FILENAME_PLACEHOLDER"
id="side-menu"
:menu="localMenu"
:menu
@update:collapsed="onToggleCollapse"
width="268px"
:collapsed="collapsed"
@@ -29,11 +29,9 @@
</sidebar-menu>
</template>
<script setup>
<script setup lang="ts">
import {
watch,
onUpdated,
onMounted,
ref,
computed,
shallowRef, h
@@ -50,34 +48,20 @@
import Environment from "./Environment.vue";
import BookmarkLinkList from "./BookmarkLinkList.vue";
import {useBookmarksStore} from "../../stores/bookmarks";
import type {MenuItem} from "override/components/useLeftMenu.js";
const props = defineProps({
generateMenu: {
type: Function,
required: true
},
showLink: {
type: Boolean,
default: true
}
const props = withDefaults(defineProps<{
menu: MenuItem[],
showLink: boolean
}>(), {
showLink: true
})
const $emit = defineEmits(["menu-collapse"])
const $route = useRoute()
const {locale, t} = useI18n({useScope: "global"});
function flattenMenu(menu) {
return menu.reduce((acc, item) => {
if (item.child) {
acc.push(...flattenMenu(item.child));
}
acc.push(item);
return acc;
}, []);
}
const {t} = useI18n({useScope: "global"});
function onToggleCollapse(folded) {
collapsed.value = folded;
@@ -136,43 +120,11 @@
component: () => h(BookmarkLinkList, {pages: bookmarksStore.pages}),
}]
}] : []),
...disabledCurrentRoute(props.generateMenu())
...disabledCurrentRoute(props.menu)
];
});
watch(locale, () => {
localMenu.value = menu.value;
}, {deep: true});
/**
* @type {import("vue").Ref<typeof import('vue-sidebar-menu').SidebarMenu>}
*/
const sideBarRef = ref(null);
watch(menu, (newVal, oldVal) => {
// Check if the active menu item has changed, if yes then update the menu
if (JSON.stringify(flattenMenu(newVal).map(e => e.class?.includes("vsm--link_active") ?? false)) !==
JSON.stringify(flattenMenu(oldVal).map(e => e.class?.includes("vsm--link_active") ?? false))) {
localMenu.value = newVal;
sideBarRef.value?.$el.querySelectorAll(".vsm--item span").forEach(e => {
//empty icon name on mouseover
e.setAttribute("title", "")
});
}
},
{
flush: "post",
deep: true
});
const collapsed = ref(localStorage.getItem("menuCollapsed") === "true")
const localMenu = ref([])
onMounted(() => {
localMenu.value = menu.value;
})
</script>
<style lang="scss">

View File

@@ -193,6 +193,7 @@
</script>
<style scoped lang="scss">
div.line {
position: relative;
cursor: text;
white-space: pre-wrap;
word-break: break-all;
@@ -274,5 +275,16 @@ div.line {
border: 1px solid var(--ks-border-primary);
user-select: none;
}
:deep(.clipboard) {
opacity: 0;
pointer-events: none;
transition: opacity 0.15s ease-in-out;
}
&:hover :deep(.clipboard) {
opacity: 1;
pointer-events: auto;
}
}
</style>
</style>

View File

@@ -57,6 +57,7 @@
import * as YAML_UTILS from "@kestra-io/ui-libs/flow-yaml-utils";
import YAML_CHART from "../dashboard/assets/logs_timeseries_chart.yaml?raw";
import {useLogsStore} from "../../stores/logs";
import {defaultNamespace} from "../../composables/useNamespaces";
export default {
mixins: [RouteContext, RestoreUrl, DataTableActions],
@@ -147,15 +148,12 @@
}
},
beforeRouteEnter(to, _, next) {
const defaultNamespace = localStorage.getItem(
storageKeys.DEFAULT_NAMESPACE,
);
const query = {...to.query};
let queryHasChanged = false;
const queryKeys = Object.keys(query);
if (defaultNamespace && !queryKeys.some(key => key.startsWith("filters[namespace]"))) {
query["filters[namespace][PREFIX]"] = defaultNamespace;
if (defaultNamespace() && !queryKeys.some(key => key.startsWith("filters[namespace]"))) {
query["filters[namespace][PREFIX]"] = defaultNamespace();
queryHasChanged = true;
}
@@ -170,12 +168,6 @@
}
},
methods: {
LogFilterLanguage() {
return LogFilterLanguage
},
onDateFilterTypeChange(event) {
this.canAutoRefresh = event;
},
showStatChart() {
return this.showChart;
},

View File

@@ -37,6 +37,9 @@
});
onMounted(() => {
const main = document.querySelector("main");
if(main) main.scrollTop = 0;
if (namespace.value) {
namespacesStore.load(namespace.value);
}

View File

@@ -41,7 +41,7 @@
import {useNamespacesStore} from "override/stores/namespaces"
import DotsSquare from "vue-material-design-icons/DotsSquare.vue"
import Lock from "vue-material-design-icons/Lock.vue";
import {storageKeys} from "../../../utils/constants";
import {defaultNamespace} from "../../../composables/useNamespaces";
const {t} = useI18n();
@@ -79,13 +79,13 @@
onMounted(() => {
if (modelValue.value === undefined || modelValue.value.length === 0) {
const defaultNamespace = localStorage.getItem(storageKeys.DEFAULT_NAMESPACE);
const defaultNamespaceVal = defaultNamespace();
if (Array.isArray(modelValue.value)) {
if (defaultNamespace != null) {
modelValue.value = [defaultNamespace];
if (defaultNamespaceVal != null) {
modelValue.value = [defaultNamespaceVal];
}
} else {
modelValue.value = defaultNamespace ?? modelValue.value;
modelValue.value = defaultNamespaceVal ?? modelValue.value;
}
}
})

View File

@@ -277,6 +277,7 @@
import Column from "./components/block/Column.vue"
import {useAuthStore} from "override/stores/auth"
import {useFlowStore} from "../../stores/flow"
import {defaultNamespace} from "../../composables/useNamespaces";
export const DATE_FORMAT_STORAGE_KEY = "dateFormat";
export const TIMEZONE_STORAGE_KEY = "timezone";
@@ -342,7 +343,7 @@
};
},
created() {
this.pendingSettings.defaultNamespace = localStorage.getItem("defaultNamespace") || "company.team";
this.pendingSettings.defaultNamespace = defaultNamespace();
this.pendingSettings.editorType = localStorage.getItem(storageKeys.EDITOR_VIEW_TYPE) || "YAML";
this.pendingSettings.defaultLogLevel = localStorage.getItem("defaultLogLevel") || "INFO";
this.pendingSettings.lang = Utils.getLang();

View File

@@ -173,9 +173,6 @@
},
},
methods: {
onDateFilterTypeChange(event) {
this.canAutoRefresh = event;
},
isRunning(item){
return State.isRunning(item.state.current);
},

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