Compare commits

...

123 Commits

Author SHA1 Message Date
brian.mulier
23bde6b716 chore: update version to v0.21.3 2025-02-18 20:41:47 +01:00
brian.mulier
0b2df61c2e ci(publish-docker): usage of qemu-user-static 2025-02-18 20:41:47 +01:00
brian.mulier
d30b331b3c fix(tests): increase timeout on JdbcServiceLivenessCoordinatorTest.taskResubmitSkipExecution 2025-02-18 17:58:14 +01:00
brian.mulier
1fa026f0ee fix(core): render delete property at the beginning in Docker task runner 2025-02-18 17:58:13 +01:00
brian.mulier
3a39c65829 fix(core): remove props with default from required in json schema to avoid validation errors
closes #7406
2025-02-18 17:58:13 +01:00
aeSouid
b174a81562 fix decode method call for labels 2025-02-18 17:58:13 +01:00
YannC
077421d59c fix(core): provide tenantId when looking for subflow (#7442) 2025-02-18 17:58:13 +01:00
Miloš Paunović
fcf999ff61 fix(ui): improve modifying inputs from no code editor (#7440) 2025-02-18 17:58:13 +01:00
Mathieu Gabelle
3e2f798ccf Revert "fix: enable rendering of commands properties inside CommandsWrapper (#7381)"
This reverts commit c95c8ed20f2905b37b559242fe479aff48cc9493.
2025-02-18 17:58:13 +01:00
Mathieu Gabelle
69faecf339 Revert "refactor: introduce render in commands wrapper for property string (#7430)"
This reverts commit 4f303f1d16b1c97fab77db32a83f6736d083e4e8.
2025-02-18 17:58:12 +01:00
Mathieu Gabelle
aa3a6854ae refactor: introduce render in commands wrapper for property string (#7430) 2025-02-18 17:58:12 +01:00
Mathieu Gabelle
bb6edfff98 fix: enable rendering of commands properties inside CommandsWrapper (#7381)
* fix: move commands to Property

migrate to Property in TaskCommands and CommandsWrapper
implement beforeCommand and interpreter
2025-02-18 17:58:11 +01:00
Miloš Paunović
f7b495d22f chore(ui): improve the labels behavior (#7397) 2025-02-18 17:58:11 +01:00
Miloš Paunović
eaf63f307c chore(ui): improve breadcrumbs on namespace view (#7386) 2025-02-18 17:58:11 +01:00
Miloš Paunović
905f778204 chore(ui): include autocompletion shortcut in the preview list (#7384) 2025-02-18 17:58:11 +01:00
Miloš Paunović
0b15711b23 chore(ui): add link to filtered triggers page from backfill dialog (#7380) 2025-02-18 17:58:11 +01:00
YannC
a51b193f4b fix(scheduler): delete trigger when flow is not found (#7366)
close #7312
2025-02-18 17:58:11 +01:00
YannC
42a7938d38 chore: update version to v0.21.2 2025-02-13 09:15:56 +01:00
Ludovic DEHON
5783a95db3 fix(cicd): add mariadb plugins on docker image 2025-02-13 09:15:56 +01:00
YannC
785afe7884 fix(h2): remove indenting in sql file (#7306) 2025-02-11 13:52:53 +01:00
Miloš Paunović
28fea2e5dc chore(ui): remove the option to change editor theme separately (#7192)
* chore(ui): remove obsolete log statements from console

* chore(ui): remove the option to change editor theme separately
2025-02-11 13:51:46 +01:00
YannC
dcc59fde35 ui(): missing translation 2025-02-11 13:49:53 +01:00
YannC
4e9ac8b3a2 fix(core): do not validate subflow if namespace or id is pebble (#7294)
close #7271
2025-02-11 13:44:44 +01:00
Ludovic DEHON
5d5b74613b fix(core): http client was not using deprecated setter 2025-02-11 11:23:07 +01:00
Florian Hussonnois
44c149e8d5 fix(core): make flow/namespace variables available for input expr
related-to: kestra-io/kestra-ee#2826
2025-02-11 10:44:43 +01:00
Bart Ledoux
c262525341 fix: use the udpated labelsFromQuery in labels 2025-02-11 08:25:52 +01:00
Miloš Paunović
7da24df76f chore(ui): make action columns always visible on executions and flows (#7291) 2025-02-11 08:18:17 +01:00
Miloš Paunović
2664307517 chore(ui): disable saving flow actions if there are errors (#7278) 2025-02-10 12:00:20 +01:00
Aabhas Sao
8c0f0f86b6 chore(ui): align chart duration label with switch toggle (#7259) 2025-02-10 10:32:40 +01:00
Florian Hussonnois
b651f53e8a fix(ci): fix and remove unecessary setps in set version workflows 2025-02-07 12:01:52 +01:00
Florian Hussonnois
10fad29923 feat(ci): add workflows for release process
* move scripts to folder dev-tools
* add new workflow gradle-release.yml
* add new workflow setversion-tag.yml
* rename existing workflow
2025-02-07 12:01:32 +01:00
Bart Ledoux
d9962a89a7 fix: labels should not be purple if inactive 2025-02-07 11:50:13 +01:00
Bart Ledoux
60b189d101 fix: add comment on i18n code 2025-02-07 11:49:10 +01:00
咬轮猫
6b065815b7 fix(ui): amend the language switching issue (#7235) 2025-02-07 11:48:48 +01:00
Loïc Mathieu
8c943b43f0 fix(core): possible NPE on LabelService.containsAll 2025-02-06 16:26:48 +01:00
Piyush Bhaskar
8b813115a9 chore(ui): only show warning on bulk execution deletion if nonTerminated is true (#7211)
Co-authored-by: MilosPaunovic <paun992@hotmail.com>
2025-02-06 15:31:00 +01:00
Miloš Paunović
4a6bb0ba87 chore(ui): generate random flow ID using combination of animal names and numbers (#7223)
* chore(ui): creating a flow from the namespace view should use it's ID as the designated value

* chore(ui): generate random flow ID using combination of animal names and numbers
2025-02-06 15:30:51 +01:00
Pravesh-Sudha
a2daf0f493 chore(ui): amended global pagination coloring (#7201)
Co-authored-by: MilosPaunovic <paun992@hotmail.com>
2025-02-06 13:13:28 +01:00
MilosPaunovic
0e3218c7be fix(ui): amend bar chart colors on the main dashboard 2025-02-06 12:48:11 +01:00
Barthélémy Ledoux
d98c5e19fc feat: theme switch to "theme switch" the charts (#7151)
* chore: store the theme in the store

* use the new theme in charts

* use the theme value in more places

* create a useTheme composable

* create the useScheme composable

* restore nodata
2025-02-06 12:38:56 +01:00
MilosPaunovic
e086099d6c fix(ui): prevent doubling the executions chart on flow overview 2025-02-06 12:02:07 +01:00
Bart Ledoux
df3bec4d6c chore: remove console.log 2025-02-06 09:54:58 +01:00
Miloš Paunović
4b946175bf chore(ui): re-order the list of optional columns (#7213) 2025-02-06 09:37:41 +01:00
Florian Hussonnois
0e891f64a2 chore(version): update to version 'v0.21.1' 2025-02-05 19:05:25 +01:00
nKwiatkowski
47cc38d89e fix(core): request option doesn't initialize properly 2025-02-05 19:04:38 +01:00
Miloš Paunović
d2f9060b5c chore(ui): replace the visual for no tabs opened on namespace editor (#7204) 2025-02-05 18:37:57 +01:00
Barthélémy Ledoux
c36cc504eb chore(ui): add the missing chart component 2025-02-05 18:24:23 +01:00
Bart Ledoux
8d3b3a8493 fix: remove editor theme from english 2025-02-05 17:25:40 +01:00
Nicolas K.
e7955ca7bf fix(core): #7181 log level rendered as string (#7198)
Co-authored-by: nKwiatkowski <nkwiatkowski@kestra.io>
2025-02-05 16:32:12 +01:00
Miloš Paunović
016cd09849 chore(translations): remove extra keys from translation files (#7193) 2025-02-05 13:25:31 +01:00
GitHub Action
23846d6100 chore(translations): auto generate values for languages other than english 2025-02-05 13:23:40 +01:00
Miloš Paunović
0b247b709e chore(ui): rename advanced properties to other in no code (#7190) 2025-02-05 13:23:32 +01:00
GitHub Action
bfee53a9b1 chore(translations): auto generate values for languages other than english 2025-02-05 13:23:26 +01:00
Miloš Paunović
70a3c98aca chore(ui): rename advanced properties to other in no code (#7189) 2025-02-05 13:23:19 +01:00
GitHub Action
a923124108 chore(translations): auto generate values for languages other than english 2025-02-05 13:22:46 +01:00
Piyush Bhaskar
92484c0333 feat(ui): Docs markdown alert styled based on alert level in product. (#6818)
* feat(ui): Align the CSS or style configuration so that all documentation components (Docs, plugin docs, blueprints) use the same markdown style.

* Remove alert styling from DocsLayout since it has been handled within Alert.vue using ---ks variables.

* revert Input_Count.

---------

Co-authored-by: Barthélémy Ledoux <ledouxb@me.com>
Co-authored-by: Miloš Paunović <paun992@hotmail.com>
2025-02-05 13:21:27 +01:00
Piyush Bhaskar
eb21452a83 feat(ui): add option to choose visible columns in flow and execution listings (#6932)
Co-authored-by: Miloš Paunović <paun992@hotmail.com>
2025-02-05 13:21:20 +01:00
Piyush Bhaskar
433fe963e2 chore(ui): improve the states options list inside filter values (#7176)
Co-authored-by: Miloš Paunović <paun992@hotmail.com>
2025-02-05 13:19:54 +01:00
Piyush Bhaskar
7a2390ddf7 chore(ui): update the visual of no data component (#7179)
Co-authored-by: MilosPaunovic <paun992@hotmail.com>
2025-02-05 13:19:36 +01:00
Shruti Mantri
1c6a14d17a chore(ui): improve the example for not condition (#6820) 2025-02-05 13:19:25 +01:00
Miloš Paunović
0ba64f7979 fix(ui): align dashboard button label to icon (#7175) 2025-02-05 13:18:55 +01:00
GitHub Action
38720e96a9 chore(translations): auto generate values for languages other than english 2025-02-05 13:18:15 +01:00
Miloš Paunović
0f7d9b2adc fix(ui): amend translation string for no results (#7172) 2025-02-05 13:18:08 +01:00
GitHub Action
210fc246ac chore(translations): auto generate values for languages other than english 2025-02-05 13:18:02 +01:00
Aabhas Sao
df0d037f66 chore(ui): enable command palette for monaco editor (#6944)
Signed-off-by: Aabhas Sao <aabhassao0@gmail.com>
Co-authored-by: Miloš Paunović <paun992@hotmail.com>
2025-02-05 13:17:58 +01:00
Piyush Bhaskar
07ea309a47 chore(ui): amend color of the input length counter (#6990) 2025-02-05 13:17:38 +01:00
GitHub Action
1f09f53a88 chore(translations): auto generate values for languages other than english 2025-02-05 13:15:38 +01:00
Piyush Bhaskar
f356921daa feat(ui): add keyboard shortcuts dialog to editor (#6628)
Co-authored-by: Miloš Paunović <paun992@hotmail.com>
2025-02-05 13:15:26 +01:00
rajatsingh23
3d50ef03f7 chore(ui): show each plugin deprecation warning in new line (#6839)
Co-authored-by: Miloš Paunović <paun992@hotmail.com>
2025-02-05 13:15:18 +01:00
Miloš Paunović
7b309eb2d2 fix(ui): amend pagination on namespace flows listing (#7163) 2025-02-05 13:15:04 +01:00
Barthélémy Ledoux
b22b0642ed feat: show a lock on EE only pages (#7093) 2025-02-05 09:46:18 +01:00
Barthélémy Ledoux
1cbc9195c4 fix: make table links primary instead of purple (#7106) 2025-02-05 09:46:18 +01:00
Barthélémy Ledoux
b853dd0b6e fix: use the proper variable for select header in table (#7107) 2025-02-05 09:46:17 +01:00
Bart Ledoux
f7df60419c fix: avoid clearing selected value on every error
closes #7115
2025-02-05 09:46:17 +01:00
brian.mulier
9f76cae55e fix(ui): global plugin doc with new redesign + auto-expand properties initially 2025-02-04 22:03:15 +01:00
Loïc Mathieu
aca5a9ff4c chore: version 0.21.0 2025-02-04 13:42:40 +01:00
brian.mulier
a6ce86d702 fix(ui): null-safe search filters 2025-02-04 11:41:18 +01:00
Ludovic DEHON
4392c89ec7 fix(core): process runner are not serialized correctly on worker
close #7053
2025-02-03 21:28:03 +01:00
Loïc Mathieu
d74a31ba7f chore: version 0.21.0-rc2-SNAPSHOT 2025-02-03 16:08:11 +01:00
Bart Ledoux
cb3195900f fix: enterprise edition tag in light mode 2025-02-03 16:07:14 +01:00
Bart Ledoux
cf4b91f44d fix: bring back hover in main menu 2025-02-03 16:06:17 +01:00
Bart Ledoux
33ecf8d5f5 fix: sidemenu bring back the gray hover 2025-02-03 16:06:09 +01:00
Bart Ledoux
39a2293a45 fix: setup docId for blueprints 2025-02-03 16:05:45 +01:00
Miloš Paunović
88c93995df fix(ui): get the string fields in no code to use editor and have auto completion back (#7150) 2025-02-03 15:06:26 +01:00
MilosPaunovic
6afe5ff41f chore(ui): properly pass a prop related to saved searches 2025-02-03 15:06:26 +01:00
Miloš Paunović
a3a8863f46 feat(ui): multiple improvements of no code editor (#7146)
* refactor(ui): prevent multiple warning in console by adding inheritAttrs properly

* chore(ui): make plugin selector field not clearable

* feat(ui): allow re-ordering of array items

* fix(ui): remove concurrency when limit set to 0
2025-02-03 15:06:26 +01:00
Piyush Bhaskar
fcfee5116b fix(ui): Custom Dashboard name overflows. (#7124)
* fix(ui): Custom Dashboard name overflows.

* fix(ui): avoid dashboard button being too long

---------

Co-authored-by: YannC <ycoornaert@kestra.io>
2025-02-03 15:05:30 +01:00
brian.mulier
3f2d91014b fix(ui): switching from custom Flow blueprints tab to dashboard was not working 2025-02-03 14:00:53 +01:00
Florian Hussonnois
41149a83b3 ci: fix release workflows 2025-02-03 11:50:23 +01:00
Loïc Mathieu
1ed882e8f3 fix(core): remove the dynamic property patterns 2025-02-03 10:08:09 +01:00
brian-mulier-p
0f6e0de29c fix(ui): restore namespace filter manual typing & various improvements (#7127) 2025-02-01 10:09:19 +01:00
brian.mulier
238bc532c3 chore(deps): bump ui-libs to v0.0.125 2025-01-31 18:15:47 +01:00
Florian Hussonnois
6919848ab3 ci: fix runner on release workflows 2025-01-31 16:29:20 +01:00
Florian Hussonnois
86aec88de4 chore(version): update to version 'v0.21.0-rc1-SNAPSHOT' 2025-01-31 15:54:24 +01:00
Bart Ledoux
f609d57a0c build: prevent corepack crash 2025-01-31 15:54:24 +01:00
Bart Ledoux
f3852a3c24 build: try and fix FE CI 2025-01-31 15:54:24 +01:00
GitHub Action
804ff6a81c chore(translations): auto generate values for languages other than english 2025-01-31 13:52:51 +01:00
Miloš Paunović
7869f90edd feat(ui): add finally block to no code editor (#7123) 2025-01-31 13:52:45 +01:00
Florian Hussonnois
2b72306b3d fix(ci): update scripts/workflows for plugins 2025-01-31 12:10:30 +01:00
Florian Hussonnois
f0d5d4b93f ci: fix workflow docker for all plugins 2025-01-31 11:45:45 +01:00
Florian Hussonnois
4e4ab80b2f ci: fix workflow docker 2025-01-31 11:45:31 +01:00
Florian Hussonnois
c33d08afda ci: update workflow docker 2025-01-31 11:45:17 +01:00
Florian Hussonnois
a246ac38f5 ci: update workflow docker 2025-01-31 11:45:07 +01:00
Florian Hussonnois
7bdaa81dee fix(ui): fix missing param kind for blueprint in flow editor (#7087)
fix: #7087
2025-01-31 11:44:11 +01:00
Miloš Paunović
6a1d831849 feat(ui): allow task re-ordering from no code editor (#7120) 2025-01-31 10:56:16 +01:00
Loïc Mathieu
95d2d1dfa3 fix(core): retry flaky test TimeoutTest.timeout()
As its failure cannot be reproduced locally even with 100 repetitions, there is no other choice than retrying it.
2025-01-31 09:48:11 +01:00
Loïc Mathieu
d12dd179c2 fix(core): subflow labels must not be overriden by parent flow ones 2025-01-31 09:47:59 +01:00
Loïc Mathieu
ceda5eb8ee fix(core): subflow validation didn't work anymore 2025-01-30 16:17:59 +01:00
Miloš Paunović
1301aaac76 feat(ui): improve the task array component (#7095)
* feat(ui): improve the task array component

* chore(ui): replace existing task on editing during creation instead of re-adding them
2025-01-30 14:37:09 +01:00
AJ Emerich
5f7468a9a4 fix(docs): remove custom dashboard website component
https://github.com/kestra-io/kestra/issues/7085
2025-01-30 12:16:30 +01:00
Miloš Paunović
aa24c888a3 chore(ui): properly check the existence of fields inside schema
* chore(ui): remove unnecessary binding of listeners

* chore(ui): check the existence of fields
2025-01-30 11:42:37 +01:00
Loïc Mathieu
c792d9b6ea fix(cli): repeate flaky tests FileChangedEventListenerTest
This is inherently racy as it's async and watch the filesystem which cannot be done reliabily.
2025-01-30 10:55:57 +01:00
Loïc Mathieu
a921b95404 chore(deps): downgrade Protobuf to 3.25.5
3.25.6 is not compatible with 3.25.5 and Orc still uses 3.25.5
2025-01-30 10:40:57 +01:00
Miloš Paunović
e46df069a9 feat(ui): multiple improvements of no code editor (#7076)
* fix(ui): allow creation of multiple tasks from the no code editor

* chore(ui): make input text be of textarea type for resizability

* chore(ui): allow to add task from topology either before or after the target one
2025-01-30 10:33:11 +01:00
Loïc Mathieu
c08f4f24ca fix(script): AbstractExecScript.injectDefaults should throw IllegalVariableEvaluationException 2025-01-30 09:58:45 +01:00
Miloš Paunović
67b3937824 chore(ui): move apps link in left menu just below the flows (#7063) 2025-01-30 09:25:16 +01:00
Miloš Paunović
17e1623342 fix(ui): amend no code editor breadcrumbs issue (#7054)
* chore(ui): task array component to have margins between lines

* fix(ui): amend no code editor breadcrumbs issue
2025-01-30 09:25:01 +01:00
Loïc Mathieu
d12fbf05b0 fix(core): restartForEachItem() is flaky
With this test change, running 100 tests with MySQL pass!
2025-01-29 17:11:14 +01:00
YannC
efa2d44e76 feat(webserver): if no date provided for dashboard, then use default timewindow 2025-01-29 16:37:27 +01:00
YannC
acdb46cea0 fix(ui): dynamic format date
close #7015
2025-01-29 16:37:21 +01:00
Loïc Mathieu
c1807516f5 chore(deps): downgrade protobug
Orc uses an older version.
And probably also other libs that we're using are still in 3.x
2025-01-29 15:52:10 +01:00
brian.mulier
ab796dff93 feat(ui): don't show deprecated tasks in the plugins list
closes #4526
2025-01-29 15:49:23 +01:00
Loïc Mathieu
2d98f909de fix(cli): flow watcher should compute plugin defaults
fixes #6908
2025-01-29 15:42:55 +01:00
166 changed files with 3765 additions and 1552 deletions

View File

@@ -9,6 +9,8 @@ jobs:
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
GOOGLE_SERVICE_ACCOUNT: ${{ secrets.GOOGLE_SERVICE_ACCOUNT }}
# to save corepack from itself
COREPACK_INTEGRITY_KEYS: 0
name: Check & Publish
runs-on: ubuntu-latest
timeout-minutes: 60

View File

@@ -1,4 +1,4 @@
name: Create Docker images on tag
name: Create Docker images on Release
on:
workflow_dispatch:
@@ -11,6 +11,10 @@ on:
options:
- "true"
- "false"
release-tag:
description: 'Kestra Release Tag'
required: false
type: string
plugin-version:
description: 'Plugin version'
required: false
@@ -38,7 +42,6 @@ jobs:
name: Publish Docker
needs: [ plugins ]
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
strategy:
matrix:
image:
@@ -57,10 +60,19 @@ jobs:
- name: Set image name
id: vars
run: |
TAG=${GITHUB_REF#refs/*/}
echo "tag=${TAG}" >> $GITHUB_OUTPUT
echo "plugins=${{ matrix.image.plugins }}" >> $GITHUB_OUTPUT
if [[ "${{ inputs.release-tag }}" == "" ]]; then
TAG=${GITHUB_REF#refs/*/}
echo "tag=${TAG}" >> $GITHUB_OUTPUT
else
TAG="${{ inputs.release-tag }}"
echo "tag=${TAG}" >> $GITHUB_OUTPUT
fi
if [[ "${{ env.PLUGIN_VERSION }}" == *"-SNAPSHOT" ]]; then
echo "plugins=--repositories=https://s01.oss.sonatype.org/content/repositories/snapshots ${{ matrix.image.plugins }}" >> $GITHUB_OUTPUT;
else
echo "plugins=${{ matrix.image.plugins }}" >> $GITHUB_OUTPUT
fi
# Download release
- name: Download release
uses: robinraju/release-downloader@v1.11
@@ -87,6 +99,11 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Docker - Fix Qemu
shell: bash
run: |
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes -c yes
# Docker Build and push
- name: Push to Docker Hub
uses: docker/build-push-action@v6

View File

@@ -4,7 +4,7 @@ on:
workflow_dispatch:
inputs:
releaseVersion:
description: 'The release version (e.g., 0.21.0)'
description: 'The release version (e.g., 0.21.0-rc1)'
required: true
type: string
nextVersion:
@@ -18,13 +18,29 @@ on:
jobs:
release:
name: Release plugins
runs-on: kestra-private-standard
runs-on: ubuntu-latest
steps:
# Checkout
- uses: actions/checkout@v4
with:
fetch-depth: 0
# Checkout GitHub Actions
- uses: actions/checkout@v4
with:
repository: kestra-io/actions
path: actions
ref: main
# Setup build
- uses: ./actions/.github/actions/setup-build
id: build
with:
java-enabled: true
node-enabled: true
python-enabled: true
caches-enabled: true
# Get Plugins List
- name: Get Plugins List
uses: ./.github/actions/plugins-list
@@ -33,14 +49,20 @@ jobs:
with:
plugin-version: 'LATEST'
- name: 'Configure Git'
run: |
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --global user.name "github-actions[bot]"
# Execute
- name: Run Gradle Release
if: ${{ github.event.inputs.dryRun == 'false' }}
env:
GITHUB_PAT: ${{ secrets.GH_PERSONAL_TOKEN }}
run: |
chmod +x ./release-plugins.sh;
./release-plugins.sh \
chmod +x ./dev-tools/release-plugins.sh;
./dev-tools/release-plugins.sh \
--release-version=${{github.event.inputs.releaseVersion}} \
--next-version=${{github.event.inputs.nextVersion}} \
--yes \
@@ -51,8 +73,9 @@ jobs:
env:
GITHUB_PAT: ${{ secrets.GH_PERSONAL_TOKEN }}
run: |
chmod +x ./release-plugins.sh;
./release-plugins.sh \
chmod +x ./dev-tools/release-plugins.sh;
./dev-tools/release-plugins.sh \
--release-version=${{github.event.inputs.releaseVersion}} \
--next-version=${{github.event.inputs.nextVersion}} \
--dry-run \

89
.github/workflows/gradle-release.yml vendored Normal file
View File

@@ -0,0 +1,89 @@
name: Run Gradle Release
run-name: "Releasing Kestra ${{ github.event.inputs.releaseVersion }} 🚀"
on:
workflow_dispatch:
inputs:
releaseVersion:
description: 'The release version (e.g., 0.21.0-rc1)'
required: true
type: string
nextVersion:
description: 'The next version (e.g., 0.22.0-SNAPSHOT)'
required: true
type: string
env:
RELEASE_VERSION: "${{ github.event.inputs.releaseVersion }}"
NEXT_VERSION: "${{ github.event.inputs.nextVersion }}"
jobs:
release:
name: Release Kestra
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/develop'
steps:
# Checks
- name: Check Inputs
run: |
if ! [[ "$RELEASE_VERSION" =~ ^[0-9]+(\.[0-9]+)\.0-rc[01](-SNAPSHOT)?$ ]]; then
echo "Invalid release version. Must match regex: ^[0-9]+(\.[0-9]+)\.0-rc[01](-SNAPSHOT)?$"
exit 1
fi
if ! [[ "$NEXT_VERSION" =~ ^[0-9]+(\.[0-9]+)\.0-SNAPSHOT$ ]]; then
echo "Invalid next version. Must match regex: ^[0-9]+(\.[0-9]+)\.0-SNAPSHOT$"
exit 1;
fi
# Checkout
- uses: actions/checkout@v4
with:
fetch-depth: 0
# Checkout GitHub Actions
- uses: actions/checkout@v4
with:
repository: kestra-io/actions
path: actions
ref: main
# Setup build
- uses: ./actions/.github/actions/setup-build
id: build
with:
java-enabled: true
node-enabled: true
python-enabled: true
caches-enabled: true
- name: Configure Git
run: |
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --global user.name "github-actions[bot]"
# Execute
- name: Run Gradle Release
env:
GITHUB_PAT: ${{ secrets.GH_PERSONAL_TOKEN }}
run: |
# Extract the major and minor versions
BASE_VERSION=$(echo "$RELEASE_VERSION" | sed -E 's/^([0-9]+\.[0-9]+)\..*/\1/')
PUSH_RELEASE_BRANCH="releases/v${BASE_VERSION}.x"
# Create and push release branch
git checkout -b "$PUSH_RELEASE_BRANCH";
git push -u origin "$PUSH_RELEASE_BRANCH";
# Run gradle release
git checkout develop;
if [[ "$RELEASE_VERSION" == *"-SNAPSHOT" ]]; then
# -SNAPSHOT qualifier maybe used to test release-candidates
./gradlew release -Prelease.useAutomaticVersion=true \
-Prelease.releaseVersion="${RELEASE_VERSION}" \
-Prelease.newVersion="${NEXT_VERSION}" \
-Prelease.pushReleaseVersionBranch="${PUSH_RELEASE_BRANCH}" \
-Prelease.failOnSnapshotDependencies=false
else
./gradlew release -Prelease.useAutomaticVersion=true \
-Prelease.releaseVersion="${RELEASE_VERSION}" \
-Prelease.newVersion="${NEXT_VERSION}" \
-Prelease.pushReleaseVersionBranch="${PUSH_RELEASE_BRANCH}"
fi

View File

@@ -35,6 +35,8 @@ env:
DOCKER_APT_PACKAGES: python3 python3-venv python-is-python3 python3-pip nodejs npm curl zip unzip
DOCKER_PYTHON_LIBRARIES: kestra
PLUGIN_VERSION: ${{ github.event.inputs.plugin-version != null && github.event.inputs.plugin-version || 'LATEST' }}
# to save corepack from itself
COREPACK_INTEGRITY_KEYS: 0
jobs:
build-artifacts:
name: Build Artifacts
@@ -45,13 +47,14 @@ jobs:
docker-artifact-name: ${{ steps.vars.outputs.artifact }}
plugins: ${{ steps.plugins-list.outputs.plugins }}
steps:
# Checkout
- uses: actions/checkout@v4
- name: Checkout current ref
uses: actions/checkout@v4
with:
fetch-depth: 0
# Checkout GitHub Actions
- uses: actions/checkout@v4
- name: Checkout GitHub Actions
uses: actions/checkout@v4
with:
repository: kestra-io/actions
path: actions

View File

@@ -1,4 +1,4 @@
name: Update and Tag Kestra Plugins
name: Set Version and Tag Plugins
on:
workflow_dispatch:
@@ -14,7 +14,7 @@ on:
jobs:
tag:
name: Release plugins
runs-on: kestra-private-standard
runs-on: ubuntu-latest
steps:
# Checkout
- uses: actions/checkout@v4
@@ -29,25 +29,32 @@ jobs:
with:
plugin-version: 'LATEST'
- name: 'Configure Git'
run: |
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --global user.name "github-actions[bot]"
# Execute
- name: Tag Plugins
- name: Set Version and Tag Plugins
if: ${{ github.event.inputs.dryRun == 'false' }}
env:
GITHUB_PAT: ${{ secrets.GH_PERSONAL_TOKEN }}
run: |
chmod +x ./tag-release-plugins.sh;
./tag-release-plugins.sh \
chmod +x ./dev-tools/setversion-tag-plugins.sh;
./dev-tools/setversion-tag-plugins.sh \
--release-version=${{github.event.inputs.releaseVersion}} \
--yes \
${{ steps.plugins-list.outputs.repositories }}
- name: Run Gradle Release (DRY_RUN)
- name: Set Version and Tag Plugins (DRY_RUN)
if: ${{ github.event.inputs.dryRun == 'true' }}
env:
GITHUB_PAT: ${{ secrets.GH_PERSONAL_TOKEN }}
run: |
chmod +x ./tag-release-plugins.sh;
./tag-release-plugins.sh \
chmod +x ./dev-tools/setversion-tag-plugins.sh;
./dev-tools/setversion-tag-plugins.sh \
--release-version=${{github.event.inputs.releaseVersion}} \
--dry-run \
--yes \

58
.github/workflows/setversion-tag.yml vendored Normal file
View File

@@ -0,0 +1,58 @@
name: Set Version and Tag
run-name: "Set version and Tag Kestra to ${{ github.event.inputs.releaseVersion }} 🚀"
on:
workflow_dispatch:
inputs:
releaseVersion:
description: 'The release version (e.g., 0.21.1)'
required: true
type: string
env:
RELEASE_VERSION: "${{ github.event.inputs.releaseVersion }}"
jobs:
release:
name: Release Kestra
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/heads/releases/v')
steps:
# Checks
- name: Check Inputs
run: |
if ! [[ "$RELEASE_VERSION" =~ ^[0-9]+(\.[0-9]+)(\.[0-9]+)(-rc[0-9])?(-SNAPSHOT)?$ ]]; then
echo "Invalid release version. Must match regex: ^[0-9]+(\.[0-9]+)(\.[0-9]+)-(rc[0-9])?(-SNAPSHOT)?$"
exit 1
fi
CURRENT_BRANCH="{{ github.ref }}"
# Extract the major and minor versions
BASE_VERSION=$(echo "$RELEASE_VERSION" | sed -E 's/^([0-9]+\.[0-9]+)\..*/\1/')
RELEASE_BRANCH="refs/heads/releases/v${BASE_VERSION}.x"
if ! [[ "$CURRENT_BRANCH" == "$RELEASE_BRANCH" ]]; then
echo "Invalid release branch. Expected $RELEASE_BRANCH, was $CURRENT_BRANCH"
exit 1
fi
# Checkout
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Configure Git
run: |
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --global user.name "github-actions[bot]"
# Execute
- name: Run Gradle Release
env:
GITHUB_PAT: ${{ secrets.GH_PERSONAL_TOKEN }}
run: |
# Update version
sed -i "s/^version=.*/version=$RELEASE_VERSION/" ./gradle.properties
git add ./gradle.properties
git commit -m"chore(version): update to version '$RELEASE_VERSION'"
git push
git tag -a "v$RELEASE_VERSION" -m"v$RELEASE_VERSION"
git push origin "v$RELEASE_VERSION"

View File

@@ -40,6 +40,7 @@
#plugin-jdbc:io.kestra.plugin:plugin-jdbc-db2:LATEST
#plugin-jdbc:io.kestra.plugin:plugin-jdbc-duckdb:LATEST
#plugin-jdbc:io.kestra.plugin:plugin-jdbc-druid:LATEST
#plugin-jdbc:io.kestra.plugin:plugin-jdbc-mariadb:LATEST
#plugin-jdbc:io.kestra.plugin:plugin-jdbc-mysql:LATEST
#plugin-jdbc:io.kestra.plugin:plugin-jdbc-oracle:LATEST
#plugin-jdbc:io.kestra.plugin:plugin-jdbc-pinot:LATEST

View File

@@ -7,6 +7,7 @@ import io.kestra.core.models.validations.ModelValidator;
import io.kestra.core.repositories.FlowRepositoryInterface;
import io.kestra.core.serializers.YamlParser;
import io.kestra.core.services.FlowListenersInterface;
import io.kestra.core.services.PluginDefaultService;
import io.micronaut.context.annotation.Requires;
import io.micronaut.context.annotation.Value;
import io.micronaut.scheduling.io.watch.FileWatchConfiguration;
@@ -36,6 +37,9 @@ public class FileChangedEventListener {
@Inject
private FlowRepositoryInterface flowRepositoryInterface;
@Inject
private PluginDefaultService pluginDefaultService;
@Inject
private YamlParser yamlParser;
@@ -64,7 +68,7 @@ public class FileChangedEventListener {
public void startListeningFromConfig() throws IOException, InterruptedException {
if (fileWatchConfiguration != null && fileWatchConfiguration.isEnabled()) {
this.flowFilesManager = new LocalFlowFileWatcher(flowRepositoryInterface);
this.flowFilesManager = new LocalFlowFileWatcher(flowRepositoryInterface, pluginDefaultService);
List<Path> paths = fileWatchConfiguration.getPaths();
this.setup(paths);
@@ -107,7 +111,6 @@ public class FileChangedEventListener {
} else {
log.info("File watching is disabled.");
}
}
public void startListening(List<Path> paths) throws IOException, InterruptedException {
@@ -118,60 +121,64 @@ public class FileChangedEventListener {
WatchKey key;
while ((key = watchService.take()) != null) {
for (WatchEvent<?> watchEvent : key.pollEvents()) {
WatchEvent.Kind<?> kind = watchEvent.kind();
Path entry = (Path) watchEvent.context();
try {
WatchEvent.Kind<?> kind = watchEvent.kind();
Path entry = (Path) watchEvent.context();
if (entry.toString().endsWith(".yml") || entry.toString().endsWith(".yaml")) {
if (entry.toString().endsWith(".yml") || entry.toString().endsWith(".yaml")) {
if (kind == StandardWatchEventKinds.ENTRY_CREATE || kind == StandardWatchEventKinds.ENTRY_MODIFY) {
if (kind == StandardWatchEventKinds.ENTRY_CREATE || kind == StandardWatchEventKinds.ENTRY_MODIFY) {
Path filePath = ((Path) key.watchable()).resolve(entry);
if (Files.isDirectory(filePath)) {
loadFlowsFromFolder(filePath);
} else {
Path filePath = ((Path) key.watchable()).resolve(entry);
if (Files.isDirectory(filePath)) {
loadFlowsFromFolder(filePath);
} else {
try {
String content = Files.readString(filePath, Charset.defaultCharset());
try {
String content = Files.readString(filePath, Charset.defaultCharset());
Optional<Flow> flow = parseFlow(content, entry);
if (flow.isPresent()) {
if (kind == StandardWatchEventKinds.ENTRY_MODIFY) {
// Check if we already have a file with the given path
if (flows.stream().anyMatch(flowWithPath -> flowWithPath.getPath().equals(filePath.toString()))) {
Optional<FlowWithPath> previous = flows.stream().filter(flowWithPath -> flowWithPath.getPath().equals(filePath.toString())).findFirst();
// Check if Flow from file has id/namespace updated
if (previous.isPresent() && !previous.get().uidWithoutRevision().equals(flow.get().uidWithoutRevision())) {
flows.removeIf(flowWithPath -> flowWithPath.getPath().equals(filePath.toString()));
flowFilesManager.deleteFlow(previous.get().getTenantId(), previous.get().getNamespace(), previous.get().getId());
Optional<Flow> flow = parseFlow(content, entry);
if (flow.isPresent()) {
if (kind == StandardWatchEventKinds.ENTRY_MODIFY) {
// Check if we already have a file with the given path
if (flows.stream().anyMatch(flowWithPath -> flowWithPath.getPath().equals(filePath.toString()))) {
Optional<FlowWithPath> previous = flows.stream().filter(flowWithPath -> flowWithPath.getPath().equals(filePath.toString())).findFirst();
// Check if Flow from file has id/namespace updated
if (previous.isPresent() && !previous.get().uidWithoutRevision().equals(flow.get().uidWithoutRevision())) {
flows.removeIf(flowWithPath -> flowWithPath.getPath().equals(filePath.toString()));
flowFilesManager.deleteFlow(previous.get().getTenantId(), previous.get().getNamespace(), previous.get().getId());
flows.add(FlowWithPath.of(flow.get(), filePath.toString()));
}
} else {
flows.add(FlowWithPath.of(flow.get(), filePath.toString()));
}
} else {
flows.add(FlowWithPath.of(flow.get(), filePath.toString()));
}
} else {
flows.add(FlowWithPath.of(flow.get(), filePath.toString()));
flowFilesManager.createOrUpdateFlow(flow.get(), content);
log.info("Flow {} from file {} has been created or modified", flow.get().getId(), entry);
}
flowFilesManager.createOrUpdateFlow(flow.get(), content);
log.info("Flow {} from file {} has been created or modified", flow.get().getId(), entry);
} catch (NoSuchFileException e) {
log.error("File not found: {}", entry, e);
} catch (IOException e) {
log.error("Error reading file: {}", entry, e);
}
} catch (NoSuchFileException e) {
log.error("File not found: {}", entry, e);
} catch (IOException e) {
log.error("Error reading file: {}", entry, e);
}
} else {
Path filePath = ((Path) key.watchable()).resolve(entry);
flows.stream()
.filter(flow -> flow.getPath().equals(filePath.toString()))
.findFirst()
.ifPresent(flowWithPath -> {
flowFilesManager.deleteFlow(flowWithPath.getTenantId(), flowWithPath.getNamespace(), flowWithPath.getId());
this.flows.removeIf(fwp -> fwp.uidWithoutRevision().equals(flowWithPath.uidWithoutRevision()));
});
}
} else {
Path filePath = ((Path) key.watchable()).resolve(entry);
flows.stream()
.filter(flow -> flow.getPath().equals(filePath.toString()))
.findFirst()
.ifPresent(flowWithPath -> {
flowFilesManager.deleteFlow(flowWithPath.getTenantId(), flowWithPath.getNamespace(), flowWithPath.getId());
this.flows.removeIf(fwp -> fwp.uidWithoutRevision().equals(flowWithPath.uidWithoutRevision()));
});
}
} catch (Exception e) {
log.error("Unexpected error while watching flows", e);
}
}
key.reset();
@@ -230,7 +237,8 @@ public class FileChangedEventListener {
private Optional<Flow> parseFlow(String content, Path entry) {
try {
Flow flow = yamlParser.parse(content, Flow.class);
modelValidator.validate(flow);
FlowWithSource withPluginDefault = pluginDefaultService.injectDefaults(FlowWithSource.of(flow, content));
modelValidator.validate(withPluginDefault);
return Optional.of(flow);
} catch (ConstraintViolationException e) {
log.warn("Error while parsing flow: {}", entry, e);

View File

@@ -3,32 +3,36 @@ package io.kestra.cli.services;
import io.kestra.core.models.flows.Flow;
import io.kestra.core.models.flows.FlowWithSource;
import io.kestra.core.repositories.FlowRepositoryInterface;
import io.micronaut.context.annotation.Requires;
import io.kestra.core.services.PluginDefaultService;
import lombok.extern.slf4j.Slf4j;
@Requires(property = "micronaut.io.watch.enabled", value = "true")
@Slf4j
public class LocalFlowFileWatcher implements FlowFilesManager {
private FlowRepositoryInterface flowRepositoryInterface;
private final FlowRepositoryInterface flowRepository;
private final PluginDefaultService pluginDefaultService;
public LocalFlowFileWatcher(FlowRepositoryInterface flowRepositoryInterface) {
this.flowRepositoryInterface = flowRepositoryInterface;
public LocalFlowFileWatcher(FlowRepositoryInterface flowRepository, PluginDefaultService pluginDefaultService) {
this.flowRepository = flowRepository;
this.pluginDefaultService = pluginDefaultService;
}
@Override
public FlowWithSource createOrUpdateFlow(Flow flow, String content) {
return flowRepositoryInterface.findById(null, flow.getNamespace(), flow.getId())
.map(previous -> flowRepositoryInterface.update(flow, previous, content, flow))
.orElseGet(() -> flowRepositoryInterface.create(flow, content, flow));
FlowWithSource withDefault = pluginDefaultService.injectDefaults(FlowWithSource.of(flow, content));
return flowRepository.findById(null, flow.getNamespace(), flow.getId())
.map(previous -> flowRepository.update(flow, previous, content, withDefault))
.orElseGet(() -> flowRepository.create(flow, content, withDefault));
}
@Override
public void deleteFlow(FlowWithSource toDelete) {
flowRepositoryInterface.findByIdWithSource(toDelete.getTenantId(), toDelete.getNamespace(), toDelete.getId()).ifPresent(flowRepositoryInterface::delete);
log.error("Flow {} has been deleted", toDelete.getId());
flowRepository.findByIdWithSource(toDelete.getTenantId(), toDelete.getNamespace(), toDelete.getId()).ifPresent(flowRepository::delete);
log.info("Flow {} has been deleted", toDelete.getId());
}
@Override
public void deleteFlow(String tenantId, String namespace, String id) {
flowRepositoryInterface.findByIdWithSource(tenantId, namespace, id).ifPresent(flowRepositoryInterface::delete);
log.error("Flow {} has been deleted", id);
flowRepository.findByIdWithSource(tenantId, namespace, id).ifPresent(flowRepository::delete);
log.info("Flow {} has been deleted", id);
}
}

View File

@@ -0,0 +1,131 @@
package io.kestra.cli.services;
import io.kestra.core.models.flows.Flow;
import io.kestra.core.repositories.FlowRepositoryInterface;
import io.kestra.core.utils.Await;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import jakarta.inject.Inject;
import org.apache.commons.io.FileUtils;
import org.junit.jupiter.api.*;
import org.junitpioneer.jupiter.RetryingTest;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import static io.kestra.core.utils.Rethrow.throwRunnable;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
@MicronautTest(environments = {"test", "file-watch"}, transactional = false)
class FileChangedEventListenerTest {
public static final String FILE_WATCH = "build/file-watch";
@Inject
private FileChangedEventListener fileWatcher;
@Inject
private FlowRepositoryInterface flowRepository;
private final ExecutorService executorService = Executors.newSingleThreadExecutor();
private final AtomicBoolean started = new AtomicBoolean(false);
@BeforeAll
static void setup() throws IOException {
if (!Files.exists(Path.of(FILE_WATCH))) {
Files.createDirectories(Path.of(FILE_WATCH));
}
}
@AfterAll
static void tearDown() throws IOException {
if (Files.exists(Path.of(FILE_WATCH))) {
FileUtils.deleteDirectory(Path.of(FILE_WATCH).toFile());
}
}
@BeforeEach
void beforeEach() throws Exception {
if (started.compareAndSet(false, true)) {
executorService.execute(throwRunnable(() -> fileWatcher.startListeningFromConfig()));
}
}
@RetryingTest(5) // Flaky on CI but always pass locally
void test() throws IOException, TimeoutException {
// remove the flow if it already exists
flowRepository.findByIdWithSource(null, "io.kestra.tests.watch", "myflow").ifPresent(flow -> flowRepository.delete(flow));
// create a basic flow
String flow = """
id: myflow
namespace: io.kestra.tests.watch
tasks:
- id: hello
type: io.kestra.plugin.core.log.Log
message: Hello World! 🚀
""";
Files.write(Path.of(FILE_WATCH + "/myflow.yaml"), flow.getBytes());
Await.until(
() -> flowRepository.findById(null, "io.kestra.tests.watch", "myflow").isPresent(),
Duration.ofMillis(100),
Duration.ofSeconds(10)
);
Flow myflow = flowRepository.findById(null, "io.kestra.tests.watch", "myflow").orElseThrow();
assertThat(myflow.getTasks(), hasSize(1));
assertThat(myflow.getTasks().getFirst().getId(), is("hello"));
assertThat(myflow.getTasks().getFirst().getType(), is("io.kestra.plugin.core.log.Log"));
// delete the flow
Files.delete(Path.of(FILE_WATCH + "/myflow.yaml"));
Await.until(
() -> flowRepository.findById(null, "io.kestra.tests.watch", "myflow").isEmpty(),
Duration.ofMillis(100),
Duration.ofSeconds(10)
);
}
@RetryingTest(5) // Flaky on CI but always pass locally
void testWithPluginDefault() throws IOException, TimeoutException {
// remove the flow if it already exists
flowRepository.findByIdWithSource(null, "io.kestra.tests.watch", "pluginDefault").ifPresent(flow -> flowRepository.delete(flow));
// create a flow with plugin default
String pluginDefault = """
id: pluginDefault
namespace: io.kestra.tests.watch
tasks:
- id: helloWithDefault
type: io.kestra.plugin.core.log.Log
pluginDefaults:
- type: io.kestra.plugin.core.log.Log
values:
message: Hello World!
""";
Files.write(Path.of(FILE_WATCH + "/plugin-default.yaml"), pluginDefault.getBytes());
Await.until(
() -> flowRepository.findById(null, "io.kestra.tests.watch", "pluginDefault").isPresent(),
Duration.ofMillis(100),
Duration.ofSeconds(10)
);
Flow pluginDefaultFlow = flowRepository.findById(null, "io.kestra.tests.watch", "pluginDefault").orElseThrow();
assertThat(pluginDefaultFlow.getTasks(), hasSize(1));
assertThat(pluginDefaultFlow.getTasks().getFirst().getId(), is("helloWithDefault"));
assertThat(pluginDefaultFlow.getTasks().getFirst().getType(), is("io.kestra.plugin.core.log.Log"));
// delete both files
Files.delete(Path.of(FILE_WATCH + "/plugin-default.yaml"));
Await.until(
() -> flowRepository.findById(null, "io.kestra.tests.watch", "pluginDefault").isEmpty(),
Duration.ofMillis(100),
Duration.ofSeconds(10)
);
}
}

View File

@@ -0,0 +1,12 @@
micronaut:
io:
watch:
enabled: true
paths:
- build/file-watch
kestra:
repository:
type: memory
queue:
type: memory

View File

@@ -82,6 +82,7 @@ public class JsonSchemaGenerator {
}
replaceAnyOfWithOneOf(objectNode);
pullOfDefaultFromOneOf(objectNode);
removeRequiredOnPropsWithDefaults(objectNode);
return JacksonMapper.toMap(objectNode);
} catch (IllegalArgumentException e) {
@@ -89,6 +90,27 @@ public class JsonSchemaGenerator {
}
}
private void removeRequiredOnPropsWithDefaults(ObjectNode objectNode) {
objectNode.findParents("required").forEach(jsonNode -> {
if (jsonNode instanceof ObjectNode clazzSchema && clazzSchema.get("required") instanceof ArrayNode requiredPropsNode && clazzSchema.get("properties") instanceof ObjectNode properties) {
List<String> requiredFieldValues = StreamSupport.stream(requiredPropsNode.spliterator(), false)
.map(JsonNode::asText)
.toList();
properties.fields().forEachRemaining(e -> {
int indexInRequiredArray = requiredFieldValues.indexOf(e.getKey());
if (indexInRequiredArray != -1 && e.getValue() instanceof ObjectNode valueNode && valueNode.has("default")) {
requiredPropsNode.remove(indexInRequiredArray);
}
});
if (requiredPropsNode.isEmpty()) {
clazzSchema.remove("required");
}
}
});
}
private void replaceAnyOfWithOneOf(ObjectNode objectNode) {
objectNode.findParents("anyOf").forEach(jsonNode -> {
if (jsonNode instanceof ObjectNode oNode) {
@@ -311,10 +333,12 @@ public class JsonSchemaGenerator {
if (member.getDeclaredType().isInstanceOf(Property.class)) {
memberAttributes.put("$dynamic", true);
// if we are in the String definition of a Property but the target type is not String: we configure the pattern
Class<?> targetType = member.getDeclaredType().getTypeParameters().getFirst().getErasedType();
if (!String.class.isAssignableFrom(targetType) && String.class.isAssignableFrom(member.getType().getErasedType())) {
memberAttributes.put("pattern", ".*{{.*}}.*");
}
// TODO this was a good idea but their is too much cases where it didn't work like in List or Map so if we want it we need to make it more clever
// I keep it for now commented but at some point we may want to re-do and improve it or remove these commented lines
// Class<?> targetType = member.getDeclaredType().getTypeParameters().getFirst().getErasedType();
// if (!String.class.isAssignableFrom(targetType) && String.class.isAssignableFrom(member.getType().getErasedType())) {
// memberAttributes.put("pattern", ".*{{.*}}.*");
// }
} else if (member.getDeclaredType().isInstanceOf(Data.class)) {
memberAttributes.put("$dynamic", false);
}
@@ -603,6 +627,7 @@ public class JsonSchemaGenerator {
ObjectNode objectNode = generator.generateSchema(cls);
replaceAnyOfWithOneOf(objectNode);
pullOfDefaultFromOneOf(objectNode);
removeRequiredOnPropsWithDefaults(objectNode);
return JacksonMapper.toMap(extractMainRef(objectNode));
} catch (IllegalArgumentException e) {

View File

@@ -40,6 +40,10 @@ public class Plugin {
private String subGroup;
public static Plugin of(RegisteredPlugin registeredPlugin, @Nullable String subgroup) {
return Plugin.of(registeredPlugin, subgroup, true);
}
public static Plugin of(RegisteredPlugin registeredPlugin, @Nullable String subgroup, boolean includeDeprecated) {
Plugin plugin = new Plugin();
plugin.name = registeredPlugin.name();
PluginSubGroup subGroupInfos = null;
@@ -80,17 +84,17 @@ public class Plugin {
plugin.subGroup = subgroup;
plugin.tasks = filterAndGetClassName(registeredPlugin.getTasks()).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
plugin.triggers = filterAndGetClassName(registeredPlugin.getTriggers()).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
plugin.conditions = filterAndGetClassName(registeredPlugin.getConditions()).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
plugin.storages = filterAndGetClassName(registeredPlugin.getStorages()).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
plugin.secrets = filterAndGetClassName(registeredPlugin.getSecrets()).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
plugin.taskRunners = filterAndGetClassName(registeredPlugin.getTaskRunners()).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
plugin.apps = filterAndGetClassName(registeredPlugin.getApps()).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
plugin.appBlocks = filterAndGetClassName(registeredPlugin.getAppBlocks()).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
plugin.charts = filterAndGetClassName(registeredPlugin.getCharts()).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
plugin.dataFilters = filterAndGetClassName(registeredPlugin.getDataFilters()).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
plugin.logExporters = filterAndGetClassName(registeredPlugin.getLogExporters()).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
plugin.tasks = filterAndGetClassName(registeredPlugin.getTasks(), includeDeprecated).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
plugin.triggers = filterAndGetClassName(registeredPlugin.getTriggers(), includeDeprecated).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
plugin.conditions = filterAndGetClassName(registeredPlugin.getConditions(), includeDeprecated).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
plugin.storages = filterAndGetClassName(registeredPlugin.getStorages(), includeDeprecated).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
plugin.secrets = filterAndGetClassName(registeredPlugin.getSecrets(), includeDeprecated).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
plugin.taskRunners = filterAndGetClassName(registeredPlugin.getTaskRunners(), includeDeprecated).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
plugin.apps = filterAndGetClassName(registeredPlugin.getApps(), includeDeprecated).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
plugin.appBlocks = filterAndGetClassName(registeredPlugin.getAppBlocks(), includeDeprecated).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
plugin.charts = filterAndGetClassName(registeredPlugin.getCharts(), includeDeprecated).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
plugin.dataFilters = filterAndGetClassName(registeredPlugin.getDataFilters(), includeDeprecated).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
plugin.logExporters = filterAndGetClassName(registeredPlugin.getLogExporters(), includeDeprecated).stream().filter(c -> subgroup == null || c.startsWith(subgroup)).toList();
return plugin;
}
@@ -100,12 +104,14 @@ public class Plugin {
* Those classes are only filtered from the documentation to ensure backward compatibility.
*
* @param list The list of classes?
* @param includeDeprecated whether to include deprecated plugins or not
* @return a filtered streams.
*/
private static List<String> filterAndGetClassName(final List<? extends Class<?>> list) {
private static List<String> filterAndGetClassName(final List<? extends Class<?>> list, boolean includeDeprecated) {
return list
.stream()
.filter(not(io.kestra.core.models.Plugin::isInternal))
.filter(p -> includeDeprecated || !io.kestra.core.models.Plugin.isDeprecated(p))
.map(Class::getName)
.filter(c -> !c.startsWith("org.kestra."))
.toList();

View File

@@ -95,7 +95,7 @@ public class HttpClient implements Closeable {
}
// proxy
if (this.configuration.getProxy() != null) {
if (this.configuration.getProxy() != null && configuration.getProxy().getAddress() != null) {
SocketAddress proxyAddr = new InetSocketAddress(
runContext.render(configuration.getProxy().getAddress()).as(String.class).orElse(null),
runContext.render(configuration.getProxy().getPort()).as(Integer.class).orElse(null)

View File

@@ -5,6 +5,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo;
import io.kestra.core.exceptions.IllegalVariableEvaluationException;
import io.kestra.core.models.property.Property;
import io.kestra.core.runners.RunContext;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
@@ -14,6 +15,7 @@ import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
@JsonSubTypes.Type(value = BearerAuthConfiguration.class, name = "BEARER")
})
@SuperBuilder(toBuilder = true)
@NoArgsConstructor
public abstract class AbstractAuthConfiguration {
public abstract Property<AuthType> getType();

View File

@@ -6,8 +6,7 @@ import io.kestra.core.models.property.Property;
import io.kestra.core.runners.RunContext;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Builder;
import lombok.Getter;
import lombok.*;
import lombok.experimental.SuperBuilder;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.apache.hc.core5.http.HttpHeaders;
@@ -16,8 +15,9 @@ import org.apache.hc.core5.http.message.BasicHeader;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
@Getter
@SuperBuilder(toBuilder = true)
@Getter
@NoArgsConstructor
public class BasicAuthConfiguration extends AbstractAuthConfiguration {
@NotNull
@JsonInclude
@@ -25,10 +25,10 @@ public class BasicAuthConfiguration extends AbstractAuthConfiguration {
protected Property<AuthType> type = Property.of(AuthType.BASIC);
@Schema(title = "The username for HTTP basic authentication.")
private final Property<String> username;
private Property<String> username;
@Schema(title = "The password for HTTP basic authentication.")
private final Property<String> password;
private Property<String> password;
@Override
public void configure(HttpClientBuilder builder, RunContext runContext) throws IllegalVariableEvaluationException {

View File

@@ -8,13 +8,15 @@ import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.message.BasicHeader;
@Getter
@SuperBuilder(toBuilder = true)
@Getter
@NoArgsConstructor
public class BearerAuthConfiguration extends AbstractAuthConfiguration {
@NotNull
@JsonInclude
@@ -22,7 +24,7 @@ public class BearerAuthConfiguration extends AbstractAuthConfiguration {
protected Property<AuthType> type = Property.of(AuthType.BEARER);
@Schema(title = "The token for bearer token authentication.")
private final Property<String> token;
private Property<String> token;
@Override
public void configure(HttpClientBuilder builder, RunContext runContext) throws IllegalVariableEvaluationException {

View File

@@ -7,6 +7,7 @@ import io.micronaut.logging.LogLevel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Getter;
import lombok.extern.jackson.Jacksonized;
import java.net.Proxy;
import java.nio.charset.Charset;
@@ -16,6 +17,7 @@ import java.time.temporal.ChronoUnit;
@Builder(toBuilder = true)
@Getter
@Jacksonized
public class HttpConfiguration {
@Schema(title = "The timeout configuration.")
@PluginProperty
@@ -55,261 +57,217 @@ public class HttpConfiguration {
}
// Deprecated properties
/**
* @deprecated
*/
@Schema(title = "The time allowed to establish a connection to the server before failing.")
@Deprecated
private final Property<Duration> connectTimeout;
private final Duration connectTimeout;
/**
* @deprecated
*/
@Deprecated
public void setConnectTimeout(Property<Duration> connectTimeout) {
if (this.timeout == null) {
this.timeout = TimeoutConfiguration.builder()
.build();
}
this.timeout = this.timeout.toBuilder()
.connectTimeout(connectTimeout)
.build();
}
/**
* @deprecated
*/
@Schema(title = "The maximum time allowed for reading data from the server before failing.")
@Builder.Default
@Deprecated
private final Property<Duration> readTimeout = Property.of(Duration.ofSeconds(HttpClientConfiguration.DEFAULT_READ_TIMEOUT_SECONDS));
private final Duration readTimeout = Duration.ofSeconds(HttpClientConfiguration.DEFAULT_READ_TIMEOUT_SECONDS);
/**
* @deprecated
*/
@Deprecated
public void setReadTimeout(Property<Duration> readTimeout) {
if (this.timeout == null) {
this.timeout = TimeoutConfiguration.builder()
.build();
}
this.timeout = this.timeout.toBuilder()
.readIdleTimeout(readTimeout)
.build();
}
/**
* @deprecated
*/
@Schema(title = "The type of proxy to use.")
@Builder.Default
@Deprecated
private final Property<Proxy.Type> proxyType = Property.of(Proxy.Type.DIRECT);
private final Proxy.Type proxyType = Proxy.Type.DIRECT;
/**
* @deprecated
*/
@Deprecated
public void setProxyType(Property<Proxy.Type> proxyType) {
if (this.proxy == null) {
this.proxy = ProxyConfiguration.builder()
.build();
}
this.proxy = this.proxy.toBuilder()
.type(proxyType)
.build();
}
/**
* @deprecated
*/
@Schema(title = "The address of the proxy server.")
@Deprecated
private final Property<String> proxyAddress;
private final String proxyAddress;
/**
* @deprecated
*/
@Deprecated
public void setProxyAddress(Property<String> proxyAddress) {
if (this.proxy == null) {
this.proxy = ProxyConfiguration.builder()
.build();
}
this.proxy = this.proxy.toBuilder()
.address(proxyAddress)
.build();
}
/**
* @deprecated
*/
@Schema(title = "The port of the proxy server.")
@Deprecated
private final Property<Integer> proxyPort;
private final Integer proxyPort;
/**
* @deprecated
*/
@Deprecated
public void setProxyPort(Property<Integer> proxyPort) {
if (this.proxy == null) {
this.proxy = ProxyConfiguration.builder()
.build();
}
this.proxy = this.proxy.toBuilder()
.port(proxyPort)
.build();
}
/**
* @deprecated
*/
@Schema(title = "The username for proxy authentication.")
@Deprecated
private final Property<String> proxyUsername;
private final String proxyUsername;
/**
* @deprecated
*/
@Deprecated
public void setProxyUsername(Property<String> proxyUsername) {
if (this.proxy == null) {
this.proxy = ProxyConfiguration.builder()
.build();
}
this.proxy = this.proxy.toBuilder()
.username(proxyUsername)
.build();
}
/**
* @deprecated
*/
@Schema(title = "The password for proxy authentication.")
@Deprecated
private final Property<String> proxyPassword;
private final String proxyPassword;
/**
* @deprecated
*/
@Deprecated
public void setProxyPassword(Property<String> proxyPassword) {
if (this.proxy == null) {
this.proxy = ProxyConfiguration.builder()
.build();
}
this.proxy = this.proxy.toBuilder()
.password(proxyPassword)
.build();
}
/**
* @deprecated
*/
@Schema(title = "The username for HTTP basic authentication.")
@Deprecated
private final Property<String> basicAuthUser;
private final String basicAuthUser;
/**
* @deprecated
*/
@Deprecated
public void setBasicAuthUser(Property<String> basicAuthUser) {
if (this.auth == null || !(this.auth instanceof BasicAuthConfiguration)) {
this.auth = BasicAuthConfiguration.builder()
.build();
}
this.auth = ((BasicAuthConfiguration) this.auth).toBuilder()
.username(basicAuthUser)
.build();
}
/**
* @deprecated
*/
@Schema(title = "The password for HTTP basic authentication.")
@Deprecated
private final Property<String> basicAuthPassword;
private final String basicAuthPassword;
/**
* @deprecated
*/
@Deprecated
private void setBasicAuthPassword(Property<String> basicAuthPassword) {
if (this.auth == null || !(this.auth instanceof BasicAuthConfiguration)) {
this.auth = BasicAuthConfiguration.builder()
.build();
}
this.auth = ((BasicAuthConfiguration) this.auth).toBuilder()
.password(basicAuthPassword)
.build();
}
/**
* @deprecated
*/
@Schema(title = "The log level for the HTTP client.")
@PluginProperty
@Deprecated
private final LogLevel logLevel;
/**
* @deprecated
*/
@Deprecated
private void setLogLevel(LogLevel logLevel) {
if (logLevel == LogLevel.TRACE) {
this.logs = new LoggingType[]{
LoggingType.REQUEST_HEADERS,
LoggingType.REQUEST_BODY,
LoggingType.RESPONSE_HEADERS,
LoggingType.RESPONSE_BODY
};
} else if (logLevel == LogLevel.DEBUG) {
this.logs = new LoggingType[]{
LoggingType.REQUEST_HEADERS,
LoggingType.RESPONSE_HEADERS,
};
} else if (logLevel == LogLevel.INFO) {
this.logs = new LoggingType[]{
LoggingType.RESPONSE_HEADERS,
};
}
}
// Deprecated properties with no real value to be kept, silently ignore
/**
* @deprecated
*/
// Deprecated properties with no equivalent value to be kept, silently ignore
@Schema(title = "The time allowed for a read connection to remain idle before closing it.")
@Builder.Default
@Deprecated
private final Property<Duration> readIdleTimeout = Property.of(Duration.of(HttpClientConfiguration.DEFAULT_READ_IDLE_TIMEOUT_MINUTES, ChronoUnit.MINUTES));
private final Duration readIdleTimeout = Duration.of(HttpClientConfiguration.DEFAULT_READ_IDLE_TIMEOUT_MINUTES, ChronoUnit.MINUTES);
/**
* @deprecated
*/
@Schema(title = "The time an idle connection can remain in the client's connection pool before being closed.")
@Builder.Default
@Deprecated
private final Property<Duration> connectionPoolIdleTimeout = Property.of(Duration.ofSeconds(HttpClientConfiguration.DEFAULT_CONNECTION_POOL_IDLE_TIMEOUT_SECONDS));
private final Duration connectionPoolIdleTimeout = Duration.ofSeconds(HttpClientConfiguration.DEFAULT_CONNECTION_POOL_IDLE_TIMEOUT_SECONDS);
/**
* @deprecated
*/
@Schema(title = "The maximum content length of the response.")
@Builder.Default
@Deprecated
private final Property<Integer> maxContentLength = Property.of(HttpClientConfiguration.DEFAULT_MAX_CONTENT_LENGTH);
private final Integer maxContentLength = HttpClientConfiguration.DEFAULT_MAX_CONTENT_LENGTH;
public static class HttpConfigurationBuilder {
@Deprecated
public HttpConfigurationBuilder connectTimeout(Duration connectTimeout) {
if (this.timeout == null) {
this.timeout = TimeoutConfiguration.builder()
.build();
}
this.timeout = this.timeout.toBuilder()
.connectTimeout(Property.of(connectTimeout))
.build();
return this;
}
@Deprecated
public HttpConfigurationBuilder readTimeout(Duration readTimeout) {
if (this.timeout == null) {
this.timeout = TimeoutConfiguration.builder()
.build();
}
this.timeout = this.timeout.toBuilder()
.readIdleTimeout(Property.of(readTimeout))
.build();
return this;
}
@Deprecated
public HttpConfigurationBuilder proxyType(Proxy.Type proxyType) {
if (this.proxy == null) {
this.proxy = ProxyConfiguration.builder()
.build();
}
this.proxy = this.proxy.toBuilder()
.type(Property.of(proxyType))
.build();
return this;
}
@Deprecated
public HttpConfigurationBuilder proxyAddress(String proxyAddress) {
if (this.proxy == null) {
this.proxy = ProxyConfiguration.builder()
.build();
}
this.proxy = this.proxy.toBuilder()
.address(Property.of(proxyAddress))
.build();
return this;
}
@Deprecated
public HttpConfigurationBuilder proxyPort(Integer proxyPort) {
if (this.proxy == null) {
this.proxy = ProxyConfiguration.builder()
.build();
}
this.proxy = this.proxy.toBuilder()
.port(Property.of(proxyPort))
.build();
return this;
}
@Deprecated
public HttpConfigurationBuilder proxyUsername(String proxyUsername) {
if (this.proxy == null) {
this.proxy = ProxyConfiguration.builder()
.build();
}
this.proxy = this.proxy.toBuilder()
.username(Property.of(proxyUsername))
.build();
return this;
}
@Deprecated
public HttpConfigurationBuilder proxyPassword(String proxyPassword) {
if (this.proxy == null) {
this.proxy = ProxyConfiguration.builder()
.build();
}
this.proxy = this.proxy.toBuilder()
.password(Property.of(proxyPassword))
.build();
return this;
}
@SuppressWarnings("DeprecatedIsStillUsed")
@Deprecated
public HttpConfigurationBuilder basicAuthUser(String basicAuthUser) {
if (this.auth == null || !(this.auth instanceof BasicAuthConfiguration)) {
this.auth = BasicAuthConfiguration.builder()
.build();
}
this.auth = ((BasicAuthConfiguration) this.auth).toBuilder()
.username(Property.of(basicAuthUser))
.build();
return this;
}
@SuppressWarnings("DeprecatedIsStillUsed")
@Deprecated
public HttpConfigurationBuilder basicAuthPassword(String basicAuthPassword) {
if (this.auth == null || !(this.auth instanceof BasicAuthConfiguration)) {
this.auth = BasicAuthConfiguration.builder()
.build();
}
this.auth = ((BasicAuthConfiguration) this.auth).toBuilder()
.password(Property.of(basicAuthPassword))
.build();
return this;
}
@Deprecated
public HttpConfigurationBuilder logLevel(LogLevel logLevel) {
if (logLevel == LogLevel.TRACE) {
this.logs = new LoggingType[]{
LoggingType.REQUEST_HEADERS,
LoggingType.REQUEST_BODY,
LoggingType.RESPONSE_HEADERS,
LoggingType.RESPONSE_BODY
};
} else if (logLevel == LogLevel.DEBUG) {
this.logs = new LoggingType[]{
LoggingType.REQUEST_HEADERS,
LoggingType.RESPONSE_HEADERS,
};
} else if (logLevel == LogLevel.INFO) {
this.logs = new LoggingType[]{
LoggingType.RESPONSE_HEADERS,
};
}
return this;
}
}
}

View File

@@ -21,6 +21,7 @@ import io.kestra.core.models.tasks.FlowableTask;
import io.kestra.core.models.tasks.Task;
import io.kestra.core.models.tasks.retrys.AbstractRetry;
import io.kestra.core.models.triggers.AbstractTrigger;
import io.kestra.core.models.triggers.Trigger;
import io.kestra.core.models.validations.ManualConstraintViolation;
import io.kestra.core.serializers.JacksonMapper;
import io.kestra.core.serializers.ListOrMapOfLabelDeserializer;
@@ -176,6 +177,14 @@ public class Flow extends AbstractFlow implements HasUID {
);
}
public static String uid(Trigger trigger) {
return IdUtils.fromParts(
trigger.getTenantId(),
trigger.getNamespace(),
trigger.getFlowId()
);
}
public static String uidWithoutRevision(Execution execution) {
return IdUtils.fromParts(
execution.getTenantId(),

View File

@@ -2,23 +2,26 @@ package io.kestra.core.models.tasks.runners;
import io.kestra.core.models.tasks.Output;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import javax.annotation.Nullable;
@AllArgsConstructor
@Getter
@Builder
@SuperBuilder
@NoArgsConstructor
public class TaskRunnerResult<T extends TaskRunnerDetailResult> implements Output {
private int exitCode;
private AbstractLogConsumer logConsumer;
@Nullable
private T details;
@SuppressWarnings("unchecked")
public TaskRunnerResult(int exitCode, AbstractLogConsumer logConsumer) {
this.exitCode = exitCode;
this.logConsumer = logConsumer;
this.details = (T) TaskRunnerDetailResult.builder().build();
}
}

View File

@@ -15,6 +15,7 @@ import io.kestra.core.repositories.ExecutionRepositoryInterface;
import io.kestra.core.services.ExecutionService;
import io.kestra.core.storages.Storage;
import io.kestra.core.trace.TracerFactory;
import io.kestra.core.utils.ListUtils;
import io.kestra.core.utils.MapUtils;
import io.kestra.core.trace.propagation.ExecutionTextMapSetter;
import io.opentelemetry.api.OpenTelemetry;
@@ -153,7 +154,7 @@ public final class ExecutableUtils {
throw new IllegalStateException("Cannot execute an invalid flow: " + fwe.getException());
}
List<Label> newLabels = inheritLabels ? new ArrayList<>(currentExecution.getLabels()) : new ArrayList<>(systemLabels(currentExecution));
List<Label> newLabels = inheritLabels ? new ArrayList<>(filterLabels(currentExecution.getLabels(), flow)) : new ArrayList<>(systemLabels(currentExecution));
if (labels != null) {
labels.forEach(throwConsumer(label -> newLabels.add(new Label(runContext.render(label.key()), runContext.render(label.value())))));
}
@@ -201,6 +202,16 @@ public final class ExecutableUtils {
}));
}
private static List<Label> filterLabels(List<Label> labels, Flow flow) {
if (ListUtils.isEmpty(flow.getLabels())) {
return labels;
}
return labels.stream()
.filter(label -> flow.getLabels().stream().noneMatch(flowLabel -> flowLabel.key().equals(label.key())))
.toList();
}
private static List<Label> systemLabels(Execution execution) {
return Streams.of(execution.getLabels())
.filter(label -> label.key().startsWith(Label.SYSTEM_PREFIX))

View File

@@ -69,7 +69,6 @@ import static io.kestra.core.utils.Rethrow.throwFunction;
public class FlowInputOutput {
private static final Pattern URI_PATTERN = Pattern.compile("^[a-z]+:\\/\\/(?:www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b(?:[-a-zA-Z0-9()@:%_\\+.~#?&\\/=]*)$");
private static final ObjectMapper YAML_MAPPER = JacksonMapper.ofYaml();
private static final ObjectMapper JSON_MAPPER = JacksonMapper.ofJson();
private final StorageInterface storageInterface;
private final Optional<String> secretKey;
@@ -95,11 +94,12 @@ public class FlowInputOutput {
* @return The list of {@link InputAndValue}.
*/
public Mono<List<InputAndValue>> validateExecutionInputs(final List<Input<?>> inputs,
final Execution execution,
final Publisher<CompletedPart> data) {
final Flow flow,
final Execution execution,
final Publisher<CompletedPart> data) {
if (ListUtils.isEmpty(inputs)) return Mono.just(Collections.emptyList());
return readData(inputs, execution, data, false).map(inputData -> resolveInputs(inputs, execution, inputData));
return readData(inputs, execution, data, false).map(inputData -> resolveInputs(inputs, flow, execution, inputData));
}
/**
@@ -111,9 +111,9 @@ public class FlowInputOutput {
* @return The Map of typed inputs.
*/
public Mono<Map<String, Object>> readExecutionInputs(final Flow flow,
final Execution execution,
final Publisher<CompletedPart> data) {
return this.readExecutionInputs(flow.getInputs(), execution, data);
final Execution execution,
final Publisher<CompletedPart> data) {
return this.readExecutionInputs(flow.getInputs(), flow, execution, data);
}
/**
@@ -125,9 +125,10 @@ public class FlowInputOutput {
* @return The Map of typed inputs.
*/
public Mono<Map<String, Object>> readExecutionInputs(final List<Input<?>> inputs,
final Flow flow,
final Execution execution,
final Publisher<CompletedPart> data) {
return readData(inputs, execution, data, true).map(inputData -> this.readExecutionInputs(inputs, execution, inputData));
return readData(inputs, execution, data, true).map(inputData -> this.readExecutionInputs(inputs, flow, execution, inputData));
}
private Mono<Map<String, Object>> readData(List<Input<?>> inputs, Execution execution, Publisher<CompletedPart> data, boolean uploadFiles) {
@@ -192,15 +193,16 @@ public class FlowInputOutput {
final Execution execution,
final Map<String, ?> data
) {
return readExecutionInputs(flow.getInputs(), execution, data);
return readExecutionInputs(flow.getInputs(), flow, execution, data);
}
private Map<String, Object> readExecutionInputs(
final List<Input<?>> inputs,
final Flow flow,
final Execution execution,
final Map<String, ?> data
) {
Map<String, Object> resolved = this.resolveInputs(inputs, execution, data)
Map<String, Object> resolved = this.resolveInputs(inputs, flow, execution, data)
.stream()
.filter(InputAndValue::enabled)
.map(it -> {
@@ -225,6 +227,7 @@ public class FlowInputOutput {
@VisibleForTesting
public List<InputAndValue> resolveInputs(
final List<Input<?>> inputs,
final Flow flow,
final Execution execution,
final Map<String, ?> data
) {
@@ -240,7 +243,7 @@ public class FlowInputOutput {
})
.collect(Collectors.toMap(it -> it.get().input().getId(), Function.identity(), (o1, o2) -> o1, LinkedHashMap::new)));
resolvableInputMap.values().forEach(input -> resolveInputValue(input, execution, resolvableInputMap));
resolvableInputMap.values().forEach(input -> resolveInputValue(input, flow, execution, resolvableInputMap));
return resolvableInputMap.values().stream().map(ResolvableInput::get).toList();
}
@@ -248,6 +251,7 @@ public class FlowInputOutput {
@SuppressWarnings({"unchecked", "rawtypes"})
private InputAndValue resolveInputValue(
final @NotNull ResolvableInput resolvable,
final Flow flow,
final @NotNull Execution execution,
final @NotNull Map<String, ResolvableInput> inputs) {
@@ -258,8 +262,8 @@ public class FlowInputOutput {
try {
// resolve all input dependencies and check whether input is enabled
final Map<String, InputAndValue> dependencies = resolveAllDependentInputs(input, execution, inputs);
final RunContext runContext = buildRunContextForExecutionAndInputs(execution, dependencies);
final Map<String, InputAndValue> dependencies = resolveAllDependentInputs(input, flow, execution, inputs);
final RunContext runContext = buildRunContextForExecutionAndInputs(flow, execution, dependencies);
boolean isInputEnabled = dependencies.isEmpty() || dependencies.values().stream().allMatch(InputAndValue::enabled);
@@ -325,15 +329,15 @@ public class FlowInputOutput {
return resolvable.get();
}
private RunContext buildRunContextForExecutionAndInputs(Execution execution, Map<String, InputAndValue> dependencies) {
private RunContext buildRunContextForExecutionAndInputs(final Flow flow, final Execution execution, Map<String, InputAndValue> dependencies) {
Map<String, Object> flattenInputs = MapUtils.flattenToNestedMap(dependencies.entrySet()
.stream()
.collect(HashMap::new, (m, v) -> m.put(v.getKey(), v.getValue().value()), HashMap::putAll)
);
return runContextFactory.of(null, execution, vars -> vars.withInputs(flattenInputs));
return runContextFactory.of(flow, execution, vars -> vars.withInputs(flattenInputs));
}
private Map<String, InputAndValue> resolveAllDependentInputs(final Input<?> input, final Execution execution, final Map<String, ResolvableInput> inputs) {
private Map<String, InputAndValue> resolveAllDependentInputs(final Input<?> input, final Flow flow, final Execution execution, final Map<String, ResolvableInput> inputs) {
return Optional.ofNullable(input.getDependsOn())
.map(DependsOn::inputs)
.stream()
@@ -341,7 +345,7 @@ public class FlowInputOutput {
.filter(id -> !id.equals(input.getId()))
.map(inputs::get)
.filter(Objects::nonNull) // input may declare unknown or non-necessary dependencies. Let's ignore.
.map(it -> resolveInputValue(it, execution, inputs))
.map(it -> resolveInputValue(it, flow, execution, inputs))
.collect(Collectors.toMap(it -> it.input().getId(), Function.identity()));
}

View File

@@ -30,7 +30,7 @@ import io.kestra.core.utils.Await;
import io.kestra.core.utils.Either;
import io.kestra.core.utils.IdUtils;
import io.kestra.core.utils.ListUtils;
import io.kestra.core.models.triggers.RecoverMissedSchedules;
import io.kestra.core.models.flows.Flow;
import io.micronaut.context.ApplicationContext;
import io.micronaut.context.event.ApplicationEventPublisher;
import io.micronaut.inject.qualifiers.Qualifiers;
@@ -70,7 +70,8 @@ public abstract class AbstractScheduler implements Scheduler, Service {
private final QueueInterface<WorkerJob> workerTaskQueue;
private final WorkerTriggerResultQueueInterface workerTriggerResultQueue;
private final QueueInterface<ExecutionKilled> executionKilledQueue;
@SuppressWarnings("rawtypes") private final Optional<QueueInterface> clusterEventQueue;
@SuppressWarnings("rawtypes")
private final Optional<QueueInterface> clusterEventQueue;
protected final FlowListenersInterface flowListeners;
private final RunContextFactory runContextFactory;
private final RunContextInitializer runContextInitializer;
@@ -408,6 +409,16 @@ public abstract class AbstractScheduler implements Scheduler, Service {
private List<FlowWithTriggers> computeSchedulable(List<FlowWithSource> flows, List<Trigger> triggerContextsToEvaluate, ScheduleContextInterface scheduleContext) {
List<String> flowToKeep = triggerContextsToEvaluate.stream().map(Trigger::getFlowId).toList();
triggerContextsToEvaluate.stream()
.filter(trigger -> !flows.stream().map(FlowWithSource::uidWithoutRevision).toList().contains(Flow.uid(trigger)))
.forEach(trigger -> {
try {
this.triggerState.delete(trigger);
} catch (QueueException e) {
log.error("Unable to delete the trigger: {}.{}.{}", trigger.getNamespace(), trigger.getFlowId(), trigger.getTriggerId(), e);
}
});
return flows
.stream()
.filter(flow -> flowToKeep.contains(flow.getId()))

View File

@@ -6,6 +6,7 @@ import io.kestra.core.models.flows.FlowWithSource;
import io.kestra.core.models.triggers.AbstractTrigger;
import io.kestra.core.models.triggers.Trigger;
import io.kestra.core.models.triggers.TriggerContext;
import io.kestra.core.queues.QueueException;
import jakarta.validation.ConstraintViolationException;
import java.time.ZonedDateTime;
@@ -25,7 +26,10 @@ public interface SchedulerTriggerStateInterface {
Trigger update(Flow flow, AbstractTrigger abstractTrigger, ConditionContext conditionContext) throws Exception;
/**
* QueueException required for Kafka implementation
*/
void delete(Trigger trigger) throws QueueException;
/**
* Used by the JDBC implementation: find triggers in all tenants.
*/

View File

@@ -486,7 +486,7 @@ public class ExecutionService {
return getFirstPausedTaskOr(execution, flow)
.flatMap(task -> {
if (task.isPresent() && task.get() instanceof Pause pauseTask) {
return Mono.just(flowInputOutput.resolveInputs(pauseTask.getOnResume(), execution, Map.of()));
return Mono.just(flowInputOutput.resolveInputs(pauseTask.getOnResume(), flow, execution, Map.of()));
} else {
return Mono.just(Collections.emptyList());
}
@@ -507,7 +507,7 @@ public class ExecutionService {
return getFirstPausedTaskOr(execution, flow)
.flatMap(task -> {
if (task.isPresent() && task.get() instanceof Pause pauseTask) {
return flowInputOutput.validateExecutionInputs(pauseTask.getOnResume(), execution, inputs);
return flowInputOutput.validateExecutionInputs(pauseTask.getOnResume(), flow, execution, inputs);
} else {
return Mono.just(Collections.emptyList());
}
@@ -528,7 +528,7 @@ public class ExecutionService {
return getFirstPausedTaskOr(execution, flow)
.flatMap(task -> {
if (task.isPresent() && task.get() instanceof Pause pauseTask) {
return flowInputOutput.readExecutionInputs(pauseTask.getOnResume(), execution, inputs);
return flowInputOutput.readExecutionInputs(pauseTask.getOnResume(), flow, execution, inputs);
} else {
return Mono.just(Collections.<String, Object>emptyMap());
}

View File

@@ -161,7 +161,7 @@ public class FlowService {
}
// check if subflow is present in given namespace
public void checkValidSubflows(Flow flow) {
public void checkValidSubflows(Flow flow, String tenantId) {
List<io.kestra.plugin.core.flow.Subflow> subFlows = ListUtils.emptyOnNull(flow.getTasks()).stream()
.filter(io.kestra.plugin.core.flow.Subflow.class::isInstance)
.map(io.kestra.plugin.core.flow.Subflow.class::cast)
@@ -170,15 +170,23 @@ public class FlowService {
Set<ConstraintViolation<?>> violations = new HashSet<>();
subFlows.forEach(subflow -> {
Optional<Flow> optional = findById(flow.getTenantId(), subflow.getNamespace(), subflow.getFlowId());
String regex = ".*\\{\\{.+}}.*"; // regex to check if string contains pebble
String subflowId = subflow.getFlowId();
String namespace = subflow.getNamespace();
if (subflowId.matches(regex) || namespace.matches(regex)) {
return;
}
Optional<Flow> optional = findById(tenantId, subflow.getNamespace(), subflow.getFlowId());
violations.add(ManualConstraintViolation.of(
"The subflow '" + subflow.getFlowId() + "' not found in namespace '" + subflow.getNamespace() + "'.",
flow,
Flow.class,
"flow.tasks",
flow.getNamespace()
));
if (optional.isEmpty()) {
violations.add(ManualConstraintViolation.of(
"The subflow '" + subflow.getFlowId() + "' not found in namespace '" + subflow.getNamespace() + "'.",
flow,
Flow.class,
"flow.tasks",
flow.getNamespace()
));
}
});
if (!violations.isEmpty()) {

View File

@@ -6,6 +6,7 @@ import io.kestra.core.models.flows.Flow;
import io.kestra.core.models.triggers.AbstractTrigger;
import io.kestra.core.runners.RunContext;
import io.kestra.core.utils.ListUtils;
import jakarta.annotation.Nullable;
import java.util.*;
@@ -54,9 +55,9 @@ public final class LabelService {
}
}
public static boolean containsAll(List<Label> labelsContainer, List<Label> labelsThatMustBeIncluded) {
Map<String, String> labelsContainerMap = labelsContainer.stream().collect(HashMap::new, (m, label)-> m.put(label.key(), label.value()), HashMap::putAll);
public static boolean containsAll(@Nullable List<Label> labelsContainer, @Nullable List<Label> labelsThatMustBeIncluded) {
Map<String, String> labelsContainerMap = ListUtils.emptyOnNull(labelsContainer).stream().collect(HashMap::new, (m, label)-> m.put(label.key(), label.value()), HashMap::putAll);
return labelsThatMustBeIncluded.stream().allMatch(label -> Objects.equals(labelsContainerMap.get(label.key()), label.value()));
return ListUtils.emptyOnNull(labelsThatMustBeIncluded).stream().allMatch(label -> Objects.equals(labelsContainerMap.get(label.key()), label.value()));
}
}

View File

@@ -37,8 +37,8 @@ import static io.kestra.core.utils.Rethrow.throwPredicate;
"- conditions:",
" - type: io.kestra.plugin.core.condition.Not",
" conditions:",
" - type: io.kestra.plugin.core.condition.DateBetween",
" after: \"2013-09-08T16:19:12\"",
" - type: io.kestra.plugin.core.condition.DateTimeBetween",
" after: \"2013-09-08T16:19:12Z\"",
}
)
},

View File

@@ -96,7 +96,7 @@ public class PurgeLogs extends Task implements RunnableTask<PurgeLogs.Output> {
flowService.checkAllowedNamespace(flowInfo.tenantId(), runContext.render(namespace).as(String.class).orElse(null), flowInfo.tenantId(), flowInfo.namespace());
}
var logLevelsRendered = runContext.render(this.logLevels).asList(String.class);
var logLevelsRendered = runContext.render(this.logLevels).asList(Level.class);
var renderedDate = runContext.render(startDate).as(String.class).orElse(null);
int deleted = logService.purge(
flowInfo.tenantId(),

View File

@@ -149,7 +149,7 @@ class ClassPluginDocumentationTest {
assertThat(oneOf.getFirst().get("type"), is("integer"));
assertThat(oneOf.getFirst().get("$dynamic"), is(true));
assertThat(oneOf.get(1).get("type"), is("string"));
assertThat(oneOf.get(1).get("pattern"), is(".*{{.*}}.*"));
// assertThat(oneOf.get(1).get("pattern"), is(".*{{.*}}.*"));
Map<String, Object> withDefault = (Map<String, Object>) properties.get("withDefault");
assertThat(withDefault.get("type"), is("string"));

View File

@@ -26,6 +26,7 @@ import io.kestra.plugin.core.flow.Dag;
import io.kestra.plugin.core.log.Log;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.inject.Inject;
import jakarta.validation.constraints.NotNull;
import lombok.*;
import lombok.experimental.SuperBuilder;
import org.hamcrest.Matchers;
@@ -238,6 +239,15 @@ class JsonSchemaGeneratorTest {
assertThat(((Map<String, Map<String, Object>>) generate.get("properties")).get("beta").get("$beta"), is(true));
}
@SuppressWarnings("unchecked")
@Test
void requiredAreRemovedIfThereIsADefault() {
Map<String, Object> generate = jsonSchemaGenerator.properties(Task.class, RequiredWithDefault.class);
assertThat(generate, is(not(nullValue())));
assertThat((List<String>) generate.get("required"), not(containsInAnyOrder("requiredWithDefault")));
assertThat((List<String>) generate.get("required"), containsInAnyOrder("requiredWithNoDefault"));
}
@SuppressWarnings("unchecked")
@Test
void dashboard() throws URISyntaxException {
@@ -324,6 +334,7 @@ class JsonSchemaGeneratorTest {
}
@Schema(title = "Test class")
@Builder
private static class TestClass {
@Schema(title = "Test property")
public String testProperty;
@@ -360,4 +371,21 @@ class JsonSchemaGeneratorTest {
return null;
}
}
@SuperBuilder
@ToString
@EqualsAndHashCode
@Getter
@NoArgsConstructor
@Plugin
public static class RequiredWithDefault extends Task {
@PluginProperty
@NotNull
@Builder.Default
private Property<TaskWithEnum.TestClass> requiredWithDefault = Property.of(TaskWithEnum.TestClass.builder().testProperty("test").build());
@PluginProperty
@NotNull
private Property<TaskWithEnum.TestClass> requiredWithNoDefault;
}
}

View File

@@ -65,7 +65,7 @@ class FlowInputOutputTest {
Map<String, Object> data = Map.of("input1", "value1", "input2", "value2");
// When
List<InputAndValue> values = flowInputOutput.resolveInputs(inputs, DEFAULT_TEST_EXECUTION, data);
List<InputAndValue> values = flowInputOutput.resolveInputs(inputs, null, DEFAULT_TEST_EXECUTION, data);
// Then
Assertions.assertEquals(
@@ -98,7 +98,7 @@ class FlowInputOutputTest {
Map<String, Object> data = Map.of("input1", "v1", "input2", "v2", "input3", "v3");
// When
List<InputAndValue> values = flowInputOutput.resolveInputs(inputs, DEFAULT_TEST_EXECUTION, data);
List<InputAndValue> values = flowInputOutput.resolveInputs(inputs, null, DEFAULT_TEST_EXECUTION, data);
// Then
Assertions.assertEquals(
@@ -132,7 +132,7 @@ class FlowInputOutputTest {
Map<String, Object> data = Map.of("input1", "v1", "input2", "v2", "input3", "v3");
// When
List<InputAndValue> values = flowInputOutput.resolveInputs(inputs, DEFAULT_TEST_EXECUTION, data);
List<InputAndValue> values = flowInputOutput.resolveInputs(inputs, null, DEFAULT_TEST_EXECUTION, data);
// Then
Assertions.assertEquals(
@@ -162,7 +162,7 @@ class FlowInputOutputTest {
Map<String, Object> data = Map.of("input1", "value1", "input2", "value2");
// When
List<InputAndValue> values = flowInputOutput.resolveInputs(inputs, DEFAULT_TEST_EXECUTION, data);
List<InputAndValue> values = flowInputOutput.resolveInputs(inputs, null, DEFAULT_TEST_EXECUTION, data);
// Then
Assertions.assertEquals(
@@ -191,7 +191,7 @@ class FlowInputOutputTest {
Map<String, Object> data = Map.of("input1", "value1", "input2", "value2");
// When
List<InputAndValue> values = flowInputOutput.resolveInputs(inputs, DEFAULT_TEST_EXECUTION, data);
List<InputAndValue> values = flowInputOutput.resolveInputs(inputs, null, DEFAULT_TEST_EXECUTION, data);
// Then
Assertions.assertEquals(2, values.size());
@@ -211,7 +211,7 @@ class FlowInputOutputTest {
Publisher<CompletedPart> data = Mono.just(new MemoryCompletedFileUpload("input", "input", "???".getBytes(StandardCharsets.UTF_8)));
// When
List<InputAndValue> values = flowInputOutput.validateExecutionInputs(List.of(input), DEFAULT_TEST_EXECUTION, data).block();
List<InputAndValue> values = flowInputOutput.validateExecutionInputs(List.of(input), null, DEFAULT_TEST_EXECUTION, data).block();
// Then
Assertions.assertNull(values.getFirst().exception());
@@ -238,7 +238,7 @@ class FlowInputOutputTest {
Map<String, Object> data = Map.of("input42", "foo");
// When
List<InputAndValue> values = flowInputOutput.resolveInputs(inputs, DEFAULT_TEST_EXECUTION, data);
List<InputAndValue> values = flowInputOutput.resolveInputs(inputs, null, DEFAULT_TEST_EXECUTION, data);
// Then
Assertions.assertEquals(

View File

@@ -22,6 +22,7 @@ import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThrows;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertTrue;
@KestraTest
@@ -323,7 +324,7 @@ class FlowServiceTest {
}
@Test
void checkValidSubflowsNotFound() {
void checkSubflowNotFound() {
Flow flow = create("mainFlow", "task", 1).toBuilder()
.tasks(List.of(
io.kestra.plugin.core.flow.Subflow.builder()
@@ -336,10 +337,29 @@ class FlowServiceTest {
.build();
ConstraintViolationException exception = assertThrows(ConstraintViolationException.class, () -> {
flowService.checkValidSubflows(flow);
flowService.checkValidSubflows(flow, null);
});
assertThat(exception.getConstraintViolations().size(), is(1));
assertThat(exception.getConstraintViolations().iterator().next().getMessage(), is("The subflow 'nonExistentSubflow' not found in namespace 'io.kestra.unittest'."));
}
@Test
void checkValidSubflow() {
Flow subflow = create("existingSubflow", "task", 1);
flowRepository.create(subflow, subflow.generateSource(), subflow);
Flow flow = create("mainFlow", "task", 1).toBuilder()
.tasks(List.of(
io.kestra.plugin.core.flow.Subflow.builder()
.id("subflowTask")
.type(io.kestra.plugin.core.flow.Subflow.class.getName())
.namespace("io.kestra.unittest")
.flowId("existingSubflow")
.build()
))
.build();
assertDoesNotThrow(() -> flowService.checkValidSubflows(flow, null));
}
}

View File

@@ -10,11 +10,14 @@ import io.kestra.plugin.core.trigger.Schedule;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@KestraTest
class LabelServiceTest {
@@ -65,4 +68,15 @@ class LabelServiceTest {
assertThat(labels, hasSize(2));
assertThat(labels, hasItems(new Label("key", "value"), new Label("scheduleLabel", "scheduleValue")));
}
@Test
void containsAll() {
assertFalse(LabelService.containsAll(null, List.of(new Label("key", "value"))));
assertFalse(LabelService.containsAll(Collections.emptyList(), List.of(new Label("key", "value"))));
assertFalse(LabelService.containsAll(List.of(new Label("key1", "value1")), List.of(new Label("key2", "value2"))));
assertTrue(LabelService.containsAll(List.of(new Label("key", "value")), null));
assertTrue(LabelService.containsAll(List.of(new Label("key", "value")), Collections.emptyList()));
assertTrue(LabelService.containsAll(List.of(new Label("key1", "value1")), List.of(new Label("key1", "value1"))));
assertTrue(LabelService.containsAll(List.of(new Label("key1", "value1"), new Label("key2", "value2")), List.of(new Label("key1", "value1"))));
}
}

View File

@@ -99,21 +99,24 @@ public class FlowCaseTest {
assertThat(triggered.get().getState().getCurrent(), is(triggerState));
if (testInherited) {
assertThat(triggered.get().getLabels().size(), is(5));
assertThat(triggered.get().getLabels().size(), is(6));
assertThat(triggered.get().getLabels(), hasItems(
new Label(Label.CORRELATION_ID, execution.getId()),
new Label("mainFlowExecutionLabel", "execFoo"),
new Label("mainFlowLabel", "flowFoo"),
new Label("launchTaskLabel", "launchFoo"),
new Label("switchFlowLabel", "switchFoo")
new Label("switchFlowLabel", "switchFoo"),
new Label("overriding", "child")
));
} else {
assertThat(triggered.get().getLabels().size(), is(3));
assertThat(triggered.get().getLabels().size(), is(4));
assertThat(triggered.get().getLabels(), hasItems(
new Label(Label.CORRELATION_ID, execution.getId()),
new Label("launchTaskLabel", "launchFoo"),
new Label("switchFlowLabel", "switchFoo")
new Label("switchFlowLabel", "switchFoo"),
new Label("overriding", "child")
));
assertThat(triggered.get().getLabels(), not(hasItems(new Label("inherited", "label"))));
}
}
}

View File

@@ -276,7 +276,7 @@ public class ForEachItemCaseTest {
}
public void restartForEachItem() throws Exception {
CountDownLatch countDownLatch = new CountDownLatch(26);
CountDownLatch countDownLatch = new CountDownLatch(6);
Flux<Execution> receiveSubflows = TestsUtils.receive(executionQueue, either -> {
Execution subflowExecution = either.getLeft();
if (subflowExecution.getFlowId().equals("restart-child") && subflowExecution.getState().getCurrent().isFailed()) {
@@ -285,7 +285,7 @@ public class ForEachItemCaseTest {
});
URI file = storageUpload();
Map<String, Object> inputs = Map.of("file", file.toString(), "batch", 4);
Map<String, Object> inputs = Map.of("file", file.toString(), "batch", 20);
Execution execution = runnerUtils.runOne(null, TEST_NAMESPACE, "restart-for-each-item", null,
(flow, execution1) -> flowIO.readExecutionInputs(flow, execution1, inputs),
Duration.ofSeconds(30));
@@ -296,7 +296,7 @@ public class ForEachItemCaseTest {
assertTrue(countDownLatch.await(1, TimeUnit.MINUTES));
receiveSubflows.blockLast();
CountDownLatch successLatch = new CountDownLatch(26);
CountDownLatch successLatch = new CountDownLatch(6);
receiveSubflows = TestsUtils.receive(executionQueue, either -> {
Execution subflowExecution = either.getLeft();
if (subflowExecution.getFlowId().equals("restart-child") && subflowExecution.getState().getCurrent().isSuccess()) {

View File

@@ -16,7 +16,7 @@ import io.kestra.core.utils.IdUtils;
import io.kestra.core.utils.TestsUtils;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import org.junit.jupiter.api.Test;
import org.junitpioneer.jupiter.RetryingTest;
import reactor.core.publisher.Flux;
import java.time.Duration;
@@ -43,7 +43,7 @@ class TimeoutTest {
@Inject
private RunnerUtils runnerUtils;
@Test
@RetryingTest(5) // Flaky on CI but never locally even with 100 repetitions
void timeout() throws TimeoutException, QueueException {
List<LogEntry> logs = new CopyOnWriteArrayList<>();
Flux<LogEntry> receive = TestsUtils.receive(workerTaskLogQueue, either -> logs.add(either.getLeft()));

View File

@@ -437,6 +437,33 @@ class RequestTest {
}
}
@SuppressWarnings("deprecation")
@Test
void basicAuthOld() throws Exception {
try (
ApplicationContext applicationContext = ApplicationContext.run();
EmbeddedServer server = applicationContext.getBean(EmbeddedServer.class).start();
) {
Request task = Request.builder()
.id(RequestTest.class.getSimpleName())
.type(RequestTest.class.getName())
.uri(Property.of(server.getURL().toString() + "/auth/basic"))
.options(HttpConfiguration.builder()
.basicAuthUser("John")
.basicAuthPassword("p4ss")
.build()
)
.build();
RunContext runContext = TestsUtils.mockRunContext(this.runContextFactory, task, Map.of());
Request.Output output = task.run(runContext);
assertThat(output.getBody(), is("{\"hello\":\"John\"}"));
assertThat(output.getCode(), is(200));
}
}
@Test
void bearerAuth() throws Exception {
try (
@@ -464,6 +491,7 @@ class RequestTest {
}
}
@Controller
static class MockController {
@Get("/hello")

View File

@@ -1,23 +1,28 @@
package io.kestra.plugin.core.log;
import io.kestra.core.junit.annotations.KestraTest;
import io.kestra.core.junit.annotations.LoadFlows;
import io.kestra.core.models.executions.Execution;
import io.kestra.core.models.executions.LogEntry;
import io.kestra.core.models.property.Property;
import io.kestra.core.repositories.LogRepositoryInterface;
import io.kestra.core.runners.RunContextFactory;
import io.kestra.core.runners.RunnerUtils;
import jakarta.inject.Inject;
import java.time.temporal.ChronoUnit;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.slf4j.event.Level;
import java.time.Instant;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Map;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertTrue;
@KestraTest
@KestraTest(startRunner = true)
class PurgeLogsTest {
@Inject
private RunContextFactory runContextFactory;
@@ -25,8 +30,12 @@ class PurgeLogsTest {
@Inject
private LogRepositoryInterface logRepository;
@Inject
protected RunnerUtils runnerUtils;
@Test
void run() throws Exception {
@LoadFlows("flows/valids/purge_logs_no_arguments.yaml")
void run_with_no_arguments() throws Exception {
// create an execution to delete
var logEntry = LogEntry.builder()
.namespace("namespace")
@@ -37,12 +46,71 @@ class PurgeLogsTest {
.build();
logRepository.save(logEntry);
var purge = PurgeLogs.builder()
.endDate(Property.of(ZonedDateTime.now().plusMinutes(1).format(DateTimeFormatter.ISO_ZONED_DATE_TIME)))
.build();
var runContext = runContextFactory.of(Map.of("flow", Map.of("namespace", "namespace", "id", "flowId")));
var output = purge.run(runContext);
Execution execution = runnerUtils.runOne(null, "io.kestra.tests", "purge_logs_no_arguments");
assertThat(output.getCount(), is(1));
assertTrue(execution.getState().isSuccess());
assertThat(execution.getTaskRunList().size(), is(1));
assertThat(execution.getTaskRunList().getFirst().getOutputs().get("count"), is(1));
}
@ParameterizedTest
@MethodSource("buildArguments")
@LoadFlows("flows/valids/purge_logs_full_arguments.yaml")
void run_with_full_arguments(LogEntry logEntry, int resultCount, String failingReason) throws Exception {
logRepository.save(logEntry);
Execution execution = runnerUtils.runOne(null, "io.kestra.tests", "purge_logs_full_arguments");
assertTrue(execution.getState().isSuccess());
assertThat(execution.getTaskRunList().size(), is(1));
assertThat(failingReason, execution.getTaskRunList().getFirst().getOutputs().get("count"), is(resultCount));
}
static Stream<Arguments> buildArguments() {
return Stream.of(
Arguments.of(LogEntry.builder()
.namespace("purge.namespace")
.flowId("purgeFlowId")
.timestamp(Instant.now().plus(5, ChronoUnit.HOURS))
.level(Level.INFO)
.message("Hello World")
.build(), 0, "The log is too recent to be found"),
Arguments.of(LogEntry.builder()
.namespace("purge.namespace")
.flowId("purgeFlowId")
.timestamp(Instant.now().minus(5, ChronoUnit.HOURS))
.level(Level.INFO)
.message("Hello World")
.build(), 0, "The log is too old to be found"),
Arguments.of(LogEntry.builder()
.namespace("uncorrect.namespace")
.flowId("purgeFlowId")
.timestamp(Instant.now().minusSeconds(10))
.level(Level.INFO)
.message("Hello World")
.build(), 0, "The log has an incorrect namespace"),
Arguments.of(LogEntry.builder()
.namespace("purge.namespace")
.flowId("wrongFlowId")
.timestamp(Instant.now().minusSeconds(10))
.level(Level.INFO)
.message("Hello World")
.build(), 0, "The log has an incorrect flow id"),
Arguments.of(LogEntry.builder()
.namespace("purge.namespace")
.flowId("purgeFlowId")
.timestamp(Instant.now().minusSeconds(10))
.level(Level.WARN)
.message("Hello World")
.build(), 0, "The log has an incorrect LogLevel"),
Arguments.of(LogEntry.builder()
.namespace("purge.namespace")
.flowId("purgeFlowId")
.timestamp(Instant.now().minusSeconds(10))
.level(Level.INFO)
.message("Hello World")
.build(), 1, "The log should be deleted")
);
}
}

View File

@@ -0,0 +1,13 @@
id: purge_logs_full_arguments
namespace: io.kestra.tests
tasks:
- id: purge_logs
type: io.kestra.plugin.core.log.PurgeLogs
endDate: "{{ now() | dateAdd(2, 'HOURS') }}"
startDate: "{{ now() | dateAdd(-2, 'HOURS') }}"
namespace: purge.namespace
flowId: purgeFlowId
logLevels:
- INFO
- ERROR

View File

@@ -0,0 +1,7 @@
id: purge_logs_no_arguments
namespace: io.kestra.tests
tasks:
- id: purge_logs
type: io.kestra.plugin.core.log.PurgeLogs
endDate: "{{ now() | dateAdd(2, 'HOURS') }}"

View File

@@ -10,6 +10,7 @@ inputs:
labels:
switchFlowLabel: switchFoo
overriding: child
tasks:
- id: parent-seq

View File

@@ -7,6 +7,7 @@ inputs:
labels:
mainFlowLabel: flowFoo
overriding: parent
tasks:
- id: launch

View File

@@ -0,0 +1,17 @@
id: request-basicauth-deprecated
namespace: sanitycheck.plugin.core.http
tasks:
- id: request
type: io.kestra.plugin.core.http.Request
uri: https://testpages.eviltester.com/styled/auth/basic-auth-results.html
method: GET
options:
basicAuthUser: authorized
basicAuthPassword: password001
- id: assert
type: io.kestra.plugin.core.execution.Assert
errorMessage: "Invalid response code {{ outputs.request.code }}"
conditions:
- "{{ outputs.request.code == 200 }}"

View File

@@ -0,0 +1,19 @@
id: request-basicauth
namespace: sanitycheck.plugin.core.http
tasks:
- id: request
type: io.kestra.plugin.core.http.Request
uri: https://testpages.eviltester.com/styled/auth/basic-auth-results.html
method: GET
options:
auth:
type: BASIC
username: authorized
password: password001
- id: assert
type: io.kestra.plugin.core.execution.Assert
errorMessage: "Invalid response code {{ outputs.request.code }}"
conditions:
- "{{ outputs.request.code == 200 }}"

View File

@@ -19,7 +19,8 @@
# ./release-plugins.sh --release-version=0.20.0 --next-version=0.21.0-SNAPSHOT
# To release a specific plugin:
# ./release-plugins.sh --release-version=0.20.0 --next-version=0.21.0-SNAPSHOT plugin-kubernetes
# To release specific plugins from file:
# ./release-plugins.sh --release-version=0.20.0 --plugin-file .plugins
#===============================================================================
set -e;
@@ -29,7 +30,7 @@ set -e;
###############################################################
BASEDIR=$(dirname "$(readlink -f $0)")
WORKING_DIR=/tmp/kestra-release-plugins-$(date +%s);
PLUGIN_FILE="$BASEDIR/.plugins"
PLUGIN_FILE="$BASEDIR/../.plugins"
GIT_BRANCH=master
###############################################################
@@ -43,6 +44,7 @@ usage() {
echo "Options:"
echo " --release-version <version> Specify the release version (required)."
echo " --next-version <version> Specify the next version (required)."
echo " --plugin-file File containing the plugin list (default: .plugins)"
echo " --dry-run Specify to run in DRY_RUN."
echo " -y, --yes Automatically confirm prompts (non-interactive)."
echo " -h, --help Show this help message and exit."
@@ -81,6 +83,14 @@ while [[ "$#" -gt 0 ]]; do
NEXT_VERSION="${1#*=}"
shift
;;
--plugin-file)
PLUGIN_FILE="$2"
shift 2
;;
--plugin-file=*)
PLUGIN_FILE="${1#*=}"
shift
;;
--dry-run)
DRY_RUN=true
shift

View File

@@ -1,12 +1,12 @@
#!/bin/bash
#===============================================================================
# SCRIPT: tag-release-plugins.sh
# SCRIPT: setversion-tag-plugins.sh
#
# DESCRIPTION:
# This script can be used to update and tag plugins from a release branch .e.g., releases/v0.21.x.
# By default, if no `GITHUB_PAT` environment variable exist, the script will attempt to clone GitHub repositories using SSH_KEY.
#
# USAGE: ./tag-release-plugins.sh [options]
# USAGE: ./setversion-tag-plugins.sh [options]
# OPTIONS:
# --release-version <version> Specify the release version (required)
# --dry-run Specify to run in DRY_RUN.
@@ -15,10 +15,11 @@
# EXAMPLES:
# To release all plugins:
# ./tag-release-plugins.sh --release-version=0.20.0
# ./setversion-tag-plugins.sh --release-version=0.20.0
# To release a specific plugin:
# ./tag-release-plugins.sh --release-version=0.20.0 plugin-kubernetes
# ./setversion-tag-plugins.sh --release-version=0.20.0 plugin-kubernetes
# To release specific plugins from file:
# ./setversion-tag-plugins.sh --release-version=0.20.0 --plugin-file .plugins
#===============================================================================
set -e;
@@ -28,7 +29,7 @@ set -e;
###############################################################
BASEDIR=$(dirname "$(readlink -f $0)")
WORKING_DIR=/tmp/kestra-release-plugins-$(date +%s);
PLUGIN_FILE="$BASEDIR/.plugins"
PLUGIN_FILE="$BASEDIR/../.plugins"
###############################################################
# Functions
@@ -40,6 +41,7 @@ usage() {
echo
echo "Options:"
echo " --release-version <version> Specify the release version (required)."
echo " --plugin-file File containing the plugin list (default: .plugins)"
echo " --dry-run Specify to run in DRY_RUN."
echo " -y, --yes Automatically confirm prompts (non-interactive)."
echo " -h, --help Show this help message and exit."
@@ -70,6 +72,14 @@ while [[ "$#" -gt 0 ]]; do
RELEASE_VERSION="${1#*=}"
shift
;;
--plugin-file)
PLUGIN_FILE="$2"
shift 2
;;
--plugin-file=*)
PLUGIN_FILE="${1#*=}"
shift
;;
--dry-run)
DRY_RUN=true
shift
@@ -163,7 +173,7 @@ do
git checkout "$RELEASE_BRANCH";
# Update version
sed -i.bak "s/^version=.*/version=$RELEASE_VERSION/" ./gradle.properties
sed -i "s/^version=.*/version=$RELEASE_VERSION/" ./gradle.properties
git add ./gradle.properties
git commit -m"chore(version): update to version 'v$RELEASE_VERSION'."
git push

View File

@@ -1,6 +1,6 @@
version=0.21.0-rc0-SNAPSHOT
version=0.21.3
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.priority=low
org.gradle.priority=low

View File

@@ -1,5 +1,5 @@
CREATE TABLE IF NOT EXISTS dashboards (
"key" VARCHAR(250) NOT NULL PRIMARY KEY,
"key" VARCHAR(250) NOT NULL PRIMARY KEY,
"value" TEXT NOT NULL,
"deleted" BOOL NOT NULL GENERATED ALWAYS AS (JQ_BOOLEAN("value", '.deleted')),
"tenant_id" VARCHAR(250) NOT NULL GENERATED ALWAYS AS (JQ_STRING("value", '.tenantId')),

View File

@@ -6,6 +6,7 @@ import io.kestra.core.models.flows.FlowWithSource;
import io.kestra.core.models.triggers.AbstractTrigger;
import io.kestra.core.models.triggers.Trigger;
import io.kestra.core.models.triggers.TriggerContext;
import io.kestra.core.queues.QueueException;
import io.kestra.core.schedulers.ScheduleContextInterface;
import io.kestra.core.schedulers.SchedulerTriggerStateInterface;
import io.kestra.jdbc.repository.AbstractJdbcTriggerRepository;
@@ -77,6 +78,10 @@ public class JdbcSchedulerTriggerState implements SchedulerTriggerStateInterface
return this.triggerRepository.update(flow, abstractTrigger, conditionContext);
}
public void delete(Trigger trigger) throws QueueException {
this.triggerRepository.delete(trigger);
}
@Override
public List<Trigger> findByNextExecutionDateReadyForAllTenants(ZonedDateTime now, ScheduleContextInterface scheduleContext) {
return this.triggerRepository.findByNextExecutionDateReadyForAllTenants(now, scheduleContext);

View File

@@ -1,6 +1,7 @@
package io.kestra.jdbc.runner;
import com.google.common.collect.ImmutableMap;
import io.kestra.core.junit.annotations.KestraTest;
import io.kestra.core.models.conditions.ConditionContext;
import io.kestra.core.models.executions.Execution;
import io.kestra.core.models.executions.TaskRun;
@@ -11,14 +12,7 @@ import io.kestra.core.models.triggers.Trigger;
import io.kestra.core.queues.QueueFactoryInterface;
import io.kestra.core.queues.QueueInterface;
import io.kestra.core.repositories.LocalFlowRepositoryLoader;
import io.kestra.core.runners.RunContextFactory;
import io.kestra.core.runners.StandAloneRunner;
import io.kestra.core.runners.Worker;
import io.kestra.core.runners.WorkerJob;
import io.kestra.core.runners.WorkerTask;
import io.kestra.core.runners.WorkerTaskResult;
import io.kestra.core.runners.WorkerTrigger;
import io.kestra.core.runners.WorkerTriggerResult;
import io.kestra.core.runners.*;
import io.kestra.core.services.SkipExecutionService;
import io.kestra.core.tasks.test.SleepTrigger;
import io.kestra.core.utils.Await;
@@ -28,7 +22,6 @@ import io.kestra.jdbc.JdbcTestUtils;
import io.kestra.plugin.core.flow.Sleep;
import io.micronaut.context.ApplicationContext;
import io.micronaut.context.annotation.Property;
import io.kestra.core.junit.annotations.KestraTest;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import org.junit.jupiter.api.BeforeAll;
@@ -157,7 +150,7 @@ public abstract class JdbcServiceLivenessCoordinatorTest {
});
workerJobQueue.emit(workerTask);
boolean runningLatchAwait = runningLatch.await(2, TimeUnit.SECONDS);
boolean runningLatchAwait = runningLatch.await(10, TimeUnit.SECONDS);
assertThat(runningLatchAwait, is(true));
worker.shutdown();

View File

@@ -4,8 +4,6 @@ import io.kestra.core.models.annotations.Plugin.Id;
import jakarta.validation.constraints.NotNull;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
@@ -55,6 +53,18 @@ public interface Plugin {
.orElse(false);
}
/**
* Static helper method to check whether a given plugin is deprecated.
*
* @param plugin The plugin type.
* @return {@code true} if the plugin is deprecated.
*/
static boolean isDeprecated(final Class<?> plugin) {
Objects.requireNonNull(plugin, "Cannot check if a plugin is deprecated from null");
Deprecated annotation = plugin.getAnnotation(Deprecated.class);
return annotation != null;
}
/**
* Static helper method to get the id of a plugin.
*

View File

@@ -13,7 +13,7 @@ javaPlatform {
dependencies {
// versions for libraries with multiple module but no BOM
def slf4jVersion = "2.0.16"
def protobufVersion = "4.29.3"
def protobufVersion = "3.25.5" // Orc still uses 3.25.5 see https://github.com/apache/orc/blob/main/java/pom.xml
def bouncycastleVersion = "1.80"
def aetherVersion = "1.1.0"
def jollydayVersion = "0.32.0"

View File

@@ -143,7 +143,7 @@ public abstract class AbstractExecScript extends Task implements RunnableTask<Sc
* protected DockerOptions docker = DockerOptions.builder().build();
* }</pre>
*/
protected DockerOptions injectDefaults(RunContext runContext, @NotNull DockerOptions original) {
protected DockerOptions injectDefaults(RunContext runContext, @NotNull DockerOptions original) throws IllegalVariableEvaluationException {
// FIXME to keep backward compatibility, we call the old method from the new one by default
return injectDefaults(original);
}

View File

@@ -336,6 +336,8 @@ public class Docker extends TaskRunner<Docker.DockerTaskRunnerDetailResult> {
@Override
public TaskRunnerResult<DockerTaskRunnerDetailResult> run(RunContext runContext, TaskCommands taskCommands, List<String> filesToDownload) throws Exception {
Boolean renderedDelete = runContext.render(delete).as(Boolean.class).orElseThrow();
if (taskCommands.getContainerImage() == null && this.image == null) {
throw new IllegalArgumentException("This task runner needs the `containerImage` property to be set");
}
@@ -535,7 +537,7 @@ public class Docker extends TaskRunner<Docker.DockerTaskRunnerDetailResult> {
// come to a normal end.
kill();
if (Boolean.TRUE.equals(runContext.render(delete).as(Boolean.class).orElseThrow())) {
if (Boolean.TRUE.equals(renderedDelete)) {
dockerClient.removeContainerCmd(exec.getId()).exec();
if (logger.isTraceEnabled()) {
logger.trace("Container deleted: {}", exec.getId());

8
ui/package-lock.json generated
View File

@@ -9,7 +9,7 @@
"version": "0.0.0",
"dependencies": {
"@js-joda/core": "^5.6.3",
"@kestra-io/ui-libs": "^0.0.119",
"@kestra-io/ui-libs": "^0.0.129",
"@vue-flow/background": "^1.3.2",
"@vue-flow/controls": "^1.1.2",
"@vue-flow/core": "^1.42.1",
@@ -2499,9 +2499,9 @@
"license": "BSD-3-Clause"
},
"node_modules/@kestra-io/ui-libs": {
"version": "0.0.119",
"resolved": "https://registry.npmjs.org/@kestra-io/ui-libs/-/ui-libs-0.0.119.tgz",
"integrity": "sha512-KfIY0YG5OmsJW9kL1yBmgDEbs1UFru5SFSrHM5ea7IFUkipzWviVdfWb7u8lnGyN3L05BVYO3hcRi6zYYcvheQ==",
"version": "0.0.129",
"resolved": "https://registry.npmjs.org/@kestra-io/ui-libs/-/ui-libs-0.0.129.tgz",
"integrity": "sha512-SacgVN8GeRfhBeq1K76/1xdc1ZwXW4lOzlKdpV0C2xAzGDkvhORzCyRHJF7vQX+i9OCD73N90Zvu5/UZpzfj7Q==",
"dependencies": {
"@nuxtjs/mdc": "^0.12.1",
"@popperjs/core": "^2.11.8",

View File

@@ -19,7 +19,7 @@
},
"dependencies": {
"@js-joda/core": "^5.6.3",
"@kestra-io/ui-libs": "^0.0.119",
"@kestra-io/ui-libs": "^0.0.129",
"@vue-flow/background": "^1.3.2",
"@vue-flow/controls": "^1.1.2",
"@vue-flow/core": "^1.42.1",

View File

@@ -0,0 +1,28 @@
const MINIMUM: number = 100000;
const MAXIMUM: number = 999999;
const ANIMALS: string[] = [
"Aardvark", "Albatross", "Alligator", "Alpaca", "Ant", "Anteater", "Antelope", "Ape", "Armadillo", "Baboon",
"Badger", "Barracuda", "Bat", "Bear", "Beaver", "Bee", "Bison", "Boar", "Buffalo", "Butterfly", "Camel", "Capybara",
"Caribou", "Cassowary", "Cat", "Caterpillar", "Cattle", "Chamois", "Cheetah", "Chicken", "Chimpanzee", "Chinchilla",
"Chough", "Clam", "Cobra", "Cockroach", "Cod", "Cormorant", "Coyote", "Crab", "Crane", "Crocodile", "Crow", "Curlew",
"Deer", "Dinosaur", "Dog", "Dogfish", "Dolphin", "Dotterel", "Dove", "Dragonfly", "Duck", "Dugong", "Dunlin", "Eagle",
"Echidna", "Eel", "Eland", "Elephant", "Elk", "Emu", "Falcon", "Ferret", "Finch", "Fish", "Flamingo", "Fly", "Fox",
"Frog", "Gaur", "Gazelle", "Gerbil", "Giraffe", "Gnat", "Gnu", "Goat", "Goldfinch", "Goldfish", "Goose", "Gorilla",
"Goshawk", "Grasshopper", "Grouse", "Guanaco", "Gull", "Hamster", "Hare", "Hawk", "Hedgehog", "Heron", "Herring",
"Hippopotamus", "Hornet", "Horse", "Human", "Hummingbird", "Hyena", "Ibex", "Ibis", "Jackal", "Jaguar", "Jay",
"Jellyfish", "Kangaroo", "Kingfisher", "Koala", "Kookabura", "Kouprey", "Kudu", "Lapwing", "Lark", "Lemur", "Leopard",
"Lion", "Llama", "Lobster", "Locust", "Loris", "Louse", "Lyrebird", "Magpie", "Mallard", "Manatee", "Mandrill", "Mantis",
"Marten", "Meerkat", "Mink", "Mole", "Mongoose", "Monkey", "Moose", "Mosquito", "Mouse", "Mule", "Narwhal", "Newt",
"Nightingale", "Octopus", "Okapi", "Opossum", "Oryx", "Ostrich", "Otter", "Owl", "Oyster", "Panther", "Parrot", "Partridge",
"Peafowl", "Pelican", "Penguin", "Pheasant", "Pigeon", "Pony", "Porcupine", "Porpoise", "Quail", "Quelea", "Quetzal",
"Rabbit", "Rail", "Ram", "Rat", "Raven", "Rhinoceros", "Rook", "Salamander", "Salmon", "Sandpiper", "Sardine", "Scorpion",
"Seahorse", "Seal", "Shark", "Shrew", "Skunk", "Snail", "Snake", "Sparrow", "Spider", "Spoonbill", "Squid", "Squirrel",
"Starling", "Stingray", "Stinkbug", "Stork", "Swallow", "Swan", "Tapir", "Tarsier", "Termite", "Tiger", "Toad",
"Trout", "Turkey", "Turtle", "Viper", "Vulture", "Wallaby", "Walrus", "Wasp", "Weasel", "Whale", "Wildcat", "Wolf",
"Wolverine", "Wombat", "Woodcock", "Woodpecker", "Worm", "Wren", "Yak", "Zebra"
];
const getRandomNumber = (minimum: number = MINIMUM, maximum: number = MAXIMUM): number => Math.floor(Math.random() * (maximum - minimum + 1)) + minimum;
const getRandomAnimal = (): string => ANIMALS[Math.floor(Math.random() * ANIMALS.length)];
export const getRandomFlowID = (): string => `${getRandomAnimal()}_${getRandomNumber()}`.toLowerCase();

View File

@@ -105,7 +105,7 @@
},
methods: {
displayApp() {
Utils.switchTheme();
Utils.switchTheme(this.$store);
document.getElementById("loader-wrapper").style.display = "none";
document.getElementById("app-container").style.display = "block";

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -6,7 +6,6 @@ Welcome to the Custom Dashboard! This feature allows you to create and manage pe
Below is an example of a dashboard definition that displays executions over time, a table that uses metrics to display the sum of sales per namespace, and a table that shows the log count by level per namespace:
::collapse{title="Expand for a example dashboard definition"}
```yaml
title: Getting Started
description: First custom dashboard
@@ -84,7 +83,6 @@ charts:
- dev_graph
- prod_graph
```
::
To see all available properties to configure a custom dashboard as code, see examples provided in the [Enterprise Edition Examples](https://github.com/kestra-io/enterprise-edition-examples) repository.

BIN
ui/src/assets/no_data.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -171,7 +171,7 @@
const onSwitchTheme = () => {
themeIsDark.value = !themeIsDark.value;
Utils.switchTheme(themeIsDark.value ? "dark" : "light");
Utils.switchTheme(store, themeIsDark.value ? "dark" : "light");
}
</script>

View File

@@ -0,0 +1,29 @@
<template>
<span>
<slot />
<LockIcon v-if="enable" class="lock-ee" />
</span>
</template>
<script lang="ts" setup>
import LockIcon from "vue-material-design-icons/LockOutline.vue";
defineProps({
enable: {
type: Boolean,
required: true,
},
})
</script>
<style lang="scss" scoped>
span {
display: inline-flex;
align-items: center;
justify-content: start;
}
.lock-ee {
margin-left:.5rem;
opacity:.5;
}
</style>

View File

@@ -1,119 +0,0 @@
<template>
<el-tooltip
data-component="FILENAME_PLACEHOLDER"
:visible="visible"
:persistent="false"
:focus-on-show="true"
popper-class="ee-tooltip"
:disabled="!disabled"
:placement="placement"
>
<template #content v-if="link">
<el-button circle class="ee-tooltip-close" @click="changeVisibility(false)">
<Close />
</el-button>
<p>{{ $t("ee-tooltip.features-blocked") }}</p>
<a
class="el-button el-button--primary d-block"
type="primary"
:href="link"
target="_blank"
>
Talk to us
</a>
</template>
<template #default>
<span ref="slot-container" class="cursor-pointer" @click="changeVisibility()">
<slot />
<lock v-if="disabled" />
</span>
</template>
</el-tooltip>
</template>
<script>
import Close from "vue-material-design-icons/Close.vue";
import Lock from "vue-material-design-icons/Lock.vue";
export default {
components: {Close, Lock},
props: {
top: {
type: Boolean,
default: true
},
placement: {
type: String,
default: "auto"
},
disabled: {
type: Boolean,
default: false
},
content: {
type: String,
default: undefined
},
term: {
type: String,
default: undefined
},
},
data() {
return {
visible: false,
}
},
methods: {
changeVisibility(visible = true) {
if (visible) document.querySelector(".ee-tooltip")?.remove();
this.visible = visible
}
},
computed: {
link() {
let link = "https://kestra.io/demo?utm_source=app&utm_campaign=ee-tooltip";
if (this.term) {
link = link + "&utm_term=" + this.term;
}
if (this.content) {
link = link + "&utm_content=" + this.content;
}
return link;
}
}
};
</script>
<style lang="scss" scoped>
:global(.el-popper.ee-tooltip) {
max-width: 320px;
padding: 2rem;
}
p {
font-size: var(--font-size-lg);
text-align: center;
margin-bottom: 2rem;
font-weight: bold;
}
:deep(.material-design-icon) > .material-design-icon__svg {
bottom: -0.125em;
}
.ee-tooltip-close {
position: absolute;
top: 0;
right: 0;
border: none;
margin: 0.5rem;
}
</style>

View File

@@ -1,18 +1,12 @@
<template>
<div v-if="isLocked" v-bind="$attrs">
<span ref="slotContainer" class="d-none">
<slot />
</span>
<enterprise-tooltip v-if="term" :disabled="true" :term="term" content="left-menu">
<slot />
</enterprise-tooltip>
</div>
<a v-else-if="isHyperLink" v-bind="$attrs">
<a v-if="isHyperLink" v-bind="$attrs">
<slot />
</a>
<router-link v-else :to="$attrs.href" custom v-slot="{href:linkHref, navigate}">
<a v-bind="$attrs" :href="linkHref" @click="navigate">
<slot />
<enterprise-badge :enable="isLocked">
<slot />
</enterprise-badge>
</a>
</router-link>
</template>
@@ -20,7 +14,7 @@
<script setup>
import {computed, ref, onMounted} from "vue"
import {useRouter} from "vue-router";
import EnterpriseTooltip from "./EnterpriseTooltip.vue";
import EnterpriseBadge from "./EnterpriseBadge.vue";
defineOptions({
name: "LeftMenuLink",

View File

@@ -13,12 +13,10 @@
<el-tooltip v-if="tab.disabled && tab.props && tab.props.showTooltip" :content="$t('add-trigger-in-editor')" placement="top">
<span><strong>{{ tab.title }}</strong></span>
</el-tooltip>
<span v-if="!tab.hideTitle">
<enterprise-tooltip :disabled="tab.locked" :term="tab.name" content="tabs">
{{ tab.title }}
<el-badge :type="tab.count > 0 ? 'danger' : 'primary'" :value="tab.count" v-if="tab.count !== undefined" />
</enterprise-tooltip>
</span>
<enterprise-badge :enable="tab.locked">
{{ tab.title }}
<el-badge :type="tab.count > 0 ? 'danger' : 'primary'" :value="tab.count" v-if="tab.count !== undefined" />
</enterprise-badge>
</component>
</template>
</el-tab-pane>
@@ -51,10 +49,10 @@
import {mapState, mapMutations} from "vuex";
import EditorSidebar from "./inputs/EditorSidebar.vue";
import EnterpriseTooltip from "./EnterpriseTooltip.vue";
import EnterpriseBadge from "./EnterpriseBadge.vue";
export default {
components: {EditorSidebar, EnterpriseTooltip},
components: {EditorSidebar, EnterpriseBadge},
props: {
tabs: {
type: Array,

View File

@@ -13,6 +13,7 @@
:metadata
@update-metadata="(k, v) => emits('updateMetadata', {[k]: v})"
@update-task="(yaml) => emits('updateTask', yaml)"
@reorder="(yaml) => emits('reorder', yaml)"
@update-documentation="(task) => emits('updateDocumentation', task)"
/>
</div>
@@ -30,6 +31,7 @@
"updateTask",
"updateMetadata",
"updateDocumentation",
"reorder",
]);
const props = defineProps({
flow: {type: String, required: true},

View File

@@ -6,7 +6,7 @@
class="item"
@click="
(store.commit('code/removeBreadcrumb', {position: index}),
store.commit('code/unsetPanel'))
store.commit('code/unsetPanel', false))
"
>
<router-link :to="breadcrumb.to">

View File

@@ -11,15 +11,22 @@
<Creation :section="item.title" />
</template>
<template v-if="creation">
<Element
v-for="(element, elementIndex) in item.elements"
:key="elementIndex"
:section="item.title"
:element
@remove-element="removeElement(item.title, elementIndex)"
/>
</template>
<Element
v-for="(element, elementIndex) in item.elements"
:key="elementIndex"
:section="item.title"
:element
@remove-element="removeElement(item.title, elementIndex)"
@move-element="
(direction: 'up' | 'down') =>
moveElement(
item.elements,
element.id,
elementIndex,
direction,
)
"
/>
</el-collapse-item>
</el-collapse>
</template>
@@ -32,7 +39,7 @@
import Creation from "./buttons/Creation.vue";
import Element from "./Element.vue";
const emits = defineEmits(["remove"]);
const emits = defineEmits(["remove", "reorder"]);
const props = defineProps({
items: {
@@ -67,6 +74,27 @@
}
});
};
import {YamlUtils as YAML_FROM_UI_LIBS} from "@kestra-io/ui-libs";
const moveElement = (
items: Record<string, any>[] | undefined,
elementID: string,
index: number,
direction: "up" | "down",
) => {
if (!items || !props.flow) return;
if (
(direction === "up" && index === 0) ||
(direction === "down" && index === items.length - 1)
)
return;
const newIndex = direction === "up" ? index - 1 : index + 1;
emits(
"reorder",
YAML_FROM_UI_LIBS.swapTasks(props.flow, elementID, items[newIndex].id),
);
};
</script>
<style scoped lang="scss">

View File

@@ -14,17 +14,21 @@
size="small"
class="border-0"
/>
<div class="d-flex flex-column">
<ChevronUp @click.prevent.stop="emits('moveElement', 'up')" />
<ChevronDown @click.prevent.stop="emits('moveElement', 'down')" />
</div>
</div>
</template>
<script setup lang="ts">
import {computed} from "vue";
import {DeleteOutline} from "../../utils/icons";
import {DeleteOutline, ChevronUp, ChevronDown} from "../../utils/icons";
import TaskIcon from "@kestra-io/ui-libs/src/components/misc/TaskIcon.vue";
const emits = defineEmits(["removeElement"]);
const emits = defineEmits(["removeElement", "moveElement"]);
const props = defineProps({
section: {type: String, required: true},

View File

@@ -2,13 +2,22 @@
<span v-if="required" class="me-1 text-danger">*</span>
<span v-if="label" class="label">{{ label }}</span>
<div class="mt-1 mb-2 wrapper" :class="props.class">
<el-input v-model="input" @input="handleInput" :placeholder :disabled />
<el-input
v-model="input"
@input="handleInput"
:placeholder
:disabled
type="textarea"
:autosize="{minRows: 1}"
/>
</div>
</template>
<script setup lang="ts">
import {ref, watch} from "vue";
defineOptions({inheritAttrs: false});
const emits = defineEmits(["update:modelValue"]);
const props = defineProps({
modelValue: {type: [String, Number, Boolean], default: undefined},

View File

@@ -29,6 +29,7 @@
creation
:flow
@remove="(yaml) => emits('updateTask', yaml)"
@reorder="(yaml) => emits('reorder', yaml)"
/>
<hr class="my-4">
@@ -96,6 +97,7 @@
"updateTask",
"updateMetadata",
"updateDocumentation",
"reorder",
]);
const saveEvent = (e: KeyboardEvent) => {
@@ -235,6 +237,7 @@
"error_handlers",
YamlUtils.parse(props.flow).errors ?? [],
),
getSectionTitle("finally", YamlUtils.parse(props.flow).finally ?? []),
];
});
</script>

View File

@@ -10,7 +10,6 @@
v-else
:is="lastBreadcumb.component.type"
v-bind="lastBreadcumb.component.props"
v-on="lastBreadcumb.component.listeners"
:model-value="lastBreadcumb.component.props.modelValue"
@update:model-value="validateTask"
/>
@@ -126,39 +125,51 @@
YamlUtils.parse(yaml.value).id,
);
if (route.query.section === SECTIONS.TRIGGERS.toLowerCase()) {
const existingTask = YamlUtils.checkTaskAlreadyExist(
source,
CURRENT.value,
);
if (existingTask) {
store.dispatch("core/showMessage", {
variant: "error",
title: "Trigger Id already exist",
message: `Trigger Id ${existingTask} already exist in the flow.`,
});
return;
}
const currentSection = route.query.section;
const isCreation =
props.creation &&
(!route.query.identifier || route.query.identifier === "new");
emits("updateTask", YamlUtils.insertTrigger(source, CURRENT.value));
CURRENT.value = null;
} else {
const action = props.creation
? YamlUtils.insertTask(
let result;
if (isCreation) {
if (currentSection === "tasks") {
const existing = YamlUtils.checkTaskAlreadyExist(
source,
YamlUtils.getLastTask(source),
task,
"after",
)
: YamlUtils.replaceTaskInDocument(
source,
route.query.identifier,
task,
CURRENT.value,
);
emits("updateTask", action);
if (existing) {
store.dispatch("core/showMessage", {
variant: "error",
title: "Task with same ID already exist",
message: `Task in ${route.query.section} block with ID: ${existing} already exist in the flow.`,
});
return;
}
result = YamlUtils.insertTask(
source,
route.query.target ?? YamlUtils.getLastTask(source),
task,
route.query.position ?? "after",
);
} else if (currentSection === "triggers") {
result = YamlUtils.insertTrigger(source, CURRENT.value);
} else if (currentSection === "error handlers") {
result = YamlUtils.insertError(source, CURRENT.value);
} else if (currentSection === "finally") {
result = YamlUtils.insertFinally(source, CURRENT.value);
}
} else {
result = YamlUtils.replaceTaskInDocument(
source,
route.query.identifier,
task,
);
}
emits("updateTask", result);
store.commit("code/removeBreadcrumb", {last: true});
// eslint-disable-next-line @typescript-eslint/no-unused-vars

View File

@@ -53,7 +53,8 @@ $code-font-sm: var(--el-font-size-small);
font-size: $code-font-sm;
}
.delete {
.delete,
.reorder {
cursor: pointer;
padding-left: 0;
color: $code-gray-700;
@@ -64,7 +65,8 @@ $code-font-sm: var(--el-font-size-small);
:deep(*) {
--el-disabled-text-color: #{$code-gray-700};
.el-input__inner {
.el-input__inner,
.el-textarea__inner {
color: $code-gray-700;
font-size: $code-font-sm;
}

View File

@@ -1,5 +1,7 @@
import Plus from "vue-material-design-icons/Plus.vue";
import ContentSave from "vue-material-design-icons/ContentSave.vue";
import DeleteOutline from "vue-material-design-icons/DeleteOutline.vue";
import ChevronUp from "vue-material-design-icons/ChevronUp.vue";
import ChevronDown from "vue-material-design-icons/ChevronDown.vue";
export {Plus, ContentSave, DeleteOutline};
export {Plus, ContentSave, DeleteOutline, ChevronUp, ChevronDown};

View File

@@ -194,14 +194,8 @@
/>
</el-col>
<el-col :xs="24" :lg="props.flow ? 7 : 12">
<ExecutionsDoughnut
v-if="props.flow"
:data="graphData"
:total="stats.total"
class="ms-2"
/>
<ExecutionsNextScheduled
v-else-if="isAllowedTriggers"
v-if="isAllowedTriggers"
:flow="props.flowId"
:namespace="props.namespace"
class="ms-2"

View File

@@ -1,6 +1,6 @@
<template>
<div class="state">
<span class="circle" :style="{backgroundColor: getScheme(label)}" />
<span class="circle" :style="{backgroundColor: scheme[label]}" />
<p class="m-0 fw-light small">
{{ label.toLowerCase().capitalize() }}
@@ -9,7 +9,9 @@
</template>
<script setup>
import {getScheme} from "../../../utils/scheme.js";
import {useScheme} from "../../../utils/scheme.js";
const scheme = useScheme();
defineProps({
label: {

View File

@@ -18,6 +18,7 @@
import {Bar} from "vue-chartjs";
import {customBarLegend} from "../legend.js";
import {useTheme} from "../../../../../utils/utils.js";
import {defaultConfig, getConsistentHEXColor,} from "../../../../../utils/charts.js";
import {useStore} from "vuex";
@@ -52,6 +53,8 @@
const aggregator = Object.entries(data.columns).filter(([_, v]) => v.agg);
const theme = useTheme();
const options = computed(() => {
return defaultConfig({
skipNull: true,
@@ -102,7 +105,7 @@
}
},
},
});
}, theme.value);
});
function isDurationAgg() {
@@ -142,7 +145,7 @@
return Object.entries(grouped[xLabel]).map(subSectionsEntry => ({
label: subSectionsEntry[0],
data: xLabels.map(label => xLabel === label ? subSectionsEntry[1] : 0),
backgroundColor: getConsistentHEXColor(subSectionsEntry[0]),
backgroundColor: getConsistentHEXColor(theme.value, subSectionsEntry[0]),
tooltip: `(${subSectionsEntry[0]}): ${aggregator[0][0]} = ${(isDurationAgg() ? Utils.humanDuration(subSectionsEntry[1]) : subSectionsEntry[1])}`,
}));
});

View File

@@ -25,7 +25,7 @@
import {computed, onMounted, ref, watch} from "vue";
import NoData from "../../../../layout/NoData.vue";
import Utils from "../../../../../utils/utils.js";
import Utils, {useTheme} from "../../../../../utils/utils.js";
import {Doughnut, Pie} from "vue-chartjs";
@@ -56,6 +56,8 @@
const isDuration = Object.values(props.chart.data.columns).find(c => c.agg !== undefined).field === "DURATION";
const theme = useTheme();
const options = computed(() => {
return defaultConfig({
plugins: {
@@ -77,13 +79,13 @@
}
},
},
});
}, theme.value);
});
const centerPlugin = {
const centerPlugin = computed(() => ({
id: "centerPlugin",
beforeDraw(chart) {
const darkTheme = Utils.getTheme() === "dark";
const darkTheme = theme.value === "dark";
const ctx = chart.ctx;
const dataset = chart.data.datasets[0];
@@ -106,7 +108,7 @@
ctx.restore();
},
};
}));
const thicknessPlugin = {
id: "thicknessPlugin",
@@ -157,7 +159,7 @@
const labels = Object.keys(results);
const dataElements = labels.map((label) => results[label]);
const backgroundColor = labels.map((label) => getConsistentHEXColor(label));
const backgroundColor = labels.map((label) => getConsistentHEXColor(theme.value, label));
const maxDataValue = Math.max(...dataElements);
const thicknessScale = dataElements.map(

View File

@@ -19,13 +19,14 @@
import {Bar} from "vue-chartjs";
import {customBarLegend} from "../legend.js";
import {defaultConfig, getConsistentHEXColor,} from "../../../../../utils/charts.js";
import {defaultConfig, getConsistentHEXColor} from "../../../../../utils/charts.js";
import {useStore} from "vuex";
import moment from "moment";
import {useRoute} from "vue-router";
import {Utils} from "@kestra-io/ui-libs";
import KestraUtils, {useTheme} from "../../../../../utils/utils.js"
const store = useStore();
@@ -49,6 +50,8 @@
.sort((a, b) => a[1].graphStyle.localeCompare(b[1].graphStyle));
const yBShown = aggregator.length === 2;
const theme = useTheme();
const DEFAULTS = {
display: true,
stacked: true,
@@ -119,7 +122,7 @@
},
}),
},
});
}, theme.value);
});
function isDuration(field) {
@@ -129,7 +132,7 @@
const parsedData = computed(() => {
const parseValue = (value) => {
const date = moment(value, moment.ISO_8601, true);
return date.isValid() ? date.format("YYYY-MM-DD") : value;
return date.isValid() ? date.format(KestraUtils.getDateFormat(route.query.startDate, route.query.endDate)) : value;
};
const rawData = generated.value.results;
@@ -165,6 +168,7 @@
tooltip: stack,
label: params[colorByColumn],
backgroundColor: getConsistentHEXColor(
theme.value,
params[colorByColumn],
),
unique: new Set(),
@@ -220,7 +224,7 @@
pointRadius: 0,
borderWidth: 0.75,
label: label,
borderColor: getConsistentHEXColor(label),
borderColor: getConsistentHEXColor(theme.value, label),
},
...yDatasetData,
]

View File

@@ -19,12 +19,12 @@
<div
class="d-flex justify-content-end align-items-center switch-content"
>
<span class="pe-2 fw-light small">{{ t("duration") }}</span>
<el-switch
v-model="duration"
:active-icon="Check"
inline-prompt
/>
<span class="d-flex align-items-center ps-2 fw-light small">{{ t("duration") }}</span>
</div>
<div id="executions" />
</div>
@@ -51,7 +51,7 @@
import {barLegend} from "../legend.js";
import Utils from "../../../../../utils/utils.js";
import Utils, {useTheme} from "../../../../../utils/utils.js";
import {defaultConfig, getFormat} from "../../../../../utils/charts.js";
import {getScheme} from "../../../../../utils/scheme.js";
@@ -73,13 +73,14 @@
},
});
const theme = useTheme()
const parsedData = computed(() => {
let datasets = props.data.reduce(function (accumulator, value) {
Object.keys(value.executionCounts).forEach(function (state) {
if (accumulator[state] === undefined) {
accumulator[state] = {
label: state,
backgroundColor: getScheme(state),
backgroundColor: getScheme(theme.value, state),
yAxisID: "y",
data: [],
};
@@ -286,9 +287,5 @@ $height: 200px;
.small {
font-size: 0.75rem;
}
.pe-2 {
padding-right: 0.5rem;
}
}
</style>

View File

@@ -0,0 +1,241 @@
<template>
<el-tooltip
effect="light"
placement="left"
:persistent="false"
:hide-after="0"
transition=""
:popper-class="tooltipContent === '' ? 'd-none' : 'tooltip-stats'"
:disabled="!externalTooltip"
:content="tooltipContent"
raw-content
>
<div>
<Bar
:class="small ? 'small' : ''"
:data="parsedData"
:options="options"
:total="total"
:plugins="plugins"
:duration="duration"
/>
</div>
</el-tooltip>
</template>
<script setup>
import {computed, ref} from "vue";
import {useI18n} from "vue-i18n";
import moment from "moment";
import {Bar} from "vue-chartjs";
import {useRouter} from "vue-router";
const router = useRouter();
import Utils, {useTheme} from "../../../../../utils/utils.js";
import {useScheme} from "../../../../../utils/scheme.js";
import {defaultConfig, tooltip, getFormat} from "../../../../../utils/charts.js";
const {t} = useI18n({useScope: "global"});
const props = defineProps({
data: {
type: Object,
required: true,
},
plugins: {
type: Array,
default: () => [],
},
total: {
type: Number,
default: undefined,
},
duration: {
type: Boolean,
default: true,
},
scales: {
type: Boolean,
default: true,
},
small: {
type: Boolean,
default: false,
},
externalTooltip: {
type: Boolean,
default: false,
},
});
const theme = useTheme()
const scheme = useScheme();
const tooltipContent = ref("")
const parsedData = computed(() => {
let datasets = props.data.reduce(function (accumulator, value) {
Object.keys(value.executionCounts).forEach(function (state) {
if (accumulator[state] === undefined) {
accumulator[state] = {
label: state,
backgroundColor: scheme.value[state],
yAxisID: "y",
data: [],
};
}
accumulator[state].data.push(value.executionCounts[state]);
});
return accumulator;
}, Object.create(null));
return {
labels: props.data.map((r) =>
moment(r.startDate).format(getFormat(r.groupBy)),
),
datasets: props.duration
? [
{
type: "line",
label: t("duration"),
fill: false,
pointRadius: 0,
borderWidth: 0.75,
borderColor: "#A2CDFF",
yAxisID: "yB",
data: props.data.map((value) => {
return value.duration.avg === 0
? 0
: Utils.duration(value.duration.avg);
}),
},
...Object.values(datasets),
]
: Object.values(datasets),
};
});
const options = computed(() =>
defaultConfig({
barThickness: props.small ? 8 : 12,
skipNull: true,
borderSkipped: false,
borderColor: "transparent",
borderWidth: 2,
plugins: {
barLegend: {
containerID: "executions",
},
tooltip: {
enabled: !props.externalTooltip,
filter: (value) => value.raw,
callbacks: {
label: function (value) {
const {label, yAxisID} = value.dataset;
return `${label.toLowerCase().capitalize()}: ${value.raw}${yAxisID === "yB" ? "s" : ""}`;
},
},
external: props.externalTooltip ? function (context) {
let content = tooltip(context.tooltip);
tooltipContent.value = content;
} : undefined,
},
},
scales: {
x: {
display: props.scales,
title: {
display: true,
text: t("date"),
},
grid: {
display: false,
},
position: "bottom",
stacked: true,
ticks: {
maxTicksLimit: props.small ? 5 : 8,
callback: function (value) {
const label = this.getLabelForValue(value);
if (
moment(label, ["h:mm A", "HH:mm"], true).isValid()
) {
// Handle time strings like "1:15 PM" or "13:15"
return moment(label, ["h:mm A", "HH:mm"]).format(
"h:mm A",
);
} else if (moment(new Date(label)).isValid()) {
// Handle date strings
const date = moment(new Date(label));
const isCurrentYear =
date.year() === moment().year();
return date.format(
isCurrentYear ? "MM/DD" : "MM/DD/YY",
);
}
// Return the label as-is if it's neither a valid date nor time
return label;
},
},
},
y: {
display: props.scales,
title: {
display: !props.small,
text: t("executions"),
},
grid: {
display: false,
},
position: "left",
stacked: true,
ticks: {
maxTicksLimit: props.small ? 5 : 8,
},
},
yB: {
title: {
display: props.duration && !props.small,
text: t("duration"),
},
grid: {
display: false,
},
display: props.duration,
position: "right",
ticks: {
maxTicksLimit: props.small ? 5 : 8,
callback: function (value) {
return `${this.getLabelForValue(value)}s`;
},
},
},
},
onClick: (e, elements) => {
if (elements.length > 0) {
const state = parsedData.value.datasets[elements[0].datasetIndex].label;
router.push({
name: "executions/list",
query: {
state: state,
scope: "USER",
size: 100,
page: 1,
},
});
}
},
}, theme.value),
);
</script>
<style>
.small{
height: 40px;
}
</style>

View File

@@ -25,14 +25,17 @@
<script setup>
import {computed} from "vue";
import {useI18n} from "vue-i18n";
import {useRouter} from "vue-router";
import {Doughnut} from "vue-chartjs";
import {totalsLegend} from "../legend.js";
import Utils from "../../../../../utils/utils.js";
import {useTheme} from "../../../../../utils/utils.js";
import {defaultConfig} from "../../../../../utils/charts.js";
import {getScheme} from "../../../../../utils/scheme.js";
import {useScheme} from "../../../../../utils/scheme.js";
const router = useRouter();
const scheme = useScheme();
import NoData from "../../../../layout/NoData.vue";
@@ -49,6 +52,8 @@
},
});
const theme = useTheme();
const parsedData = computed(() => {
let stateCounts = Object.create(null);
@@ -64,7 +69,7 @@
const labels = Object.keys(stateCounts);
const data = labels.map((state) => stateCounts[state]);
const backgroundColor = labels.map((state) => getScheme(state));
const backgroundColor = labels.map((state) => scheme.value[state]);
const maxDataValue = Math.max(...data);
const thicknessScale = data.map(
@@ -77,6 +82,8 @@
};
});
const options = computed(() =>
defaultConfig({
plugins: {
@@ -94,13 +101,28 @@
},
},
},
}),
onClick: (e, elements) => {
if (elements.length > 0) {
const index = elements[0].index;
const state = parsedData.value.labels[index];
router.push({
name: "executions/list",
query: {
state: state,
scope: "USER",
size: 100,
page: 1,
},
});
}
},
}, theme.value),
);
const centerPlugin = {
const centerPlugin = computed(() => ({
id: "centerPlugin",
beforeDraw(chart) {
const darkTheme = Utils.getTheme() === "dark";
const darkTheme = theme.value === "dark";
const ctx = chart.ctx;
const dataset = chart.data.datasets[0];
@@ -118,7 +140,7 @@
ctx.restore();
},
};
}));
const thicknessPlugin = {
id: "thicknessPlugin",

View File

@@ -31,13 +31,16 @@
<script setup>
import {computed} from "vue";
import {useI18n} from "vue-i18n";
import {useRouter} from "vue-router";
const router = useRouter();
import {Bar} from "vue-chartjs";
import {barLegend} from "../legend.js";
import {defaultConfig} from "../../../../../utils/charts.js";
import {getScheme} from "../../../../../utils/scheme.js";
import {useScheme} from "../../../../../utils/scheme.js";
import {useTheme} from "../../../../../utils/utils.js";
import NoData from "../../../../layout/NoData.vue";
@@ -54,6 +57,9 @@
},
});
const theme = useTheme();
const scheme = useScheme()
const parsedData = computed(() => {
const labels = Object.entries(props.data)
.sort(([, a], [, b]) => b.total - a.total)
@@ -71,7 +77,7 @@
executionData[state] = {
label: state,
data: [],
backgroundColor: getScheme(state),
backgroundColor: scheme.value[state],
stack: state,
};
}
@@ -125,7 +131,6 @@
position: "bottom",
display: true,
stacked: true,
ticks: {
callback: function(value) {
const namespaceName = this.getLabelForValue(value)
@@ -149,7 +154,21 @@
},
},
},
}),
onClick: (e, elements) => {
if (elements.length > 0) {
const state = parsedData.value.datasets[elements[0].datasetIndex].label;
router.push({
name: "executions/list",
query: {
state: state,
scope: "USER",
size: 100,
page: 1,
},
});
}
},
}, theme.value),
);
</script>
@@ -171,4 +190,4 @@ $height: 200px;
color: $gray-300;
}
}
</style>
</style>

View File

@@ -142,7 +142,7 @@ export const customBarLegend = {
};
const boxSpan = document.createElement("span");
const color = getConsistentHEXColor(item.text);
const color = getConsistentHEXColor(Utils.getTheme(), item.text);
boxSpan.style.background = color;
boxSpan.style.borderColor = "transparent";
boxSpan.style.height = "5px";

View File

@@ -33,10 +33,11 @@
import {barLegend} from "../legend.js";
import {defaultConfig, getFormat} from "../../../../../utils/charts.js";
import {getScheme} from "../../../../../utils/scheme.js";
import {useScheme} from "../../../../../utils/scheme.js";
import Logs from "../../../../../utils/logs.js";
import NoData from "../../../../layout/NoData.vue";
import {useTheme} from "../../../../../utils/utils.js";
const {t} = useI18n({useScope: "global"});
@@ -47,13 +48,16 @@
},
});
const theme = useTheme();
const scheme = useScheme("logs");
const parsedData = computed(() => {
let datasets = props.data.reduce(function (accumulator, value) {
Object.keys(value.counts).forEach(function (state) {
if (accumulator[state] === undefined) {
accumulator[state] = {
label: state,
backgroundColor: getScheme(state, "logs"),
backgroundColor: scheme.value[state],
yAxisID: "y",
data: [],
};
@@ -136,7 +140,7 @@
},
},
},
}),
}, theme.value),
);
</script>

View File

@@ -211,14 +211,6 @@ code {
.nextscheduled {
--el-table-tr-bg-color: var(--ks-background-body) !important;
background: var(--ks-background-body);
// FIXME: choose variables
& a {
color: #8e71f7;
html.dark & {
color: #e0e0fc;
}
}
}
.next-toggle {

View File

@@ -68,10 +68,10 @@
z-index: -2;
background-image: linear-gradient(138.8deg, #CCE8FE 0%, #CDA0FF 27.03%, #8489F5 41.02%, #CDF1FF 68.68%, #B591E9 94%, #CCE8FE 100%);
background-size: 200% 200%;
top: -2px;
bottom: -2px;
left: -2px;
right: -2px;
top: 0px;
bottom: 0px;
left: 0px;
right: 0px;
animation: move-border 3s linear infinite;
}
@@ -79,11 +79,11 @@
.enterprise-tag::after{
z-index: -1;
background: $base-gray-200;
top: -1px;
left: -1px;
bottom: -1px;
right: -1px;
background: $base-gray-100;
top: 1px;
left: 1px;
bottom: 1px;
right: 1px;
html.dark & {
background: $base-gray-400;
}
@@ -92,14 +92,12 @@
.enterprise-tag{
position: relative;
background: $base-gray-200;
border: 1px solid transparent;
padding: 0 1rem;
padding: .125rem 1rem;
border-radius: 1rem;
display: inline-block;
z-index: 2;
html.dark &{
background: #FBFBFB26;
border-color: #FFFFFF;
}
.flare{
display: none;

View File

@@ -244,10 +244,7 @@
}
:deep(.doc-alert) {
border: 1px solid var(--ks-border-info);
border-radius: 4px;
color: var(--ks-content-info);
background: var(--ks-background-info);
padding-bottom: 1px !important;
}
}
</style>

View File

@@ -159,6 +159,7 @@
{
name: "auditlogs",
title: title("auditlogs"),
maximized: true,
locked: true
}
];

View File

@@ -46,6 +46,14 @@
refresh: {shown: true, callback: refresh},
settings: {shown: true, charts: {shown: true, value: showChart, callback: onShowChartChange}}
}"
:properties-width="182"
:properties="{
shown: true,
columns: optionalColumns,
displayColumns,
storageKey: 'executions'
}"
@update-properties="updateDisplayColumns"
/>
</template>
@@ -247,6 +255,7 @@
<el-table-column
prop="flowRevision"
v-if="displayColumn('revision')"
:label="$t('revision')"
class-name="shrink"
>
@@ -291,7 +300,11 @@
</template>
</el-table-column>
<el-table-column column-key="action" class-name="row-action">
<el-table-column
column-key="action"
class-name="row-action"
:label="$t('actions')"
>
<template #default="scope">
<router-link
:to="{name: 'executions/update', params: {namespace: scope.row.namespace, flowId: scope.row.flowId, id: scope.row.id}, query: {revision: scope.row.flowRevision}}"
@@ -460,57 +473,52 @@
showChart: ["true", null].includes(localStorage.getItem(storageKeys.SHOW_CHART)),
optionalColumns: [
{
label: "start date",
label: this.$t("start date"),
prop: "state.startDate",
default: true
},
{
label: "end date",
label: this.$t("end date"),
prop: "state.endDate",
default: true
},
{
label: "duration",
label: this.$t("duration"),
prop: "state.duration",
default: true
},
{
label: "state",
prop: "state.current",
default: true
},
{
label: "triggers",
prop: "triggers",
default: true
},
{
label: "labels",
prop: "labels",
default: true
},
{
label: "inputs",
prop: "inputs",
default: false
},
{
label: "namespace",
label: this.$t("namespace"),
prop: "namespace",
default: true
},
{
label: "flow",
label: this.$t("flow"),
prop: "flowId",
default: true
},
{
label: "revision",
label: this.$t("labels"),
prop: "labels",
default: true
},
{
label: this.$t("state"),
prop: "state.current",
default: true
},
{
label: this.$t("revision"),
prop: "flowRevision",
default: false
},
{
label: "task id",
label: this.$t("inputs"),
prop: "inputs",
default: false
},
{
label: this.$t("task id"),
prop: "taskRunList.taskId",
default: false
}
@@ -532,7 +540,7 @@
this.storageKey = storageKeys.DISPLAY_FLOW_EXECUTIONS_COLUMNS;
this.optionalColumns = this.optionalColumns.filter(col => col.prop !== "namespace" && col.prop !== "flowId")
}
this.displayColumns = localStorage.getItem(this.storageKey)?.split(",")
this.displayColumns = localStorage.getItem("columns_executions")?.split(",")
|| this.optionalColumns.filter(col => col.default).map(col => col.prop);
if (this.isConcurrency) {
this.emitStateCount([State.RUNNING, State.PAUSED])
@@ -645,6 +653,9 @@
displayColumn(column) {
return this.hidden ? !this.hidden.includes(column) : this.displayColumns.includes(column);
},
updateDisplayColumns(newColumns) {
this.displayColumns = newColumns;
},
onShowChartChange(value) {
this.showChart = value;
localStorage.setItem(storageKeys.SHOW_CHART, value);
@@ -834,18 +845,18 @@
h(ElSwitch, {
modelValue: includeNonTerminated.value,
"onUpdate:modelValue": (val) => {
includeNonTerminated.value = val
includeNonTerminated.value = val;
},
}),
]),
h(ElAlert, {
title: this.$t("execution-warn-title"),
includeNonTerminated.value ? h(ElAlert, {
title: this.$t("execution-warn-title"),
description: this.$t("execution-warn-deleting-still-running"),
type: "warning",
showIcon: true,
closable: false,
class: "custom-warning"
}),
}) : null,
h(ElCheckbox, {
modelValue: deleteLogs.value,
label: this.$t("execution_deletion.logs"),
@@ -876,7 +887,7 @@
"execution/queryDeleteExecution",
"execution/bulkDeleteExecution",
"executions deleted"
)
);
});
},
killExecutions() {

View File

@@ -95,7 +95,7 @@
<duration :histories="scope.row.value" />
</span>
<span v-else-if="scope.row.key === $t('labels')">
<labels :labels="scope.row.value" :filter-enabled="false" />
<labels :labels="scope.row.value" read-only />
</span>
<span v-else>
<span v-if="scope.row.key === $t('revision')">

View File

@@ -215,7 +215,7 @@
const taskRunList = [...execution.value.taskRunList];
return taskRunList.find((e) => e.taskId === filter);
};
const onDebugExpression = (expression) => {
const onDebugExpression = (expression: string) => {
const taskRun = selectedTask();
if (!taskRun) return;
@@ -236,7 +236,7 @@
debugExpression.value = response.data.result;
// Parsing failed, therefore, copy raw result
if (response.status === 200)
if (response.status === 200 && response.data.result)
selected.value.push(response.data.result);
}

View File

@@ -4,12 +4,13 @@
<el-select
ref="select"
:model-value="current"
:model-value="currentFilters"
value-key="label"
:placeholder="props.placeholder ?? t('filters.label')"
default-first-option
allow-create
filterable
:filter-method="(f) => (prefixFilter = f.toLowerCase())"
clearable
multiple
placement="bottom"
@@ -20,12 +21,13 @@
@keyup="(e) => handleInputChange(e.key)"
@keyup.enter="() => handleEnterKey(select?.hoverOption?.value)"
@remove-tag="(item) => removeItem(item)"
@visible-change="(visible) => dropdownClosedCallback(visible)"
@visible-change="(visible) => dropdownToggleCallback(visible)"
@clear="handleClear"
:class="{
refresh: buttons.refresh.shown,
settings: buttons.settings.shown,
dashboards: dashboards.shown,
properties: properties.shown,
}"
@focus="handleFocus"
data-test-id="KestraFilter__select"
@@ -60,12 +62,13 @@
</template>
<template v-else-if="dropdowns.second.shown">
<el-option
v-for="(comparator, index) in dropdowns.first.value.comparators"
v-for="(comparator, index) in dropdowns.first.value
.comparators"
:key="comparator.value"
:value="comparator"
:label="comparator.label"
:class="{
selected: current.some(
selected: currentFilters.some(
(c) => c.comparator === comparator,
),
}"
@@ -75,16 +78,15 @@
</template>
<template v-else-if="dropdowns.third.shown">
<el-option
v-for="(filter, index) in valueOptions"
v-for="(filter, index) in prefixFilteredValueOptions"
:key="filter.value"
:value="filter"
:disabled="isOptionDisabled(filter)"
:class="{
selected: current.some((c) =>
c.value.includes(filter.value),
),
selected: currentFilters
.at(-1)
?.value?.includes(filter.value),
disabled: isOptionDisabled(filter),
'level-3': true
'level-3': true,
}"
@click="
() => !isOptionDisabled(filter) && valueCallback(filter)
@@ -92,7 +94,10 @@
:data-test-id="`KestraFilter__value__${index}`"
>
<template v-if="filter.label.component">
<component :is="filter.label.component" v-bind="filter.label.props" />
<component
:is="filter.label.component"
v-bind="filter.label.props"
/>
</template>
<template v-else>
{{ filter.label }}
@@ -107,7 +112,8 @@
'me-1':
buttons.refresh.shown ||
buttons.settings.shown ||
dashboards.shown,
dashboards.shown ||
properties.shown,
}"
>
<KestraIcon :tooltip="$t('search')" placement="bottom">
@@ -117,13 +123,17 @@
class="rounded-0"
/>
</KestraIcon>
<Save :disabled="!current.length" :prefix="ITEMS_PREFIX" :current />
<Save
:disabled="!currentFilters.length"
:prefix="ITEMS_PREFIX"
:current="currentFilters"
/>
</el-button-group>
<el-button-group
v-if="buttons.refresh.shown || buttons.settings.shown"
class="d-inline-flex ms-1"
:class="{'me-1': dashboards.shown}"
:class="{'me-1': dashboards.shown || properties.shown}"
>
<Refresh
v-if="buttons.refresh.shown"
@@ -141,14 +151,22 @@
@dashboard="(value) => emits('dashboard', value)"
class="ms-1"
/>
<Properties
v-if="properties.shown"
:columns="properties.columns"
:model-value="properties.displayColumns"
:storage-key="properties.storageKey"
@update-properties="(v) => emits('updateProperties', v)"
class="ms-1"
/>
</section>
</template>
<script setup lang="ts">
import {ref, computed, onMounted, watch, nextTick, shallowRef} from "vue";
import {computed, nextTick, onMounted, ref, shallowRef, watch} from "vue";
import {ElSelect} from "element-plus";
import {Shown, Buttons, CurrentItem} from "./utils/types";
import {Buttons, CurrentItem, Shown, Pair, Property} from "./utils/types";
import Refresh from "../layout/RefreshButton.vue";
import Items from "./segments/Items.vue";
@@ -156,28 +174,36 @@
import Save from "./segments/Save.vue";
import Settings from "./segments/Settings.vue";
import Dashboards from "./segments/Dashboards.vue";
import Properties from "./segments/Properties.vue";
import KestraIcon from "../Kicon.vue";
import DateRange from "../layout/DateRange.vue";
import Status from "../../components/Status.vue";
import Status from "./components/Status.vue";
import {Magnify} from "./utils/icons";
import {useI18n} from "vue-i18n";
import {useStore} from "vuex";
import {useRoute, useRouter} from "vue-router";
import {useFilters} from "./composables/useFilters";
import action from "../../models/action.js";
import permission from "../../models/permission.js";
import {useValues} from "./composables/useValues";
import {decodeParams, encodeParams} from "./utils/helpers";
const {t} = useI18n({useScope: "global"});
import {useStore} from "vuex";
const store = useStore();
import {useRouter, useRoute} from "vue-router";
const router = useRouter();
const route = useRoute();
const emits = defineEmits(["dashboard", "input"]);
const emits = defineEmits(["dashboard", "input", "updateProperties"]);
const props = defineProps({
prefix: {type: String, default: undefined},
include: {type: Array, default: () => []},
values: {type: Object, default: undefined},
decode: {type: Boolean, default: true},
propertiesWidth: {type: Number, default: 144},
buttons: {
type: Object as () => Buttons,
default: () => ({
@@ -192,18 +218,33 @@
type: Object as () => Shown,
default: () => ({shown: false}),
},
properties: {
type: Object as () => Property,
default: () => ({shown: false}),
},
placeholder: {type: String, default: undefined},
searchCallback: {type: Function, default: undefined},
});
const ITEMS_PREFIX = props.prefix ?? String(route.name);
import {useFilters} from "./composables/useFilters";
const {COMPARATORS, OPTIONS} = useFilters(ITEMS_PREFIX);
const prefixFilteredValueOptions = computed(() => {
if (prefixFilter.value === "") {
return valueOptions.value;
}
return valueOptions.value.filter((o) =>
o.label.toLowerCase().startsWith(prefixFilter.value),
);
});
const select = ref<InstanceType<typeof ElSelect> | null>(null);
const updateHoveringIndex = (index) => {
select.value!.states.hoveringIndex = index >= 0 ? index : 0;
select.value!.states.hoveringIndex = undefined;
nextTick(() => {
select.value!.states.hoveringIndex = Math.max(index, 0);
});
};
const emptyLabel = ref(t("filters.empty"));
const INITIAL_DROPDOWNS = {
@@ -239,6 +280,8 @@
} else if (dropdowns.value.third.shown) {
valueCallback(option);
}
prefixFilter.value = "";
};
const getInputValue = () => select.value?.states.inputValue;
@@ -250,20 +293,20 @@
if (key === "Enter") return;
if (current.value.at(-1)?.label === "user") {
if (currentFilters.value.at(-1)?.label === "user") {
emits("input", getInputValue());
}
};
const handleClear = () => {
current.value = [];
currentFilters.value = [];
triggerSearch();
};
const activeParentFilter = ref<string | null>(null);
const lastClickedParent = ref<string | null>(null);
const showSubFilterDropdown = ref(false);
const valueOptions = ref([]);
const valueOptions = ref<Pair[]>([]);
const parentValue = ref<string | null>(null);
const filterCallback = (option) => {
@@ -279,7 +322,7 @@
};
// Check if parent filter already exists
const existingFilterIndex = current.value.findIndex(
const existingFilterIndex = currentFilters.value.findIndex(
(item) => item.label === option.value.label,
);
if (existingFilterIndex !== -1) {
@@ -296,8 +339,11 @@
} else {
// If it doesn't exist, push new filter
dropdowns.value.first = {shown: false, value: option};
dropdowns.value.second = {shown: true, index: current.value.length};
current.value.push(option.value);
dropdowns.value.second = {
shown: true,
index: currentFilters.value.length,
};
currentFilters.value.push(option.value);
activeParentFilter.value = option.value.label;
lastClickedParent.value = option.value.label;
parentValue.value = option.value.label;
@@ -309,9 +355,9 @@
}
};
const comparatorCallback = (value) => {
current.value[dropdowns.value.second.index].comparator = value;
currentFilters.value[dropdowns.value.second.index].comparator = value;
emptyLabel.value = ["labels", "details"].includes(
current.value[dropdowns.value.second.index].label,
currentFilters.value[dropdowns.value.second.index].label,
)
? t("filters.format")
: t("filters.empty");
@@ -319,34 +365,29 @@
dropdowns.value = {
first: {shown: false, value: {}},
second: {shown: false, index: -1},
third: {shown: true, index: current.value.length - 1},
third: {shown: true, index: currentFilters.value.length - 1},
};
// Set hover index to the selected comparator for highlighting
const index = valueOptions.value.findIndex((o) => o.value === value.value);
updateHoveringIndex(index);
updateHoveringIndex(0);
};
const dropdownClosedCallback = (visible) => {
const dropdownToggleCallback = (visible) => {
if (!visible) {
dropdowns.value = {...INITIAL_DROPDOWNS};
activeParentFilter.value = null;
lastClickedParent.value = null;
showSubFilterDropdown.value = false;
// If last filter item selection was not completed, remove it from array
if (current.value?.at(-1)?.value?.length === 0) current.value.pop();
if (currentFilters.value?.at(-1)?.value?.length === 0)
currentFilters.value.pop();
} else {
// Highlight all selected items by setting hoveringIndex to match the first selected item
const index = valueOptions.value.findIndex((o) => {
return current.value.some((c) => c.value.includes(o.value));
});
updateHoveringIndex(index);
updateHoveringIndex(0);
}
};
const isOptionDisabled = () => {
if (!activeParentFilter.value) return false;
const parentIndex = current.value.findIndex(
const parentIndex = currentFilters.value.findIndex(
(item) => item.label === activeParentFilter.value,
);
if (parentIndex === -1) return false;
@@ -355,38 +396,36 @@
// Don't do anything if the option is disabled
if (isOptionDisabled(filter)) return;
if (!isDate) {
const parentIndex = current.value.findIndex(
const parentIndex = currentFilters.value.findIndex(
(item) => item.label === parentValue.value,
);
if (parentIndex !== -1) {
if (
lastClickedParent.value === "Namespace" ||
lastClickedParent.value === "namespace" ||
lastClickedParent.value === "Log level"
["namespace", "log level"].includes(
lastClickedParent.value.toLowerCase(),
)
) {
const values = current.value[parentIndex].value;
const values = currentFilters.value[parentIndex].value;
const index = values.indexOf(filter.value);
if (index === -1) {
current.value[parentIndex].value = [filter.value]; // Add only the filter.value
currentFilters.value[parentIndex].value = [filter.value]; // Add only the filter.value
} else {
current.value[parentIndex].value = values.filter(
currentFilters.value[parentIndex].value = values.filter(
(value, i) => i !== index,
); // remove the clicked item
}
} else {
const values = current.value[parentIndex].value;
const values = currentFilters.value[parentIndex].value;
const index = values.indexOf(filter.value);
if (index === -1) values.push(filter.value);
else values.splice(index, 1);
}
const hoverIndex = valueOptions.value.findIndex(
(o) => o.value === filter.value,
);
updateHoveringIndex(hoverIndex);
}
} else {
const match = current.value.find((v) => v.label === "absolute_date");
const match = currentFilters.value.find(
(v) => v.label === "absolute_date",
);
if (match) {
match.value = [
{
@@ -397,16 +436,15 @@
}
}
if (!current.value[dropdowns.value.third.index].comparator?.multiple) {
if (
!currentFilters.value[dropdowns.value.third.index].comparator?.multiple
) {
// If selection is not multiple, close the dropdown
closeDropdown();
}
triggerSearch();
};
import action from "../../models/action.js";
import permission from "../../models/permission.js";
const user = computed(() => store.state.auth.user);
const namespaceOptions = ref([]);
@@ -438,11 +476,10 @@
// Load all namespaces only if that filter is included
if (props.include.includes("namespace")) loadNamespaces();
import {useValues} from "./composables/useValues";
const {VALUES} = useValues(ITEMS_PREFIX);
const isDatePickerShown = computed(() => {
return current?.value?.some(
return currentFilters?.value?.some(
(c) => c.label === "absolute_date" && c.comparator,
);
});
@@ -462,18 +499,15 @@
break;
case "state":
valueOptions.value = (props.values?.state || VALUES.EXECUTION_STATES).
map(value => {
value.label = {
"component": shallowRef(Status),
"props": {
"class": "justify-content-center",
"status": value.value,
"size": "small"
}
}
return value;
});
valueOptions.value = (
props.values?.state || VALUES.EXECUTION_STATES
).map((value) => {
value.label = {
component: shallowRef(Status),
props: {status: value.value},
};
return value;
});
break;
case "trigger_state":
@@ -541,40 +575,67 @@
break;
}
};
const current = ref<CurrentItem[]>([]);
const currentFilters = ref<CurrentItem[]>([]);
watch(
() => route.query,
(q: any) => {
// Handling change of label filters from direct click events
const routeFilters = decodeParams(route.path, q, props.include, OPTIONS);
currentFilters.value = routeFilters;
},
{immediate: true},
);
const prefixFilter = ref("");
const includedOptions = computed(() => {
const dates = ["relative_date", "absolute_date"];
const found = current.value?.find((v) => dates.includes(v?.label));
const found = currentFilters.value?.find((v) => dates.includes(v?.label));
const exclude = found ? dates.find((date) => date !== found.label) : null;
return OPTIONS.filter((o) => {
const label = o.value?.label;
return props.include.includes(label) && label !== exclude;
return (
props.include.includes(label) &&
label !== exclude &&
label.startsWith(prefixFilter.value)
);
});
});
const changeCallback = (v) => {
if (!Array.isArray(v) || !v.length) return;
const changeCallback = (wholeSearchContent) => {
if (!Array.isArray(wholeSearchContent) || !wholeSearchContent.length)
return;
if (typeof v.at(-1) === "string") {
if (["labels", "details"].includes(v.at(-2)?.label)) {
// Adding labels to proper filter
v.at(-2).value?.push(v.at(-1));
closeDropdown();
triggerSearch();
if (typeof wholeSearchContent.at(-1) === "string") {
if (
["labels", "details"].includes(wholeSearchContent.at(-2)?.label) ||
wholeSearchContent.at(-2)?.value?.length === 0
) {
// Adding value to preceding empty filter
// TODO Provide a way for user to escape infinite labels & details loop (you can never fallback to a new filter, any further text will be added as a value to the filter)
wholeSearchContent.at(-2)?.value?.push(wholeSearchContent.at(-1));
} else {
// Adding text search string
const label = t("filters.options.text");
const index = current.value.findIndex((i) => i.label === label);
const index = currentFilters.value.findIndex(
(i) => i.label === label,
);
if (index !== -1) current.value[index].value = [v.at(-1)];
else current.value.push({label, value: [v.at(-1)]});
triggerSearch();
closeDropdown();
if (index !== -1)
currentFilters.value[index].value = [wholeSearchContent.at(-1)];
else
currentFilters.value.push({
label,
value: [wholeSearchContent.at(-1)],
});
}
triggerSearch();
closeDropdown();
triggerEnter.value = false;
}
@@ -583,7 +644,7 @@
};
const removeItem = (value) => {
current.value = current.value.filter(
currentFilters.value = currentFilters.value.filter(
(item) => JSON.stringify(item) !== JSON.stringify(value),
);
@@ -591,22 +652,20 @@
};
const handleClickedItems = (value) => {
if (value) current.value = value;
if (value) currentFilters.value = value;
select.value?.focus();
};
import {encodeParams, decodeParams} from "./utils/helpers";
const triggerSearch = () => {
if (props.searchCallback) return;
else router.push({query: encodeParams(current.value, OPTIONS)});
else router.push({query: encodeParams(currentFilters.value, OPTIONS)});
};
// Include parameters from URL directly to filter
onMounted(() => {
if (props.decode) {
const decodedParams = decodeParams(route.query, props.include, OPTIONS);
current.value = decodedParams.map((item: any) => {
currentFilters.value = decodedParams.map((item: any) => {
if (item.label === "absolute_date") {
return {
...item,
@@ -635,21 +694,21 @@
const addNamespaceFilter = (namespace) => {
if (!props.decode || !namespace) return;
current.value.push({
currentFilters.value.push({
label: "namespace",
value: [namespace],
comparator: COMPARATORS.STARTS_WITH,
persistent: true,
});
};
const {name, params} = route;
const {name, params, query} = route;
if (name === "flows/update") {
// Single flow page
addNamespaceFilter(params?.namespace);
if (props.decode && params.id) {
current.value.push({
currentFilters.value.push({
label: "flow",
value: [`${params.id}`],
comparator: COMPARATORS.IS,
@@ -659,6 +718,24 @@
} else if (name === "namespaces/update") {
// Single namespace page
addNamespaceFilter(params.id);
} else if (name === "admin/triggers") {
if(query.namespace) addNamespaceFilter(query.namespace);
if(query.flowId){
currentFilters.value.push({
label: "flow",
value: [`${query.flowId}`],
comparator: COMPARATORS.EQUALS,
persistent: true,
});
}
if(query.q) {
currentFilters.value.push({
label: "text",
value: [`${query.q}`],
comparator: COMPARATORS.EQUALS,
persistent: true,
});
}
}
});
@@ -675,12 +752,12 @@
);
const handleFocus = () => {
if (current.value.length > 0 && lastClickedParent.value) {
const existingFilterIndex = current.value.findIndex(
if (currentFilters.value.length > 0 && lastClickedParent.value) {
const existingFilterIndex = currentFilters.value.findIndex(
(item) => item.label === lastClickedParent.value,
);
if (existingFilterIndex !== -1) {
if (!current.value[existingFilterIndex].comparator) {
if (!currentFilters.value[existingFilterIndex].comparator) {
dropdowns.value = {
first: {shown: false, value: {}},
second: {shown: true, index: existingFilterIndex},
@@ -741,7 +818,7 @@
const label = labelElement?.textContent;
if (label) {
const existingFilterIndex = current.value.findIndex(
const existingFilterIndex = currentFilters.value.findIndex(
(item) =>
item?.label.toLowerCase() ===
label
@@ -757,7 +834,10 @@
.replace(/\blog\b/gi, "")
.trim()
.replace(/\s+/g, "_"); // Set parentValue when a filter is clicked
if (!current.value[existingFilterIndex].comparator) {
if (
!currentFilters.value[existingFilterIndex]
.comparator
) {
dropdowns.value = {
first: {shown: false, value: {}},
second: {
@@ -801,6 +881,7 @@ $included: 144px;
$refresh: 104px;
$settins: 52px;
$dashboards: 52px;
$properties: v-bind('props.propertiesWidth + "px"');
.filters {
@include width-available;
@@ -808,6 +889,13 @@ $dashboards: 52px;
& .el-select {
width: 100%;
&.refresh.settings.dashboards.properties {
max-width: calc(
100% - $included - $refresh - $settins - $dashboards -
#{$properties}
);
}
&.refresh.settings.dashboards {
max-width: calc(
100% - $included - $refresh - $settins - $dashboards
@@ -822,10 +910,22 @@ $dashboards: 52px;
max-width: calc(100% - $included - $settins - $dashboards);
}
&.settings.properties {
max-width: calc(100% - $included - $settins - #{$properties});
}
&.refresh.dashboards {
max-width: calc(100% - $included - $refresh - $dashboards);
}
&.refresh.properties {
max-width: calc(100% - $included - $refresh - #{$properties});
}
&.dashboards.properties {
max-width: calc(100% - $included - $dashboards - #{$properties});
}
&.refresh {
max-width: calc(100% - $included - $refresh);
}
@@ -835,8 +935,13 @@ $dashboards: 52px;
}
&.dashboards {
min-width: $dashboards;
max-width: calc(100% - $included - $dashboards);
}
&.properties {
max-width: calc(100% - $included - #{$properties});
}
}
& .el-select__placeholder {
@@ -872,6 +977,7 @@ $dashboards: 52px;
.filters-select {
& .el-select-dropdown {
width: auto !important;
max-width: 300px;
&:has(.el-select-dropdown__empty) {
width: auto !important;

View File

@@ -0,0 +1,25 @@
<template>
<div class="d-flex align-items-center cursor-pointer">
<div :style class="circle" />
<span>{{ $filters.cap(status) }}</span>
</div>
</template>
<script setup>
import {computed} from "vue";
const props = defineProps({status: {type: String, required: true}});
const style = computed(() => ({
backgroundColor: `var(--ks-chart-${props.status.toLowerCase()})`,
}));
</script>
<style scoped lang="scss">
.circle {
width: 6px;
height: 6px;
border-radius: 50%;
margin-right: 8px;
}
</style>

View File

@@ -1,8 +1,8 @@
<template>
<el-dropdown trigger="click" placement="bottom-end">
<KestraIcon placement="bottom">
<el-button :icon="Menu">
{{ selectedDashboard ?? $t('default_dashboard') }}
<el-button :icon="Menu" class="main-button">
<span class="text-truncate">{{ selectedDashboard ?? $t('default_dashboard') }}</span>
</el-button>
</KestraIcon>
@@ -141,4 +141,12 @@
.items {
max-height: 160px !important; // 5 visible items
}
.main-button {
max-width: 300px;
span {
max-width: 250px;
}
}
</style>

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