Compare commits

...

107 Commits

Author SHA1 Message Date
brian.mulier
8f4a9e2dc8 chore: upgrade to version 0.19.7 2024-10-30 13:41:39 +01:00
brian.mulier
911ae32113 fix: better log display and combine temporal & taskruns view log navigation handling 2024-10-30 13:39:51 +01:00
brian.mulier
928214a22b fix: gantt view was no longer fetching logs 2024-10-30 13:39:48 +01:00
brian.mulier
d5eafa69aa chore: upgrade to version 0.19.6 2024-10-30 10:28:44 +01:00
Sachin
ae48352300 fix(ui): log navigation is now working in temporal view (#5685)
closes #5215
2024-10-30 10:23:58 +01:00
Loïc Mathieu
a59f758d28 feat(core,jdbc): add message protection metric 2024-10-25 12:24:23 +02:00
Mradul Vishwakarma
befbefbdd9 feat(ui): only show columns with data on triggers page (#5555) 2024-10-24 13:42:03 +02:00
Miloš Paunović
57c7389f9e fix(ui): prevent validation errors showing if inputs are empty (#5648) 2024-10-24 12:27:36 +02:00
AbdurRahman2004
427da64744 chore(ui): amend table link colors on main dashboard (#5638) 2024-10-23 23:12:49 +02:00
Abhishek Khairnar
430dc8ecee chore(ui): add background color to every namespace on listing (#5603) 2024-10-23 13:34:49 +02:00
YannC
2d9c98b921 chore: upgrade to version 0.19.5 2024-10-22 16:07:21 +02:00
YannC
a49b406f03 fix(core): encode filename (#5593)
close #5589
2024-10-22 16:06:04 +02:00
YannC
2a578fe651 fix(core): missing method 2024-10-22 15:58:48 +02:00
Loïc Mathieu
277bf77fb4 fix(core): serialize default inputs
Fixes https://github.com/kestra-io/kestra-ee/issues/1887
2024-10-22 09:09:07 +02:00
Harsh4902
e6ec59443a fixes #5459 used HashMap instead of Map to accept null values.
Signed-off-by: Harsh4902 <harshparmar4902@gmail.com>
2024-10-18 15:25:31 +02:00
Loïc Mathieu
b68b281ac0 fix(webserver): don't load the flow too early so a user with only EXECUTION permission can access execution files
Fixes https://github.com/kestra-io/kestra/issues/4958
2024-10-18 15:24:10 +02:00
Loïc Mathieu
37bf6ea8f3 fix(core): decrypt additional render variables
Fixes https://github.com/kestra-io/plugin-kubernetes/issues/150
2024-10-18 15:24:04 +02:00
brian.mulier
71a296a814 fix(core): better error message in case of docker socket not found / not accessible
closes #5524
2024-10-18 14:39:15 +02:00
brian.mulier
6d8bc07f5b fix(doc): CardLogos.vue had wrong URL
closes kestra-io/docs#1808
2024-10-18 14:39:11 +02:00
Florian Hussonnois
07974aa145 fix(core): fix inputs for execution resume (#5494)
fix: #5494
2024-10-18 11:54:05 +02:00
YannC
c7288bd325 chore: upgrade to version 0.19.4 2024-10-18 11:08:46 +02:00
YannC
96f553c1ba fix(ui): missing translation 2024-10-18 11:08:46 +02:00
YannC
8389102706 fix(core): correctly cast input to FLOAT for subflows (#5539)
close #5535
2024-10-18 09:29:44 +02:00
MilosPaunovic
16a0096c45 chore(ui): remove the unnecessary file for this version 2024-10-17 09:53:33 +02:00
Varsha U N
6327dcd51b chore(ui): increase the in-app docs scrollbar width (#5473) 2024-10-17 09:50:45 +02:00
Purandhar Adigarla
e91beaa15f chore(ui): add an extra space between icon and label inside a button (#5507)
Co-authored-by: PurandharAdigarla <purandharadigarla.com>
2024-10-17 09:06:10 +02:00
Sachin
833bdb38ee fix(ui): hide pagination when no flows results data available (#5501)
Co-authored-by: Sachin KS <mac@apples-MacBook-Air.local>
2024-10-17 09:05:58 +02:00
GitHub Action
0d64c74a67 chore(translations): auto generate values for languages other than english 2024-10-17 09:05:48 +02:00
Sachin
4740fa3628 chore(ui): update no logs message for flows source search
Co-authored-by: Sachin KS <mac@apples-MacBook-Air.local>
2024-10-17 09:04:38 +02:00
Loïc Mathieu
b29965c239 chore: version 0.19.3 2024-10-15 14:08:05 +02:00
Florian Hussonnois
05d1eeadef refactor(core): move to reactor for handling execution inputs (#5383)
related-to: #5383
2024-10-15 12:05:29 +02:00
Florian Hussonnois
acd2ce9041 fix(core): fix do not upload file when validating inputs (#5399)
Fix: #5399
2024-10-15 12:04:35 +02:00
Malay Dewangan
a3829c3d7e fix(core): OutputValues to support arrays and complex objects (#5440) 2024-10-14 14:06:10 +02:00
Sachin
17c18f94dd chore(ui): use standard graphs across all pages (#5443) 2024-10-14 12:43:35 +02:00
MITHIN DEV
14daa96295 chore(ui): make bar chart more responsive on smaller screens (#5439)
Signed-off-by: mithindev <mithindev1@gmail.com>
2024-10-14 12:43:29 +02:00
yuri
aa9aa80f0a feat(ui): allow searching read-only inputs (#5427) 2024-10-14 12:43:20 +02:00
Sachin
705d17340d fix(ui): prevent tab title change on cancelling unsaved changes (#5435)
Co-authored-by: Sachin KS <mac@apples-MacBook-Air.local>
2024-10-14 12:43:13 +02:00
Sachin
cf70c99e59 fix(ui): resolve issue preventing flow creation (#5444)
Co-authored-by: Sachin KS <mac@apples-MacBook-Air.local>
2024-10-14 12:43:07 +02:00
Miloš Paunović
c5e0cddca5 chore(ui): add filters for flow logs (#5446) 2024-10-14 12:42:59 +02:00
Loïc Mathieu
4d14464191 fix(cli): incorrect JDBC conf 2024-10-11 14:31:21 +02:00
Miloš Paunović
ed12797b46 chore(ui): check if tab exists before setting dirty attribute on it (#5423) 2024-10-11 12:36:02 +02:00
GitHub Action
ec85a748ce chore(translations): auto generate values for languages other than english 2024-10-11 11:50:00 +02:00
Mohammed Viqar Ahmed
3e8a63888a feat(ui): add option to delete multiple kv pairs at once (#5413) 2024-10-11 11:49:52 +02:00
Jonnadula Chaitanya
8d0bcc1da3 fix(ui): take default namespace into account on filters (#5406) 2024-10-11 11:37:04 +02:00
Loïc Mathieu
0b53f1cf25 feat(core): ForEachItme avoid checking all split file for existance but list them 2024-10-10 18:01:34 +02:00
Loïc Mathieu
3621aad6a1 fix(core): incorrect duration metric computed on the Worker 2024-10-10 18:01:26 +02:00
Miloš Paunović
dbb1cc5007 fix(ui): amend duplication the lables field on execution run (#5405) 2024-10-10 11:52:38 +02:00
Miloš Paunović
0d6e655b22 chore(ui): filter out system namespace from namespace select filter (#5403) 2024-10-10 10:57:30 +02:00
riya mustare
7a1a180fdb Pre-fill namespace from current filter (#5398) 2024-10-10 09:42:22 +02:00
Miloš Paunović
ce2daf52ff fix(ui): make sure disable toggle for triggers of next executions works every time (#5397) 2024-10-10 09:08:56 +02:00
Mohammed Viqar Ahmed
f086da3a2a chore(docs): add section about javascript memory heap out error in contributing guide (#5392)
* CONTRIBUTING.md : addind node options variable

* chore(ui): improve wording of contribution guide

---------

Co-authored-by: Miloš Paunović <paun992@hotmail.com>
2024-10-10 08:44:19 +02:00
Ahmad Midlaj B
1886a443c7 fix(ui): make execution replay dialog description readable in dark theme (#5360)
* fix(ui): make text readable in dark theme

* chore(ui): revert package-lock.json file to initial state

---------

Co-authored-by: MilosPaunovic <paun992@hotmail.com>
2024-10-10 08:42:28 +02:00
riya mustare
5a4e2b791d aligned relative filter (#5388) 2024-10-10 08:42:15 +02:00
Miloš Paunović
a595cecb3d fix(ui): revert icon coloring tweak (#5393) 2024-10-09 19:05:47 +02:00
Miloš Paunović
472b699ca7 fix(ui): amend dark mode icon color on plugins page (#5385) 2024-10-09 14:40:47 +02:00
Harshvardhan Parmar
f55f52b43a fix(ui): prevent overwriting the flow after save from topology (#5361)
Signed-off-by: Harsh4902 <harshparmar4902@gmail.com>
2024-10-09 11:09:42 +02:00
Miloš Paunović
c796308839 chore(ui): make sidebar toggle more prominent (#5357) 2024-10-09 08:40:01 +02:00
MilosPaunovic
37a880164d chore(translations) add missing key/value pair 2024-10-09 08:35:40 +02:00
GitHub Action
5f1408c560 chore(translations): auto generate values for languages other than english 2024-10-09 08:20:12 +02:00
Sai Mounika Peri
4186900fdb feat(ui): add a tooltip over flow triggers tab if empty (#5358)
Co-authored-by: Will Russell <will@wrussell.co.uk>
2024-10-09 08:15:04 +02:00
Florian Hussonnois
4338437a6f chore: update version to v0.19.2 2024-10-08 15:52:48 +02:00
MilosPaunovic
68ee5e4df0 chore(translations): add missing key in english language file 2024-10-08 15:52:48 +02:00
Loïc Mathieu
2def5cf7f8 fix(jdbc): always include deleted the the logs and metrics queries
Even if not needed to be sure we use the correct index.
2024-10-08 13:11:51 +02:00
Florian Hussonnois
d184858abf feat(core): move service usages 2024-10-08 11:02:11 +02:00
Sachin
dfa5875fa1 feat(ui): add chart visibility toggle to flows and logs page (#5345)
Co-authored-by: Sachin KS <mac@apples-MacBook-Air.local>
2024-10-08 10:08:33 +02:00
Sachin
ac4f7f261d fix(ui): amend translation keys usage (#5346)
Co-authored-by: Sachin KS <mac@apples-MacBook-Air.local>
2024-10-08 09:48:38 +02:00
GitHub Action
ae55685d2e chore(translations): auto generate values for languages other than english 2024-10-08 09:26:20 +02:00
Sai Mounika Peri
dd34317e4f feat(ui): improve page shown when flow has no dependencies (#5340) 2024-10-08 09:26:11 +02:00
riya mustare
f95e3073dd chore(ui): reduced line height on input description (#5344) 2024-10-08 09:18:03 +02:00
Florian Hussonnois
9f20988997 fix(core): use tenant for resolving worker groups 2024-10-07 14:16:00 +02:00
Sachin
5da3ab4f71 fix(ui): add bottom border on debug outputs (#5334)
Co-authored-by: Sachin KS <mac@apples-MacBook-Air.local>
2024-10-07 13:06:30 +02:00
Sachin
243eaab826 fix(ui): prevent removal of empty fields in metadata editor (#5313)
Co-authored-by: Sachin KS <mac@apples-MacBook-Air.local>
2024-10-07 11:25:37 +02:00
Sachin
6d362d688d fix(ui): amend flow disable from low code editor (#5315)
Co-authored-by: Sachin KS <mac@apples-MacBook-Air.local>
2024-10-07 11:20:28 +02:00
brian.mulier
39a01e0e7d fix(core): windows backslashes in paths were leading to wrong URI being created leading to error upon execution deletion 2024-10-07 11:19:35 +02:00
Sachin
a44b2ef7cb fix(ui): persisting flow metadata from low code editor (#5316)
Co-authored-by: Sachin KS <mac@apples-MacBook-Air.local>
2024-10-07 11:15:16 +02:00
Sachin
6bcad13444 feat(ui): added executions tab to single namespace (#5322)
Co-authored-by: Sachin KS <mac@apples-MacBook-Air.local>
2024-10-07 11:05:02 +02:00
Antoine Gauthier
02acf01ea5 chore(ui): update button conditions based on flow states (#5319) 2024-10-07 10:39:06 +02:00
Sai Mounika Peri
55193361b8 chore(ui): improve validation for kv store (#5321)
* Validation error of previous type should be cleared once the KV type is changed

* chore(ui): remove comment as code is self-explanatory

---------

Co-authored-by: MilosPaunovic <paun992@hotmail.com>
2024-10-07 09:28:38 +02:00
brian.mulier
8d509a3ba5 fix(core): path matcher for windows were not working 2024-10-04 19:41:30 +02:00
GitHub Action
500680bcf7 chore(translations): auto generate values for languages other than english 2024-10-04 15:47:10 +02:00
Miloš Paunović
412c27cb12 chore(ui): improve the dashboard ratios calculation (#5311) 2024-10-04 15:46:59 +02:00
Sachin
8d7d9a356f chore(ui): use improved chart for flow executions (#5309)
* Replace the Flows Execution barchart with the barchart used on the main dashboard

* chore(ui): added bottom margin

---------

Co-authored-by: Sachin KS <mac@apples-MacBook-Air.local>
Co-authored-by: MilosPaunovic <paun992@hotmail.com>
2024-10-04 15:01:14 +02:00
Miloš Paunović
d2ab2e97b4 fix(ui): prevent cases where dashboard totals shows nan instead of value (#5308) 2024-10-04 11:01:41 +02:00
Miloš Paunović
6a0f360fc6 fix(ui): amend end date on dashboard refresh (#5303) 2024-10-04 09:14:07 +02:00
Vivek Gangwani
0484fd389a chore(ui): making the color scheme the same for gantt and topology(#5280) 2024-10-04 09:13:14 +02:00
Miloš Paunović
e92aac3b39 chore(ui): re-calculate translation strings for left menu after language change (#5302) 2024-10-04 08:04:02 +02:00
Miloš Paunović
39b8ac8804 chore(ci): add check for translation keys matching (#5301) 2024-10-04 07:37:15 +02:00
Miloš Paunović
f928ed5876 chore(ui): uniform translation keys across languages (#5298) 2024-10-04 07:37:06 +02:00
Miloš Paunović
54856af0a8 fix(ui): amend logs scrolling for the last task (#5294) 2024-10-03 16:28:02 +02:00
MilosPaunovic
8bd79e82ab chore(ci): exit workflow with success if no changes are present 2024-10-03 16:27:53 +02:00
MilosPaunovic
104a491b92 chore(ci): separate direct pull requests and the ones from forked repositories 2024-10-03 16:27:44 +02:00
MilosPaunovic
5f46a0dd16 chore(ci): expose paste to editor function globally for testing 2024-10-03 16:27:35 +02:00
Loïc Mathieu
24c3703418 fix(core): hide secret inputs in logs
Fixes #5259
2024-10-03 10:34:27 +02:00
yuri
e5af245855 fix(ui): enable keyboard shortcut to launch execution (#5288) 2024-10-03 08:19:06 +02:00
Vivek Gangwani
d58e8f98a2 fix (ui): Unable to unselect the currently chosen log level (#5287)
* Update root.scss to Fix Topology View for Light Mode

* Update root-dark.scss to unify Gantt and TOpology View Colors

* Added deselect button for Log Levels
2024-10-03 08:18:49 +02:00
MilosPaunovic
ce2f1bfdb3 chore(ui): uniform using import class 2024-10-02 15:15:48 +02:00
Miloš Paunović
b619f88eff chore(ci): generate translation values as a commit to existing pull request (#5278) 2024-10-02 12:48:39 +02:00
Sai Mounika Peri
1f1775752b chore(ui): update parent from metadata editor (#5265) 2024-10-02 11:10:03 +02:00
AbdurRahman2004
b2475e53a2 chore(ui): move the delete logs button to top (#5266)
* Move 'Delete logs' button to top right corner of navigation

---------

Co-authored-by: Miloš Paunović <paun992@hotmail.com>
2024-10-02 10:49:59 +02:00
Antoine Gauthier
7e8956a0b7 fix(ui): amend typos in french translations (#5272) 2024-10-02 10:48:14 +02:00
brian.mulier
6537ee984b chore(version): update to version 'v0.19.1'. 2024-10-01 22:32:48 +02:00
brian.mulier
573aa48237 fix(ci): add back datahub plugin to ci build 2024-10-01 22:32:07 +02:00
brian.mulier
66ddeaa219 chore(version): update to version 'v0.19.0'. 2024-10-01 18:15:40 +02:00
brian.mulier
02c5e8a1a2 fix(ci): remove datahub plugin for now as it's not finished 2024-10-01 18:15:40 +02:00
brian.mulier
733c7897b9 fix(ci): restore github release on main workflow in case of skipped e2e 2024-10-01 15:33:36 +02:00
brian.mulier
c051287688 fix(ci): publish maven even if E2E were skipped 2024-10-01 14:26:02 +02:00
brian.mulier
1af8de6bce fix(ci): no more docker build & E2E for tags build 2024-10-01 13:43:35 +02:00
117 changed files with 2064 additions and 643 deletions

View File

@@ -52,14 +52,17 @@ The backend is made with [Micronaut](https://micronaut.io).
Open the cloned repository in your favorite IDE. In most of decent IDEs, Gradle build will be detected and all dependencies will be downloaded.
You can also build it from a terminal using `./gradlew build`, the Gradle wrapper will download the right Gradle version to use.
- You may need to enable java annotation processors since we are using it a lot.
- The main class is `io.kestra.cli.App` from module `kestra.cli.main`
- Pass as program arguments the server you want to develop, for example `server local` will start the [standalone local](https://kestra.io/docs/administrator-guide/server-cli#kestra-local-development-server-with-no-dependencies)
- ![Intellij Idea Configuration ](https://user-images.githubusercontent.com/2064609/161399626-1b681add-cfa8-4e0e-a843-2631cc59758d.png) Intellij Idea configuration can be found in screenshot below.
- `MICRONAUT_ENVIRONMENTS`: can be set any string and will load a custom configuration file in `cli/src/main/resources/application-{env}.yml`
- `KESTRA_PLUGINS_PATH`: is the path where you will save plugins as Jar and will be load on the startup.
- You can also use the gradle task `./gradlew runLocal` that will run a standalone server with `MICRONAUT_ENVIRONMENTS=override` and plugins path `local/plugins`
- The server start by default on port 8080 and is reachable on `http://localhost:8080`
- You may need to enable java annotation processors since we are using them.
- On IntelliJ IDEA, click on **Run -> Edit Configurations -> + Add new Configuration** to create a run configuration to start Kestra.
- The main class is `io.kestra.cli.App` from module `kestra.cli.main`.
- Pass as program arguments the server you want to work with, for example `server local` will start the [standalone local](https://kestra.io/docs/administrator-guide/server-cli#kestra-local-development-server-with-no-dependencies). You can also use `server standalone` and use the provided `docker-compose-ci.yml` Docker compose file to start a standalone server with a real database as a backend that would need to be configured properly.
- Configure the following environment variables:
- `MICRONAUT_ENVIRONMENTS`: can be set to any string and will load a custom configuration file in `cli/src/main/resources/application-{env}.yml`.
- `KESTRA_PLUGINS_PATH`: is the path where you will save plugins as Jar and will be load on startup.
- See the screenshot bellow for an example: ![Intellij IDEA Configuration ](run-app.png)
- If you encounter **JavaScript memory heap out** error during startup, configure `NODE_OPTIONS` environment variable with some large value.
- Example `NODE_OPTIONS: --max-old-space-size=4096` or `NODE_OPTIONS: --max-old-space-size=8192` ![Intellij IDEA Configuration ](node_option_env_var.png)
- The server starts by default on port 8080 and is reachable on `http://localhost:8080`
If you want to launch all tests, you need Python and some packages installed on your machine, on Ubuntu you can install them with:

BIN
.github/node_option_env_var.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

View File

@@ -1,45 +1,111 @@
name: Generate Translations
on:
pull_request:
types: [opened, synchronize]
paths:
- "ui/src/translations/en.json"
push:
branches:
- develop
paths:
- 'ui/src/translations/en.json'
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
jobs:
generate-translations:
name: Generate Translations and Create PR
commit:
name: Commit directly to PR
runs-on: ubuntu-latest
if: ${{ github.event.pull_request.head.repo.fork == false }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 10 # Ensures that at least 10 commits are fetched for comparison
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 10
ref: ${{ github.head_ref }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.x"
- name: Install dependencies
run: pip install gitpython openai
- name: Install Python dependencies
run: pip install gitpython openai
- name: Generate translations
run: python ui/src/translations/generate_translations.py
- name: Generate translations
run: python ui/src/translations/generate_translations.py
- name: Commit, push changes, and create PR
env:
GH_TOKEN: ${{ github.token }}
run: |
git config --global user.name "GitHub Action"
git config --global user.email "actions@github.com"
BRANCH_NAME="translations/update-translations-$(date +%s)"
git checkout -b $BRANCH_NAME
git add ui/src/translations/*.json
git commit -m "Auto-generate translations from en.json"
git push --set-upstream origin $BRANCH_NAME
gh pr create --title "Auto-generate translations from en.json" --body "This PR was created automatically by a GitHub Action." --base develop --head $BRANCH_NAME --assignee anna-geller --reviewer anna-geller
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: "20.x"
- name: Check keys matching
run: node ui/src/translations/check.js
- name: Set up Git
run: |
git config --global user.name "GitHub Action"
git config --global user.email "actions@github.com"
- name: Check for changes and commit
env:
GH_TOKEN: ${{ github.token }}
run: |
git add ui/src/translations/*.json
if git diff --cached --quiet; then
echo "No changes to commit. Exiting with success."
exit 0
fi
git commit -m "chore(translations): auto generate values for languages other than english"
git push origin ${{ github.head_ref }}
pull_request:
name: Open PR for a forked repository
runs-on: ubuntu-latest
if: ${{ github.event.pull_request.head.repo.fork == true }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.x"
- name: Install Python dependencies
run: pip install gitpython openai
- name: Generate translations
run: python ui/src/translations/generate_translations.py
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: "20.x"
- name: Check keys matching
run: node ui/src/translations/check.js
- name: Set up Git
run: |
git config --global user.name "GitHub Action"
git config --global user.email "actions@github.com"
- name: Create and push a new branch
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
BRANCH_NAME="generated-translations-${{ github.event.pull_request.head.repo.name }}"
git checkout -b $BRANCH_NAME
git add ui/src/translations/*.json
if git diff --cached --quiet; then
echo "No changes to commit. Exiting with success."
exit 0
fi
git commit -m "chore(translations): auto generate values for languages other than english"
git push origin $BRANCH_NAME

View File

@@ -68,6 +68,7 @@ jobs:
# Get Plugins List
- name: Get Plugins List
uses: ./.github/actions/plugins-list
if: "!startsWith(github.ref, 'refs/tags/v')"
id: plugins-list
with:
plugin-version: ${{ env.PLUGIN_VERSION }}
@@ -75,6 +76,7 @@ jobs:
# Set Plugins List
- name: Set Plugin List
id: plugins
if: "!startsWith(github.ref, 'refs/tags/v')"
run: |
PLUGINS="${{ steps.plugins-list.outputs.plugins }}"
TAG=${GITHUB_REF#refs/*/}
@@ -122,6 +124,7 @@ jobs:
# Docker Build
- name: Build & Export Docker Image
uses: docker/build-push-action@v6
if: "!startsWith(github.ref, 'refs/tags/v')"
with:
context: .
push: false
@@ -149,6 +152,7 @@ jobs:
- name: Upload Docker
uses: actions/upload-artifact@v4
if: "!startsWith(github.ref, 'refs/tags/v')"
with:
name: ${{ steps.vars.outputs.artifact }}
path: /tmp/${{ steps.vars.outputs.artifact }}.tar
@@ -156,7 +160,7 @@ jobs:
check-e2e:
name: Check E2E Tests
needs: build-artifacts
if: ${{ github.event.inputs.skip-test == 'false' || github.event.inputs.skip-test == '' }}
if: ${{ (github.event.inputs.skip-test == 'false' || github.event.inputs.skip-test == '') && !startsWith(github.ref, 'refs/tags/v') }}
uses: ./.github/workflows/e2e.yml
strategy:
fail-fast: false
@@ -276,7 +280,11 @@ jobs:
name: Github Release
runs-on: ubuntu-latest
needs: [ check, check-e2e ]
if: startsWith(github.ref, 'refs/tags/v')
if: |
always() &&
startsWith(github.ref, 'refs/tags/v') &&
needs.check.result == 'success' &&
(needs.check-e2e.result == 'skipped' || needs.check-e2e.result == 'success')
steps:
# Download Exec
- name: Download executable
@@ -368,7 +376,11 @@ jobs:
name: Publish to Maven
runs-on: ubuntu-latest
needs: [check, check-e2e]
if: github.ref == 'refs/heads/develop' || startsWith(github.ref, 'refs/tags/v')
if: |
always() &&
github.ref == 'refs/heads/develop' || startsWith(github.ref, 'refs/tags/v') &&
needs.check.result == 'success' &&
(needs.check-e2e.result == 'skipped' || needs.check-e2e.result == 'success')
steps:
- uses: actions/checkout@v4

View File

@@ -124,6 +124,7 @@ kestra:
delay: 1s
maxDelay: ""
jdbc:
queues:
min-poll-interval: 25ms
max-poll-interval: 1000ms

View File

@@ -19,58 +19,60 @@ import org.apache.commons.lang3.ArrayUtils;
@Singleton
@Slf4j
public class MetricRegistry {
public final static String METRIC_WORKER_JOB_PENDING_COUNT = "worker.job.pending";
public final static String METRIC_WORKER_JOB_RUNNING_COUNT = "worker.job.running";
public final static String METRIC_WORKER_JOB_THREAD_COUNT = "worker.job.thread";
public final static String METRIC_WORKER_RUNNING_COUNT = "worker.running.count";
public final static String METRIC_WORKER_QUEUED_DURATION = "worker.queued.duration";
public final static String METRIC_WORKER_STARTED_COUNT = "worker.started.count";
public final static String METRIC_WORKER_TIMEOUT_COUNT = "worker.timeout.count";
public final static String METRIC_WORKER_ENDED_COUNT = "worker.ended.count";
public final static String METRIC_WORKER_ENDED_DURATION = "worker.ended.duration";
public final static String METRIC_WORKER_TRIGGER_DURATION = "worker.trigger.duration";
public final static String METRIC_WORKER_TRIGGER_RUNNING_COUNT = "worker.trigger.running.count";
public final static String METRIC_WORKER_TRIGGER_STARTED_COUNT = "worker.trigger.started.count";
public final static String METRIC_WORKER_TRIGGER_ENDED_COUNT = "worker.trigger.ended.count";
public final static String METRIC_WORKER_TRIGGER_ERROR_COUNT = "worker.trigger.error.count";
public final static String METRIC_WORKER_TRIGGER_EXECUTION_COUNT = "worker.trigger.execution.count";
public static final String METRIC_WORKER_JOB_PENDING_COUNT = "worker.job.pending";
public static final String METRIC_WORKER_JOB_RUNNING_COUNT = "worker.job.running";
public static final String METRIC_WORKER_JOB_THREAD_COUNT = "worker.job.thread";
public static final String METRIC_WORKER_RUNNING_COUNT = "worker.running.count";
public static final String METRIC_WORKER_QUEUED_DURATION = "worker.queued.duration";
public static final String METRIC_WORKER_STARTED_COUNT = "worker.started.count";
public static final String METRIC_WORKER_TIMEOUT_COUNT = "worker.timeout.count";
public static final String METRIC_WORKER_ENDED_COUNT = "worker.ended.count";
public static final String METRIC_WORKER_ENDED_DURATION = "worker.ended.duration";
public static final String METRIC_WORKER_TRIGGER_DURATION = "worker.trigger.duration";
public static final String METRIC_WORKER_TRIGGER_RUNNING_COUNT = "worker.trigger.running.count";
public static final String METRIC_WORKER_TRIGGER_STARTED_COUNT = "worker.trigger.started.count";
public static final String METRIC_WORKER_TRIGGER_ENDED_COUNT = "worker.trigger.ended.count";
public static final String METRIC_WORKER_TRIGGER_ERROR_COUNT = "worker.trigger.error.count";
public static final String METRIC_WORKER_TRIGGER_EXECUTION_COUNT = "worker.trigger.execution.count";
public final static String EXECUTOR_TASKRUN_NEXT_COUNT = "executor.taskrun.next.count";
public final static String EXECUTOR_TASKRUN_ENDED_COUNT = "executor.taskrun.ended.count";
public final static String EXECUTOR_TASKRUN_ENDED_DURATION = "executor.taskrun.ended.duration";
public final static String EXECUTOR_WORKERTASKRESULT_COUNT = "executor.workertaskresult.count";
public final static String EXECUTOR_EXECUTION_STARTED_COUNT = "executor.execution.started.count";
public final static String EXECUTOR_EXECUTION_END_COUNT = "executor.execution.end.count";
public final static String EXECUTOR_EXECUTION_DURATION = "executor.execution.duration";
public static final String EXECUTOR_TASKRUN_NEXT_COUNT = "executor.taskrun.next.count";
public static final String EXECUTOR_TASKRUN_ENDED_COUNT = "executor.taskrun.ended.count";
public static final String EXECUTOR_TASKRUN_ENDED_DURATION = "executor.taskrun.ended.duration";
public static final String EXECUTOR_WORKERTASKRESULT_COUNT = "executor.workertaskresult.count";
public static final String EXECUTOR_EXECUTION_STARTED_COUNT = "executor.execution.started.count";
public static final String EXECUTOR_EXECUTION_END_COUNT = "executor.execution.end.count";
public static final String EXECUTOR_EXECUTION_DURATION = "executor.execution.duration";
public final static String METRIC_INDEXER_REQUEST_COUNT = "indexer.request.count";
public final static String METRIC_INDEXER_REQUEST_DURATION = "indexer.request.duration";
public final static String METRIC_INDEXER_REQUEST_RETRY_COUNT = "indexer.request.retry.count";
public final static String METRIC_INDEXER_SERVER_DURATION = "indexer.server.duration";
public final static String METRIC_INDEXER_MESSAGE_FAILED_COUNT = "indexer.message.failed.count";
public final static String METRIC_INDEXER_MESSAGE_IN_COUNT = "indexer.message.in.count";
public final static String METRIC_INDEXER_MESSAGE_OUT_COUNT = "indexer.message.out.count";
public static final String METRIC_INDEXER_REQUEST_COUNT = "indexer.request.count";
public static final String METRIC_INDEXER_REQUEST_DURATION = "indexer.request.duration";
public static final String METRIC_INDEXER_REQUEST_RETRY_COUNT = "indexer.request.retry.count";
public static final String METRIC_INDEXER_SERVER_DURATION = "indexer.server.duration";
public static final String METRIC_INDEXER_MESSAGE_FAILED_COUNT = "indexer.message.failed.count";
public static final String METRIC_INDEXER_MESSAGE_IN_COUNT = "indexer.message.in.count";
public static final String METRIC_INDEXER_MESSAGE_OUT_COUNT = "indexer.message.out.count";
public final static String SCHEDULER_LOOP_COUNT = "scheduler.loop.count";
public final static String SCHEDULER_TRIGGER_COUNT = "scheduler.trigger.count";
public final static String SCHEDULER_TRIGGER_DELAY_DURATION = "scheduler.trigger.delay.duration";
public final static String SCHEDULER_EVALUATE_COUNT = "scheduler.evaluate.count";
public final static String SCHEDULER_EXECUTION_RUNNING_DURATION = "scheduler.execution.running.duration";
public final static String SCHEDULER_EXECUTION_MISSING_DURATION = "scheduler.execution.missing.duration";
public static final String SCHEDULER_LOOP_COUNT = "scheduler.loop.count";
public static final String SCHEDULER_TRIGGER_COUNT = "scheduler.trigger.count";
public static final String SCHEDULER_TRIGGER_DELAY_DURATION = "scheduler.trigger.delay.duration";
public static final String SCHEDULER_EVALUATE_COUNT = "scheduler.evaluate.count";
public static final String SCHEDULER_EXECUTION_RUNNING_DURATION = "scheduler.execution.running.duration";
public static final String SCHEDULER_EXECUTION_MISSING_DURATION = "scheduler.execution.missing.duration";
public final static String STREAMS_STATE_COUNT = "stream.state.count";
public static final String STREAMS_STATE_COUNT = "stream.state.count";
public static final String JDBC_QUERY_DURATION = "jdbc.query.duration";
public final static String JDBC_QUERY_DURATION = "jdbc.query.duration";
public static final String QUEUE_BIG_MESSAGE_COUNT = "queue.big_message.count";
public final static String TAG_TASK_TYPE = "task_type";
public final static String TAG_TRIGGER_TYPE = "trigger_type";
public final static String TAG_FLOW_ID = "flow_id";
public final static String TAG_NAMESPACE_ID = "namespace_id";
public final static String TAG_STATE = "state";
public final static String TAG_ATTEMPT_COUNT = "attempt_count";
public final static String TAG_WORKER_GROUP = "worker_group";
public final static String TAG_TENANT_ID = "tenant_id";
public static final String TAG_TASK_TYPE = "task_type";
public static final String TAG_TRIGGER_TYPE = "trigger_type";
public static final String TAG_FLOW_ID = "flow_id";
public static final String TAG_NAMESPACE_ID = "namespace_id";
public static final String TAG_STATE = "state";
public static final String TAG_ATTEMPT_COUNT = "attempt_count";
public static final String TAG_WORKER_GROUP = "worker_group";
public static final String TAG_TENANT_ID = "tenant_id";
public static final String TAG_CLASS_NAME = "class_name";
@Inject
private MeterRegistry meterRegistry;

View File

@@ -0,0 +1,150 @@
package io.kestra.core.models.collectors;
import com.google.common.annotations.VisibleForTesting;
import io.kestra.core.repositories.ServiceInstanceRepositoryInterface;
import io.kestra.core.server.Service;
import io.kestra.core.server.ServiceInstance;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.LongSummaryStatistics;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Statistics about the number of running services over a given period.
*/
public record ServiceUsage(
List<DailyServiceStatistics> dailyStatistics
) {
/**
* Daily statistics for a specific service type.
*
* @param type The service type.
* @param values The statistic values.
*/
public record DailyServiceStatistics(
String type,
List<DailyStatistics> values
) {
}
/**
* Statistics about the number of services running at any given time interval (e.g., 15 minutes) over a day.
*
* @param date The {@link LocalDate}.
* @param min The minimum number of services.
* @param max The maximum number of services.
* @param avg The average number of services.
*/
public record DailyStatistics(
LocalDate date,
long min,
long max,
long avg
) {
}
public static ServiceUsage of(final Instant from,
final Instant to,
final ServiceInstanceRepositoryInterface repository,
final Duration interval) {
List<DailyServiceStatistics> statistics = Arrays
.stream(Service.ServiceType.values())
.map(type -> of(from, to, repository, type, interval))
.toList();
return new ServiceUsage(statistics);
}
private static DailyServiceStatistics of(final Instant from,
final Instant to,
final ServiceInstanceRepositoryInterface repository,
final Service.ServiceType serviceType,
final Duration interval) {
return of(serviceType, interval, repository.findAllInstancesBetween(serviceType, from, to));
}
@VisibleForTesting
static DailyServiceStatistics of(final Service.ServiceType serviceType,
final Duration interval,
final List<ServiceInstance> instances) {
// Compute the number of running service per time-interval.
final long timeIntervalInMillis = interval.toMillis();
final Map<Long, Long> aggregatePerTimeIntervals = instances
.stream()
.flatMap(instance -> {
List<ServiceInstance.TimestampedEvent> events = instance.events();
long start = 0;
long end = 0;
for (ServiceInstance.TimestampedEvent event : events) {
long epochMilli = event.ts().toEpochMilli();
if (event.state().equals(Service.ServiceState.RUNNING)) {
start = epochMilli;
}
else if (event.state().equals(Service.ServiceState.NOT_RUNNING) && end == 0) {
end = epochMilli;
}
else if (event.state().equals(Service.ServiceState.TERMINATED_GRACEFULLY)) {
end = epochMilli; // more precise than NOT_RUNNING
}
else if (event.state().equals(Service.ServiceState.TERMINATED_FORCED)) {
end = epochMilli; // more precise than NOT_RUNNING
}
}
if (instance.state().equals(Service.ServiceState.RUNNING)) {
end = Instant.now().toEpochMilli();
}
if (start != 0 && end != 0) {
// align to epoch-time by removing precision.
start = (start / timeIntervalInMillis) * timeIntervalInMillis;
// approximate the number of time interval for the current service
int intervals = (int) ((end - start) / timeIntervalInMillis);
// compute all time intervals
List<Long> keys = new ArrayList<>(intervals);
while (start < end) {
keys.add(start);
start = start + timeIntervalInMillis; // Next window
}
return keys.stream();
}
return Stream.empty(); // invalid service
})
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
// Aggregate per day
List<DailyStatistics> dailyStatistics = aggregatePerTimeIntervals.entrySet()
.stream()
.collect(Collectors.groupingBy(entry -> {
Long epochTimeMilli = entry.getKey();
return Instant.ofEpochMilli(epochTimeMilli).atZone(ZoneId.systemDefault()).toLocalDate();
}, Collectors.toList()))
.entrySet()
.stream()
.map(entry -> {
LongSummaryStatistics statistics = entry.getValue().stream().collect(Collectors.summarizingLong(Map.Entry::getValue));
return new DailyStatistics(
entry.getKey(),
statistics.getMin(),
statistics.getMax(),
BigDecimal.valueOf(statistics.getAverage()).setScale(2, RoundingMode.HALF_EVEN).longValue()
);
})
.toList();
return new DailyServiceStatistics(serviceType.name(), dailyStatistics);
}
}

View File

@@ -62,4 +62,8 @@ public class Usage {
@Valid
private final ExecutionUsage executions;
@Valid
@Nullable
private ServiceUsage services;
}

View File

@@ -358,4 +358,8 @@ public class Flow extends AbstractFlow {
.deleted(true)
.build();
}
public FlowWithSource withSource(String source) {
return FlowWithSource.of(this, source);
}
}

View File

@@ -1,6 +1,5 @@
package io.kestra.core.models.flows;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonSetter;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@@ -43,7 +42,6 @@ import lombok.experimental.SuperBuilder;
@JsonSubTypes.Type(value = MultiselectInput.class, name = "MULTISELECT"),
@JsonSubTypes.Type(value = YamlInput.class, name = "YAML")
})
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
public abstract class Input<T> implements Data {
@Schema(
title = "The ID of the input."

View File

@@ -11,6 +11,7 @@ import io.kestra.core.services.KVStoreService;
import io.kestra.core.storages.Storage;
import io.kestra.core.storages.StorageInterface;
import io.kestra.core.storages.kv.KVStore;
import io.kestra.core.utils.ListUtils;
import io.kestra.core.utils.VersionProvider;
import io.micronaut.context.ApplicationContext;
import io.micronaut.context.annotation.Value;
@@ -30,7 +31,6 @@ import java.nio.file.Path;
import java.security.GeneralSecurityException;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import static io.kestra.core.utils.MapUtils.mergeWithNullableValues;
@@ -67,6 +67,7 @@ public class DefaultRunContext extends RunContext {
private String triggerExecutionId;
private Storage storage;
private Map<String, Object> pluginConfiguration;
private List<String> secretInputs;
private final AtomicBoolean isInitialized = new AtomicBoolean(false);
@@ -98,6 +99,15 @@ public class DefaultRunContext extends RunContext {
return variables;
}
/**
* {@inheritDoc}
*/
@Override
@JsonInclude
public List<String> getSecretInputs() {
return secretInputs;
}
@JsonIgnore
public ApplicationContext getApplicationContext() {
return applicationContext;
@@ -123,6 +133,17 @@ public class DefaultRunContext extends RunContext {
void setLogger(final RunContextLogger logger) {
this.logger = logger;
// this is used when a run context is re-hydrated so we need to add again the secrets from the inputs
if (!ListUtils.isEmpty(secretInputs) && getVariables().containsKey("inputs")) {
Map<String, Object> inputs = (Map<String, Object>) getVariables().get("inputs");
for (String secretInput : secretInputs) {
String secret = (String) inputs.get(secretInput);
if (secret != null) {
logger.usedSecret(secret);
}
}
}
}
void setPluginConfiguration(final Map<String, Object> pluginConfiguration) {
@@ -179,7 +200,7 @@ public class DefaultRunContext extends RunContext {
@Override
@SuppressWarnings("unchecked")
public String render(String inline, Map<String, Object> variables) throws IllegalVariableEvaluationException {
return variableRenderer.render(inline, mergeWithNullableValues(this.variables, variables));
return variableRenderer.render(inline, mergeWithNullableValues(this.variables, decryptVariables(variables)));
}
/**
@@ -196,7 +217,7 @@ public class DefaultRunContext extends RunContext {
@Override
@SuppressWarnings("unchecked")
public List<String> render(List<String> inline, Map<String, Object> variables) throws IllegalVariableEvaluationException {
return variableRenderer.render(inline, mergeWithNullableValues(this.variables, variables));
return variableRenderer.render(inline, mergeWithNullableValues(this.variables, decryptVariables(variables)));
}
/**
@@ -213,7 +234,7 @@ public class DefaultRunContext extends RunContext {
@Override
@SuppressWarnings("unchecked")
public Set<String> render(Set<String> inline, Map<String, Object> variables) throws IllegalVariableEvaluationException {
return variableRenderer.render(inline, mergeWithNullableValues(this.variables, variables));
return variableRenderer.render(inline, mergeWithNullableValues(this.variables, decryptVariables(variables)));
}
@Override
@@ -224,7 +245,7 @@ public class DefaultRunContext extends RunContext {
@Override
@SuppressWarnings("unchecked")
public Map<String, Object> render(Map<String, Object> inline, Map<String, Object> variables) throws IllegalVariableEvaluationException {
return variableRenderer.render(inline, mergeWithNullableValues(this.variables, variables));
return variableRenderer.render(inline, mergeWithNullableValues(this.variables, decryptVariables(variables)));
}
@Override
@@ -239,7 +260,7 @@ public class DefaultRunContext extends RunContext {
return null;
}
Map<String, Object> allVariables = mergeWithNullableValues(this.variables, variables);
Map<String, Object> allVariables = mergeWithNullableValues(this.variables, decryptVariables(variables));
return inline
.entrySet()
.stream()
@@ -350,6 +371,14 @@ public class DefaultRunContext extends RunContext {
return this;
}
private Map<String, Object> decryptVariables(Map<String, Object> variables) {
if (secretKey.isPresent()) {
final Secret secret = new Secret(secretKey, logger);
return secret.decrypt(variables);
}
return variables;
}
@SuppressWarnings("unchecked")
private Map<String, String> metricsTags() {
ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
@@ -488,6 +517,7 @@ public class DefaultRunContext extends RunContext {
private String triggerExecutionId;
private RunContextLogger logger;
private KVStoreService kvStoreService;
private List<String> secretInputs;
/**
* Builds the new {@link DefaultRunContext} object.
@@ -507,6 +537,7 @@ public class DefaultRunContext extends RunContext {
context.storage = storage;
context.triggerExecutionId = triggerExecutionId;
context.kvStoreService = kvStoreService;
context.secretInputs = secretInputs;
return context;
}
}

View File

@@ -748,7 +748,8 @@ public class ExecutorService {
.map(WorkerGroup::getKey)
.orElse(null);
// Check if the worker group exist
if (workerGroupExecutorInterface.isWorkerGroupExistForKey(workerGroup)) {
String tenantId = executor.getFlow().getTenantId();
if (workerGroupExecutorInterface.isWorkerGroupExistForKey(workerGroup, tenantId)) {
// Check whether at-least one worker is available
if (workerGroupExecutorInterface.isWorkerGroupAvailableForKey(workerGroup)) {
return workerTask;

View File

@@ -5,6 +5,7 @@ import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import io.kestra.core.encryption.EncryptionService;
import io.kestra.core.exceptions.IllegalVariableEvaluationException;
import io.kestra.core.exceptions.KestraRuntimeException;
import io.kestra.core.models.executions.Execution;
import io.kestra.core.models.flows.Data;
import io.kestra.core.models.flows.DependsOn;
@@ -18,6 +19,7 @@ import io.kestra.core.models.flows.input.ItemTypeInterface;
import io.kestra.core.models.tasks.common.EncryptedString;
import io.kestra.core.models.validations.ManualConstraintViolation;
import io.kestra.core.serializers.JacksonMapper;
import io.kestra.core.storages.StorageContext;
import io.kestra.core.storages.StorageInterface;
import io.kestra.core.utils.ListUtils;
import io.kestra.core.utils.MapUtils;
@@ -33,7 +35,7 @@ import org.reactivestreams.Publisher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
import reactor.core.scheduler.Schedulers;
import reactor.core.publisher.Mono;
import java.io.File;
import java.io.FileOutputStream;
@@ -90,31 +92,14 @@ public class FlowInputOutput {
* @param inputs The Flow's inputs.
* @param execution The Execution.
* @param data The Execution's inputs data.
* @param deleteInputsFromStorage Specifies whether inputs stored on internal storage should be deleted before returning.
* @return The list of {@link InputAndValue}.
*/
public List<InputAndValue> validateExecutionInputs(final List<Input<?>> inputs,
public Mono<List<InputAndValue>> validateExecutionInputs(final List<Input<?>> inputs,
final Execution execution,
final Publisher<CompletedPart> data,
final boolean deleteInputsFromStorage) throws IOException {
if (ListUtils.isEmpty(inputs)) return Collections.emptyList();
final Publisher<CompletedPart> data) {
if (ListUtils.isEmpty(inputs)) return Mono.just(Collections.emptyList());
Map<String, ?> dataByInputId = readData(inputs, execution, data);
List<InputAndValue> values = this.resolveInputs(inputs, execution, dataByInputId);
if (deleteInputsFromStorage) {
values.stream()
.filter(it -> it.input() instanceof FileInput && Objects.nonNull(it.value()))
.forEach(it -> {
try {
URI uri = URI.create(it.value().toString());
storageInterface.delete(execution.getTenantId(), uri);
} catch (IllegalArgumentException | IOException e) {
log.debug("Failed to remove execution input after validation [{}]", it.value(), e);
}
});
}
return values;
return readData(inputs, execution, data, false).map(inputData -> resolveInputs(inputs, execution, inputData));
}
/**
@@ -125,9 +110,9 @@ public class FlowInputOutput {
* @param data The Execution's inputs data.
* @return The Map of typed inputs.
*/
public Map<String, Object> readExecutionInputs(final Flow flow,
public Mono<Map<String, Object>> readExecutionInputs(final Flow flow,
final Execution execution,
final Publisher<CompletedPart> data) throws IOException {
final Publisher<CompletedPart> data) {
return this.readExecutionInputs(flow.getInputs(), execution, data);
}
@@ -139,39 +124,51 @@ public class FlowInputOutput {
* @param data The Execution's inputs data.
* @return The Map of typed inputs.
*/
public Map<String, Object> readExecutionInputs(final List<Input<?>> inputs,
final Execution execution,
final Publisher<CompletedPart> data) throws IOException {
return this.readExecutionInputs(inputs, execution, readData(inputs, execution, data));
public Mono<Map<String, Object>> readExecutionInputs(final List<Input<?>> inputs,
final Execution execution,
final Publisher<CompletedPart> data) {
return readData(inputs, execution, data, true).map(inputData -> this.readExecutionInputs(inputs, execution, inputData));
}
private Map<String, ?> readData(List<Input<?>> inputs, Execution execution, Publisher<CompletedPart> data) throws IOException {
private Mono<Map<String, Object>> readData(List<Input<?>> inputs, Execution execution, Publisher<CompletedPart> data, boolean uploadFiles) {
return Flux.from(data)
.subscribeOn(Schedulers.boundedElastic())
.map(throwFunction(input -> {
if (input instanceof CompletedFileUpload fileUpload) {
final String fileExtension = FileInput.findFileInputExtension(inputs, fileUpload.getFilename());
File tempFile = File.createTempFile(fileUpload.getFilename() + "_", fileExtension);
try (var inputStream = fileUpload.getInputStream();
var outputStream = new FileOutputStream(tempFile)) {
long transferredBytes = inputStream.transferTo(outputStream);
if (transferredBytes == 0) {
throw new RuntimeException("Can't upload file: " + fileUpload.getFilename());
.<AbstractMap.SimpleEntry<String, String>>handle((input, sink) -> {
try {
if (input instanceof CompletedFileUpload fileUpload) {
if (!uploadFiles) {
final String fileExtension = FileInput.findFileInputExtension(inputs, fileUpload.getFilename());
URI from = URI.create("kestra://" + StorageContext
.forInput(execution, fileUpload.getFilename(), fileUpload.getFilename() + fileExtension)
.getContextStorageURI()
);
sink.next(new AbstractMap.SimpleEntry<>(fileUpload.getFilename(), from.toString()));
return;
}
final String fileExtension = FileInput.findFileInputExtension(inputs, fileUpload.getFilename());
URI from = storageInterface.from(execution, fileUpload.getFilename(), tempFile);
return new AbstractMap.SimpleEntry<>(fileUpload.getFilename(), from.toString());
} finally {
if (!tempFile.delete()) {
tempFile.deleteOnExit();
}
File tempFile = File.createTempFile(fileUpload.getFilename() + "_", fileExtension);
try (var inputStream = fileUpload.getInputStream();
var outputStream = new FileOutputStream(tempFile)) {
long transferredBytes = inputStream.transferTo(outputStream);
if (transferredBytes == 0) {
sink.error(new KestraRuntimeException("Can't upload file: " + fileUpload.getFilename()));
return;
}
URI from = storageInterface.from(execution, fileUpload.getFilename(), tempFile);
sink.next(new AbstractMap.SimpleEntry<>(fileUpload.getFilename(), from.toString()));
} finally {
if (!tempFile.delete()) {
tempFile.deleteOnExit();
}
}
} else {
sink.next(new AbstractMap.SimpleEntry<>(input.getName(), new String(input.getBytes())));
}
} else {
return new AbstractMap.SimpleEntry<>(input.getName(), new String(input.getBytes()));
} catch (IOException e) {
sink.error(e);
}
}))
.collectMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue)
.block();
})
.collectMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue);
}
/**
@@ -404,7 +401,8 @@ public class FlowInputOutput {
yield EncryptionService.encrypt(secretKey.get(), (String) current);
}
case INT -> current instanceof Integer ? current : Integer.valueOf((String) current);
case FLOAT -> current instanceof Float ? current : Float.valueOf((String) current);
// Assuming that after the render we must have a double/int, so we can safely use its toString representation
case FLOAT -> current instanceof Float ? current : Float.valueOf(current.toString());
case BOOLEAN -> current instanceof Boolean ? current : Boolean.valueOf((String) current);
case DATETIME -> Instant.parse(((String) current));
case DATE -> LocalDate.parse(((String) current));

View File

@@ -47,6 +47,12 @@ public abstract class RunContext {
@JsonInclude
public abstract Map<String, Object> getVariables();
/**
* Returns the list of inputs of type SECRET.
*/
@JsonInclude
public abstract List<String> getSecretInputs();
public abstract String render(String inline) throws IllegalVariableEvaluationException;
public abstract Object renderTyped(String inline) throws IllegalVariableEvaluationException;

View File

@@ -5,6 +5,7 @@ import io.kestra.core.metrics.MetricRegistry;
import io.kestra.core.models.executions.Execution;
import io.kestra.core.models.executions.TaskRun;
import io.kestra.core.models.flows.Flow;
import io.kestra.core.models.flows.Type;
import io.kestra.core.models.tasks.Task;
import io.kestra.core.models.triggers.AbstractTrigger;
import io.kestra.core.plugins.PluginConfigurations;
@@ -15,12 +16,12 @@ import io.kestra.core.storages.StorageContext;
import io.kestra.core.storages.StorageInterface;
import io.micronaut.context.ApplicationContext;
import io.micronaut.context.annotation.Value;
import jakarta.annotation.Nullable;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import jakarta.validation.constraints.NotNull;
import java.net.URI;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
@@ -83,8 +84,10 @@ public class RunContextFactory {
.withFlow(flow)
.withExecution(execution)
.withDecryptVariables(true)
.withSecretInputs(secretInputsFromFlow(flow))
)
.build(runContextLogger))
.withSecretInputs(secretInputsFromFlow(flow))
.build();
}
@@ -107,8 +110,10 @@ public class RunContextFactory {
.withExecution(execution)
.withTaskRun(taskRun)
.withDecryptVariables(decryptVariables)
.withSecretInputs(secretInputsFromFlow(flow))
.build(runContextLogger))
.withKvStoreService(kvStoreService)
.withSecretInputs(secretInputsFromFlow(flow))
.build();
}
@@ -122,8 +127,10 @@ public class RunContextFactory {
.withVariables(newRunVariablesBuilder()
.withFlow(flow)
.withTrigger(trigger)
.withSecretInputs(secretInputsFromFlow(flow))
.build(runContextLogger)
)
.withSecretInputs(secretInputsFromFlow(flow))
.build();
}
@@ -135,6 +142,7 @@ public class RunContextFactory {
.withLogger(runContextLogger)
.withStorage(new InternalStorage(runContextLogger.logger(), StorageContext.forFlow(flow), storageInterface, flowService))
.withVariables(variables)
.withSecretInputs(secretInputsFromFlow(flow))
.build();
}
@@ -177,6 +185,16 @@ public class RunContextFactory {
return of(Map.of());
}
private List<String> secretInputsFromFlow(Flow flow) {
if (flow == null || flow.getInputs() == null) {
return Collections.emptyList();
}
return flow.getInputs().stream()
.filter(input -> input.getType() == Type.SECRET)
.map(input -> input.getId()).toList();
}
private DefaultRunContext.Builder newBuilder() {
return new DefaultRunContext.Builder()
// inject mandatory services and config

View File

@@ -9,6 +9,7 @@ import io.kestra.core.models.flows.State;
import io.kestra.core.models.flows.input.SecretInput;
import io.kestra.core.models.tasks.Task;
import io.kestra.core.models.triggers.AbstractTrigger;
import io.kestra.core.utils.ListUtils;
import lombok.AllArgsConstructor;
import lombok.With;
@@ -125,6 +126,8 @@ public final class RunVariables {
Builder withGlobals(Map<?, ?> globals);
Builder withSecretInputs(List<String> secretInputs);
/**
* Builds the immutable map of run variables.
*
@@ -152,6 +155,7 @@ public final class RunVariables {
protected Map<String, ?> envs;
protected Map<?, ?> globals;
private final Optional<String> secretKey;
private List<String> secretInputs;
public DefaultBuilder() {
this(Optional.empty());
@@ -252,6 +256,16 @@ public final class RunVariables {
if (!inputs.isEmpty()) {
builder.put("inputs", inputs);
// if a secret input is used, add it to the list of secrets to mask on the logger
if (logger != null && !ListUtils.isEmpty(secretInputs)) {
for (String secretInput : secretInputs) {
String secret = (String) inputs.get(secretInput);
if (secret != null) {
logger.usedSecret(secret);
}
}
}
}
if (execution.getTrigger() != null && execution.getTrigger().getVariables() != null) {

View File

@@ -530,10 +530,6 @@ public class Worker implements Service, Runnable, AutoCloseable {
.increment();
}
private static ZonedDateTime now() {
return ZonedDateTime.now().truncatedTo(ChronoUnit.SECONDS);
}
private WorkerTask cleanUpTransient(WorkerTask workerTask) {
try {
return MAPPER.readValue(MAPPER.writeValueAsString(workerTask), WorkerTask.class);
@@ -553,7 +549,7 @@ public class Worker implements Service, Runnable, AutoCloseable {
metricRegistry
.timer(MetricRegistry.METRIC_WORKER_QUEUED_DURATION, metricRegistry.tags(workerTask, workerGroup))
.record(Duration.between(
workerTask.getTaskRun().getState().getStartDate(), now()
workerTask.getTaskRun().getState().getStartDate(), Instant.now()
));
}
@@ -704,8 +700,7 @@ public class Worker implements Service, Runnable, AutoCloseable {
}
private WorkerTask runAttempt(WorkerTask workerTask) throws QueueException {
DefaultRunContext runContext = (DefaultRunContext) workerTask.getRunContext();
runContextInitializer.forWorker(runContext, workerTask);
DefaultRunContext runContext = runContextInitializer.forWorker((DefaultRunContext) workerTask.getRunContext(), workerTask);;
Logger logger = runContext.logger();

View File

@@ -13,12 +13,13 @@ import java.util.Set;
public interface WorkerGroupExecutorInterface {
/**
* Checks whether a Worker Group exists for the given key.
* Checks whether a Worker Group exists for the given key and tenant.
*
* @param key The Worker Group's key - can be {@code null}.
* @param tenant The tenant's ID - can be {@code null}.
* @return {@code true} if the worker group exists, or is {@code null}, {@code false} otherwise.
*/
boolean isWorkerGroupExistForKey(String key);
boolean isWorkerGroupExistForKey(String key, String tenant);
/**
* Checks whether the Worker Group is available.
@@ -46,7 +47,7 @@ public interface WorkerGroupExecutorInterface {
class DefaultWorkerGroupExecutorInterface implements WorkerGroupExecutorInterface {
@Override
public boolean isWorkerGroupExistForKey(String key) {
public boolean isWorkerGroupExistForKey(String key, String tenant) {
return true;
}

View File

@@ -5,6 +5,7 @@ import io.kestra.core.models.collectors.*;
import io.kestra.core.plugins.PluginRegistry;
import io.kestra.core.repositories.ExecutionRepositoryInterface;
import io.kestra.core.repositories.FlowRepositoryInterface;
import io.kestra.core.repositories.ServiceInstanceRepositoryInterface;
import io.kestra.core.serializers.JacksonMapper;
import io.kestra.core.utils.IdUtils;
import io.kestra.core.utils.VersionProvider;
@@ -24,6 +25,7 @@ import lombok.extern.slf4j.Slf4j;
import java.lang.management.ManagementFactory;
import java.net.URI;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
@@ -66,6 +68,9 @@ public class CollectorService {
@Value("${kestra.anonymous-usage-report.uri}")
protected URI url;
@Inject
private ServiceInstanceRepositoryInterface serviceRepository;
private transient Usage defaultUsage;
protected synchronized Usage defaultUsage() {
@@ -109,7 +114,8 @@ public class CollectorService {
if (details) {
builder = builder
.flows(FlowUsage.of(flowRepository))
.executions(ExecutionUsage.of(executionRepository, from, to));
.executions(ExecutionUsage.of(executionRepository, from, to))
.services(ServiceUsage.of(from.toInstant(), to.toInstant(), serviceRepository, Duration.ofMinutes(5)));
}
return builder.build();
}

View File

@@ -3,7 +3,11 @@ package io.kestra.core.services;
import io.kestra.core.events.CrudEvent;
import io.kestra.core.events.CrudEventType;
import io.kestra.core.exceptions.InternalException;
import io.kestra.core.models.executions.*;
import io.kestra.core.models.executions.Execution;
import io.kestra.core.models.executions.ExecutionKilled;
import io.kestra.core.models.executions.ExecutionKilledExecution;
import io.kestra.core.models.executions.TaskRun;
import io.kestra.core.models.executions.TaskRunAttempt;
import io.kestra.core.models.flows.Flow;
import io.kestra.core.models.flows.State;
import io.kestra.core.models.flows.input.InputAndValue;
@@ -26,7 +30,6 @@ import io.kestra.plugin.core.flow.Pause;
import io.kestra.plugin.core.flow.WorkingDirectory;
import io.micronaut.context.event.ApplicationEventPublisher;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.multipart.CompletedPart;
import jakarta.inject.Inject;
import jakarta.inject.Named;
@@ -38,12 +41,21 @@ import lombok.experimental.SuperBuilder;
import lombok.extern.slf4j.Slf4j;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.io.IOException;
import java.net.URI;
import java.time.Instant;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Predicate;
import java.util.stream.Collectors;
@@ -447,19 +459,16 @@ public class ExecutionService {
* @param flow the flow of the execution
* @param inputs the onResume inputs
* @return the execution in the new state.
* @throws Exception if the state of the execution cannot be updated
*/
public List<InputAndValue> validateForResume(final Execution execution, Flow flow, @Nullable Publisher<CompletedPart> inputs) throws Exception {
Task task = getFirstPausedTaskOrThrow(execution, flow);
if (task instanceof Pause pauseTask) {
return flowInputOutput.validateExecutionInputs(
pauseTask.getOnResume(),
execution,
inputs,
true
);
}
return Collections.emptyList();
public Mono<List<InputAndValue>> validateForResume(final Execution execution, Flow flow, @Nullable Publisher<CompletedPart> inputs) {
return getFirstPausedTaskOrThrow(execution, flow)
.flatMap(task -> {
if (task instanceof Pause pauseTask) {
return flowInputOutput.validateExecutionInputs(pauseTask.getOnResume(), execution, inputs);
} else {
return Mono.just(Collections.emptyList());
}
});
}
/**
@@ -471,27 +480,36 @@ public class ExecutionService {
* @param flow the flow of the execution
* @param inputs the onResume inputs
* @return the execution in the new state.
* @throws Exception if the state of the execution cannot be updated
*/
public Execution resume(final Execution execution, Flow flow, State.Type newState, @Nullable Publisher<CompletedPart> inputs) throws Exception {
var task = getFirstPausedTaskOrThrow(execution, flow);
Map<String, Object> pauseOutputs = Collections.emptyMap();
if (task instanceof Pause pauseTask) {
pauseOutputs = flowInputOutput.readExecutionInputs(
pauseTask.getOnResume(),
execution,
inputs
);
}
return resume(execution, flow, newState, pauseOutputs);
public Mono<Execution> resume(final Execution execution, Flow flow, State.Type newState, @Nullable Publisher<CompletedPart> inputs) {
return getFirstPausedTaskOrThrow(execution, flow)
.flatMap(task -> {
if (task instanceof Pause pauseTask) {
return flowInputOutput.readExecutionInputs(pauseTask.getOnResume(), execution, inputs);
} else {
return Mono.just(Collections.<String, Object>emptyMap());
}
})
.handle((resumeInputs, sink) -> {
try {
sink.next(resume(execution, flow, newState, resumeInputs));
} catch (Exception e) {
sink.error(e);
}
});
}
private static Task getFirstPausedTaskOrThrow(Execution execution, Flow flow) throws InternalException {
var runningTaskRun = execution
.findFirstByState(State.Type.PAUSED)
.orElseThrow(() -> new IllegalArgumentException("No paused task found on execution " + execution.getId()));
return flow.findTaskByTaskId(runningTaskRun.getTaskId());
private static Mono<Task> getFirstPausedTaskOrThrow(Execution execution, Flow flow){
return Mono.create(sink -> {
try {
var runningTaskRun = execution
.findFirstByState(State.Type.PAUSED)
.orElseThrow(() -> new IllegalArgumentException("No paused task found on execution " + execution.getId()));
sink.success(flow.findTaskByTaskId(runningTaskRun.getTaskId()));
} catch (InternalException e) {
sink.error(e);
}
});
}
/**

View File

@@ -99,7 +99,7 @@ public final class PathMatcherPredicate implements Predicate<Path> {
} else {
pattern = mayAddRecursiveMatch(p);
}
syntaxAndPattern = SYNTAX_GLOB + pattern;
syntaxAndPattern = SYNTAX_GLOB + pattern.replace("\\", "/");
}
return syntaxAndPattern;
})
@@ -125,7 +125,7 @@ public final class PathMatcherPredicate implements Predicate<Path> {
}
private static String mayAddLeadingSlash(final String path) {
return path.startsWith("/") ? path : "/" + path;
return (path.startsWith("/") || path.startsWith("\\")) ? path : "/" + path;
}
public static boolean isPrefixWithSyntax(final String pattern) {

View File

@@ -19,6 +19,9 @@ import io.kestra.core.models.tasks.*;
import io.kestra.core.runners.*;
import io.kestra.core.serializers.FileSerde;
import io.kestra.core.services.StorageService;
import io.kestra.core.storages.FileAttributes;
import io.kestra.core.storages.StorageContext;
import io.kestra.core.storages.StorageInterface;
import io.kestra.core.storages.StorageSplitInterface;
import io.kestra.core.utils.GraphUtils;
import io.swagger.v3.oas.annotations.media.Schema;
@@ -580,23 +583,25 @@ public class ForEachItem extends Task implements FlowableTask<VoidOutput>, Child
return null;
}
Integer iterations = (Integer) taskOutput.get(ExecutableUtils.TASK_VARIABLE_NUMBER_OF_BATCHES);
String subflowOutputsBaseUri = (String) taskOutput.get(ExecutableUtils.TASK_VARIABLE_SUBFLOW_OUTPUTS_BASE_URI);
String subflowOutputsBase = (String) taskOutput.get(ExecutableUtils.TASK_VARIABLE_SUBFLOW_OUTPUTS_BASE_URI);
URI subflowOutputsBaseUri = URI.create(StorageContext.KESTRA_PROTOCOL + subflowOutputsBase + "/");
List<URI> outputsURIs = IntStream.rangeClosed(1, iterations)
.mapToObj(it -> "kestra://" + subflowOutputsBaseUri + "/" + it + "/outputs.ion")
.map(throwFunction(URI::create))
.filter(runContext.storage()::isFileExist)
.toList();
StorageInterface storage = ((DefaultRunContext) runContext).getApplicationContext().getBean(StorageInterface.class);
if (storage.exists(runContext.tenantId(), subflowOutputsBaseUri)) {
List<FileAttributes> list = storage.list(runContext.tenantId(), subflowOutputsBaseUri);
if (!outputsURIs.isEmpty()) {
// Merge outputs from each sub-flow into a single stored in the internal storage.
List<InputStream> streams = outputsURIs.stream()
.map(throwFunction(runContext.storage()::getFile))
.toList();
try (InputStream is = new SequenceInputStream(Collections.enumeration(streams))) {
URI uri = runContext.storage().putFile(is, "outputs.ion");
return ForEachItemMergeOutputs.Output.builder().subflowOutputs(uri).build();
if (!list.isEmpty()) {
// Merge outputs from each sub-flow into a single stored in the internal storage.
List<InputStream> streams = list.stream()
.map(throwFunction(attr -> {
URI file = subflowOutputsBaseUri.resolve(attr.getFileName() + "/outputs.ion");
return runContext.storage().getFile(file);
}))
.toList();
try (InputStream is = new SequenceInputStream(Collections.enumeration(streams))) {
URI uri = runContext.storage().putFile(is, "outputs.ion");
return ForEachItemMergeOutputs.Output.builder().subflowOutputs(uri).build();
}
}
}

View File

@@ -21,6 +21,8 @@ import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
@@ -128,6 +130,9 @@ public class Download extends AbstractHttp implements RunnableTask<Download.Outp
String contentDisposition = builder.headers.get("Content-Disposition").getFirst();
filename = filenameFromHeader(runContext, contentDisposition);
}
if (filename != null) {
filename = URLEncoder.encode(filename, StandardCharsets.UTF_8);
}
builder.uri(runContext.storage().putFile(tempFile, filename));
@@ -145,7 +150,7 @@ public class Download extends AbstractHttp implements RunnableTask<Download.Outp
String filename = null;
for (String part : parts) {
if (part.startsWith("filename")) {
filename = part.substring(part.lastIndexOf('=') + 2, part.length() - 1);
filename = part.substring(part.lastIndexOf('=') + 1);
}
if (part.startsWith("filename*")) {
// following https://datatracker.ietf.org/doc/html/rfc5987 the filename* should be <ENCODING>'(lang)'<filename>

View File

@@ -16,6 +16,7 @@ import lombok.experimental.SuperBuilder;
import org.slf4j.Logger;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
@@ -67,7 +68,7 @@ import java.util.Optional;
- id: log_response
type: io.kestra.plugin.core.log.Log
message: '{{ trigger.body }}'
triggers:
- id: http
type: io.kestra.plugin.core.http.Trigger
@@ -154,12 +155,12 @@ public class Trigger extends AbstractTrigger implements PollingTriggerInterface,
Object body = this.encryptBody
? runContext.decrypt(output.getEncryptedBody().getValue())
: output.getBody();
Map<String, Object> responseVariables = Map.of("response", Map.of(
"statusCode", output.getCode(),
"body", body,
"headers", output.getHeaders()
)
);
Map<String, Object> response = new HashMap<>();
response.put("statusCode", output.getCode());
response.put("body", body); // body can be null so we need a null-friendly map
response.put("headers", output.getHeaders());
Map<String, Object> responseVariables = Map.of("response", response);
var renderedCondition = runContext.render(this.responseCondition, responseVariables);
if (TruthUtils.isTruthy(renderedCondition)) {
Execution execution = TriggerService.generateExecution(this, conditionContext, context, output);

View File

@@ -23,6 +23,7 @@ import java.util.Map;
You can use this task to return some outputs and pass them to downstream tasks.
It's helpful for parsing and returning values from a task. You can then access these outputs in your downstream tasks
using the expression `{{ outputs.mytask_id.values.my_output_name }}` and you can see them in the Outputs tab.
The values can be strings, numbers, arrays, or any valid JSON object.
"""
)
@Plugin(
@@ -39,6 +40,11 @@ tasks:
values:
taskrun_data: "{{ task.id }} > {{ taskrun.startDate }}"
execution_data: "{{ flow.id }} > {{ execution.startDate }}"
number_value: 42
array_value: ["{{ task.id }}", "{{ flow.id }}", "static value"]
nested_object:
key1: "value1"
key2: "{{ execution.id }}"
- id: log_values
type: io.kestra.plugin.core.log.Log
@@ -51,15 +57,16 @@ tasks:
)
public class OutputValues extends Task implements RunnableTask<OutputValues.Output> {
@Schema(
title = "The templated strings to render."
title = "The templated strings to render.",
description = "These values can be strings, numbers, arrays, or objects. Templated strings (enclosed in {{ }}) will be rendered using the current context."
)
private HashMap<String, String> values;
private HashMap<String, Object> values;
@Override
public OutputValues.Output run(RunContext runContext) throws Exception {
return OutputValues.Output.builder()
.values(runContext.renderMap(values))
.values(runContext.render(values))
.build();
}
@@ -69,6 +76,6 @@ public class OutputValues extends Task implements RunnableTask<OutputValues.Outp
@Schema(
title = "The generated values."
)
private Map<String, String> values;
private Map<String, Object> values;
}
}

View File

@@ -0,0 +1,61 @@
package io.kestra.core.models.collectors;
import io.kestra.core.server.Service;
import io.kestra.core.server.ServiceInstance;
import io.kestra.core.utils.IdUtils;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
class ServiceUsageTest {
@Test
void shouldGetDailyUsage() {
// Given
LocalDate now = LocalDate.now();
LocalDate start = now.withDayOfMonth(1);
LocalDate end = start.withDayOfMonth(start.getMonth().length(start.isLeapYear()));
List<ServiceInstance> instances = new ArrayList<>();
while (start.toEpochDay() < end.toEpochDay()) {
Instant createAt = start.atStartOfDay(ZoneId.systemDefault()).toInstant();
Instant updatedAt = start.atStartOfDay(ZoneId.systemDefault()).plus(Duration.ofHours(10)).toInstant();
ServiceInstance instance = new ServiceInstance(
IdUtils.create(),
Service.ServiceType.WORKER,
Service.ServiceState.EMPTY,
null,
createAt,
updatedAt,
List.of(),
null,
Map.of(),
Set.of()
);
instance = instance
.state(Service.ServiceState.RUNNING, createAt)
.state(Service.ServiceState.NOT_RUNNING, updatedAt);
instances.add(instance);
start = start.plusDays(1);
}
// When
ServiceUsage.DailyServiceStatistics statistics = ServiceUsage.of(
Service.ServiceType.WORKER,
Duration.ofMinutes(15),
instances
);
// Then
Assertions.assertEquals(instances.size(), statistics.values().size());
}
}

View File

@@ -1,16 +1,32 @@
package io.kestra.core.runners;
import io.kestra.core.encryption.EncryptionService;
import io.kestra.core.exceptions.IllegalVariableEvaluationException;
import io.kestra.core.models.tasks.common.EncryptedString;
import io.micronaut.context.ApplicationContext;
import io.micronaut.context.annotation.Value;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.security.GeneralSecurityException;
import java.util.Map;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
@MicronautTest
class DefaultRunContextTest {
@Inject
ApplicationContext applicationContext;
private ApplicationContext applicationContext;
@Value("${kestra.encryption.secret-key}")
private String secretKey;
@Inject
private RunContextFactory runContextFactory;
@Test
void shouldGetKestraVersion() {
@@ -18,4 +34,16 @@ class DefaultRunContextTest {
runContext.init(applicationContext);
Assertions.assertNotNull(runContext.version());
}
@Test
void shouldDecryptVariables() throws GeneralSecurityException, IllegalVariableEvaluationException {
RunContext runContext = runContextFactory.of();
String encryptedSecret = EncryptionService.encrypt(secretKey, "It's a secret");
Map<String, Object> variables = Map.of("test", "test",
"secret", Map.of("type", EncryptedString.TYPE, "value", encryptedSecret));
String render = runContext.render("What ? {{secret}}", variables);
assertThat(render, is(("What ? It's a secret")));
}
}

View File

@@ -4,6 +4,7 @@ import io.kestra.core.junit.annotations.KestraTest;
import io.kestra.core.models.executions.Execution;
import io.kestra.core.models.flows.DependsOn;
import io.kestra.core.models.flows.Input;
import io.kestra.core.models.flows.Type;
import io.kestra.core.models.flows.input.FileInput;
import io.kestra.core.models.flows.input.InputAndValue;
import io.kestra.core.models.flows.input.StringInput;
@@ -198,35 +199,22 @@ class FlowInputOutputTest {
}
@Test
void shouldDeleteFileInputAfterValidationGivenDeleteTrue() throws IOException {
void shouldNotUploadFileInputAfterValidation() throws IOException {
// Given
FileInput input = FileInput.builder()
FileInput input = FileInput
.builder()
.id("input")
.type(Type.FILE)
.build();
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, true);
List<InputAndValue> values = flowInputOutput.validateExecutionInputs(List.of(input), DEFAULT_TEST_EXECUTION, data).block();
// Then
Assertions.assertFalse(storageInterface.exists(null, URI.create(values.get(0).value().toString())));
}
@Test
void shouldNotDeleteFileInputAfterValidationGivenDeleteFalse() throws IOException {
// Given
FileInput input = FileInput.builder()
.id("input")
.build();
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, false);
// Then
Assertions.assertTrue(storageInterface.exists(null, URI.create(values.get(0).value().toString())));
Assertions.assertNull(values.getFirst().exception());
Assertions.assertFalse(storageInterface.exists(null, URI.create(values.getFirst().value().toString())));
}
private static final class MemoryCompletedFileUpload implements CompletedFileUpload {

View File

@@ -3,16 +3,23 @@ package io.kestra.core.runners;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.CharStreams;
import io.kestra.core.models.executions.Execution;
import io.kestra.core.models.executions.LogEntry;
import io.kestra.core.models.flows.Flow;
import io.kestra.core.models.flows.State;
import io.kestra.core.queues.QueueException;
import io.kestra.core.queues.QueueFactoryInterface;
import io.kestra.core.queues.QueueInterface;
import io.kestra.core.repositories.FlowRepositoryInterface;
import io.kestra.core.storages.StorageInterface;
import io.kestra.core.utils.TestsUtils;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import org.jcodings.util.Hash;
import org.junit.jupiter.api.Test;
import jakarta.validation.ConstraintViolationException;
import reactor.core.publisher.Flux;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
@@ -22,17 +29,19 @@ import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.*;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class InputsTest extends AbstractMemoryRunnerTest {
@Inject
@Named(QueueFactoryInterface.WORKERTASKLOG_NAMED)
private QueueInterface<LogEntry> logQueue;
public static Map<String, Object> inputs = ImmutableMap.<String, Object>builder()
.put("string", "myString")
.put("enum", "ENUM_VALUE")
@@ -351,4 +360,22 @@ public class InputsTest extends AbstractMemoryRunnerTest {
assertThat(((Map<?, ?>) execution.getInputs().get("json")).size(), is(0));
assertThat((String) execution.findTaskRunsByTaskId("jsonOutput").getFirst().getOutputs().get("value"), is("{}"));
}
@Test
void shouldNotLogSecretInput() throws TimeoutException, QueueException {
Flux<LogEntry> receive = TestsUtils.receive(logQueue, l -> {});
Execution execution = runnerUtils.runOne(
null,
"io.kestra.tests",
"input-log-secret"
);
assertThat(execution.getTaskRunList(), hasSize(1));
assertThat(execution.getState().getCurrent(), is(State.Type.SUCCESS));
var logEntry = receive.blockLast();
assertThat(logEntry, notNullValue());
assertThat(logEntry.getMessage(), is("This is my secret: ********"));
}
}

View File

@@ -123,10 +123,10 @@ class YamlFlowParserTest {
void inputs() {
Flow flow = this.parse("flows/valids/inputs.yaml");
assertThat(flow.getInputs().size(), is(28));
assertThat(flow.getInputs().stream().filter(Input::getRequired).count(), is(10L));
assertThat(flow.getInputs().size(), is(29));
assertThat(flow.getInputs().stream().filter(Input::getRequired).count(), is(11L));
assertThat(flow.getInputs().stream().filter(r -> !r.getRequired()).count(), is(18L));
assertThat(flow.getInputs().stream().filter(r -> r.getDefaults() != null).count(), is(2L));
assertThat(flow.getInputs().stream().filter(r -> r.getDefaults() != null).count(), is(3L));
assertThat(flow.getInputs().stream().filter(r -> r instanceof StringInput && ((StringInput)r).getValidator() != null).count(), is(1L));
}

View File

@@ -26,7 +26,7 @@ public class OutputValuesTest extends AbstractMemoryRunnerTest {
assertThat(execution.getState().getCurrent(), is(State.Type.SUCCESS));
assertThat(execution.getTaskRunList(), hasSize(1));
TaskRun outputValues = execution.getTaskRunList().getFirst();
Map<String, String> values = (Map<String, String>) outputValues.getOutputs().get("values");
Map<String, Object> values = (Map<String, Object>) outputValues.getOutputs().get("values");
assertThat(values.get("output1"), is("xyz"));
assertThat(values.get("output2"), is("abc"));
}

View File

@@ -216,7 +216,7 @@ public class PauseTest extends AbstractMemoryRunnerTest {
flow,
State.Type.RUNNING,
Flux.just(part1, part2)
);
).block();
execution = runnerUtils.awaitExecution(
e -> e.getId().equals(executionId) && e.getState().getCurrent() == State.Type.SUCCESS,
@@ -243,7 +243,7 @@ public class PauseTest extends AbstractMemoryRunnerTest {
ConstraintViolationException e = assertThrows(
ConstraintViolationException.class,
() -> executionService.resume(execution, flow, State.Type.RUNNING, Mono.empty())
() -> executionService.resume(execution, flow, State.Type.RUNNING, Mono.empty()).block()
);
assertThat(e.getMessage(), containsString("Invalid input for `asked`, missing required input, but received `null`"));

View File

@@ -1,6 +1,7 @@
package io.kestra.plugin.core.http;
import com.google.common.collect.ImmutableMap;
import io.kestra.core.junit.annotations.KestraTest;
import io.kestra.core.runners.RunContext;
import io.kestra.core.runners.RunContextFactory;
import io.kestra.core.storages.StorageInterface;
@@ -12,19 +13,16 @@ import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.client.exceptions.HttpClientResponseException;
import io.micronaut.runtime.server.EmbeddedServer;
import io.kestra.core.junit.annotations.KestraTest;
import jakarta.inject.Inject;
import org.apache.commons.io.IOUtils;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -135,7 +133,7 @@ class DownloadTest {
Download.Output output = task.run(runContext);
assertThat(output.getUri().toString(), endsWith("filename.jpg"));
assertThat(output.getUri().toString(), containsString("filename.jpg"));
}
@Controller()

View File

@@ -0,0 +1,12 @@
id: input-log-secret
namespace: io.kestra.tests
inputs:
- id: secret
type: SECRET
defaults: password
tasks:
- id: log-secret
type: io.kestra.plugin.core.log.Log
message: "This is my secret: {{inputs.secret}}"

View File

@@ -104,6 +104,11 @@ inputs:
value: value1
- key: key2
value: value2
# required true and an empty default value will only work if we correctly serialize default values which is what this input is about to test.
- name: empty
type: STRING
defaults: ''
required: true
tasks:
- id: string

View File

@@ -1,5 +1,5 @@
version=0.19.0-SNAPSHOT
version=0.19.7
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.priority=low
org.gradle.priority=low

View File

@@ -350,7 +350,10 @@ public abstract class AbstractJdbcLogRepository extends AbstractJdbcRepository i
DSLContext context = DSL.using(configuration);
return context.delete(this.jdbcRepository.getTable())
.where(field("execution_id", String.class).eq(execution.getId()))
// The deleted field is not used, so ti will always be false.
// We add it here to be sure to use the correct index.
.where(field("deleted", Boolean.class).eq(false))
.and(field("execution_id", String.class).eq(execution.getId()))
.execute();
});
}

View File

@@ -150,7 +150,10 @@ public abstract class AbstractJdbcMetricRepository extends AbstractJdbcRepositor
DSLContext context = DSL.using(configuration);
return context.delete(this.jdbcRepository.getTable())
.where(field("execution_id", String.class).eq(execution.getId()))
// The deleted field is not used, so ti will always be false.
// We add it here to be sure to use the correct index.
.where(field("deleted", Boolean.class).eq(false))
.and(field("execution_id", String.class).eq(execution.getId()))
.execute();
});
}
@@ -168,8 +171,7 @@ public abstract class AbstractJdbcMetricRepository extends AbstractJdbcRepositor
.getDslContextWrapper()
.transactionResult(configuration -> {
DSLContext context = DSL.using(configuration);
SelectConditionStep<Record1<Object>> select = DSL
.using(configuration)
SelectConditionStep<Record1<Object>> select = context
.selectDistinct(field(field))
.from(this.jdbcRepository.getTable())
.where(this.defaultFilter(tenantId));
@@ -185,8 +187,7 @@ public abstract class AbstractJdbcMetricRepository extends AbstractJdbcRepositor
.getDslContextWrapper()
.transactionResult(configuration -> {
DSLContext context = DSL.using(configuration);
SelectConditionStep<Record1<Object>> select = DSL
.using(configuration)
SelectConditionStep<Record1<Object>> select = context
.select(field("value"))
.from(this.jdbcRepository.getTable())
.where(this.defaultFilter(tenantId));

View File

@@ -4,6 +4,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.CaseFormat;
import io.kestra.core.exceptions.DeserializationException;
import io.kestra.core.metrics.MetricRegistry;
import io.kestra.core.models.executions.Execution;
import io.kestra.core.queues.QueueException;
import io.kestra.core.queues.QueueInterface;
@@ -64,6 +65,8 @@ public abstract class JdbcQueue<T> implements QueueInterface<T> {
protected final MessageProtectionConfiguration messageProtectionConfiguration;
private final MetricRegistry metricRegistry;
protected final Table<Record> table;
protected final JdbcQueueIndexer jdbcQueueIndexer;
@@ -80,6 +83,7 @@ public abstract class JdbcQueue<T> implements QueueInterface<T> {
this.dslContextWrapper = applicationContext.getBean(JooqDSLContextWrapper.class);
this.configuration = applicationContext.getBean(Configuration.class);
this.messageProtectionConfiguration = applicationContext.getBean(MessageProtectionConfiguration.class);
this.metricRegistry = applicationContext.getBean(MetricRegistry.class);
JdbcTableConfigs jdbcTableConfigs = applicationContext.getBean(JdbcTableConfigs.class);
@@ -97,6 +101,10 @@ public abstract class JdbcQueue<T> implements QueueInterface<T> {
}
if (messageProtectionConfiguration.enabled && bytes.length >= messageProtectionConfiguration.limit) {
metricRegistry
.counter(MetricRegistry.QUEUE_BIG_MESSAGE_COUNT, MetricRegistry.TAG_CLASS_NAME, cls.getName())
.increment();
// we let terminated execution messages to go through anyway
if (!(message instanceof Execution execution) || !execution.getState().isTerminated()) {
throw new MessageTooBigException("Message of size " + bytes.length + " has exceeded the configured limit of " + messageProtectionConfiguration.limit);

View File

@@ -9,6 +9,8 @@ import com.github.dockerjava.api.model.*;
import com.github.dockerjava.core.DefaultDockerClientConfig;
import com.github.dockerjava.core.DockerClientConfig;
import com.github.dockerjava.core.NameParser;
import com.github.dockerjava.transport.DomainSocket;
import com.sun.jna.LastErrorException;
import io.kestra.core.exceptions.IllegalVariableEvaluationException;
import io.kestra.core.models.annotations.Example;
import io.kestra.core.models.annotations.Plugin;
@@ -27,7 +29,6 @@ import jakarta.validation.constraints.NotNull;
import lombok.*;
import lombok.experimental.SuperBuilder;
import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.ArchiveOutputStream;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
@@ -39,8 +40,8 @@ import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.time.Duration;
@@ -331,7 +332,8 @@ public class Docker extends TaskRunner {
String image = runContext.render(this.image, additionalVars);
try (DockerClient dockerClient = dockerClient(runContext, image)) {
String resolvedHost = DockerService.findHost(runContext, this.host);
try (DockerClient dockerClient = dockerClient(runContext, image, resolvedHost)) {
// pull image
if (this.getPullPolicy() != PullPolicy.NEVER) {
pullImage(dockerClient, image, this.getPullPolicy(), logger);
@@ -530,6 +532,21 @@ public class Docker extends TaskRunner {
}
}
} catch (RuntimeException e) {
try {
if (e.getCause() instanceof IOException io &&
io.getCause() instanceof LastErrorException socketException &&
socketException.getMessage().contains("No such file or directory") &&
Socket.class.isAssignableFrom(Class.forName(io.getStackTrace()[0].getClassName()))) {
throw new IllegalStateException("Docker socket is not accessible or not found. " +
"Please make sure you properly mounted the Docker socket into your Kestra container (`-v /var/run/docker.sock:/var/run/docker.sock`) and that your user or group has at least the read and write privilege. " +
"Tried socket: " + resolvedHost, e);
}
} catch (ClassNotFoundException ignored) {
// If we can't check if the stacktrace class is a Socket, we just ignore the exception
throw e;
}
throw e;
}
}
@@ -562,9 +579,9 @@ public class Docker extends TaskRunner {
return vars;
}
private DockerClient dockerClient(RunContext runContext, String image) throws IOException, IllegalVariableEvaluationException {
private DockerClient dockerClient(RunContext runContext, String image, String host) throws IOException, IllegalVariableEvaluationException {
DefaultDockerClientConfig.Builder dockerClientConfigBuilder = DefaultDockerClientConfig.createDefaultConfigBuilder()
.withDockerHost(DockerService.findHost(runContext, this.host));
.withDockerHost(host);
if (this.getConfig() != null || this.getCredentials() != null) {
Path config = DockerService.createConfig(

View File

@@ -161,7 +161,7 @@ public class LocalStorage implements StorageInterface {
}
}
return URI.create("kestra://" + uri.getPath());
return URI.create("kestra://" + uri.getRawPath());
}
@Override
@@ -237,7 +237,7 @@ public class LocalStorage implements StorageInterface {
Path prefix = (tenantId == null) ?
basePath.toAbsolutePath() :
Path.of(basePath.toAbsolutePath().toString(), tenantId);
return URI.create("kestra:///" + prefix.relativize(path));
return URI.create("kestra:///" + prefix.relativize(path).toString().replace("\\", "/"));
}
private void parentTraversalGuard(URI uri) {

View File

@@ -10,7 +10,8 @@
"preview": "vite preview",
"test:unit": "vitest run",
"test:lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix"
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix",
"translations:check": "node ./src/translations/check.js"
},
"dependencies": {
"@js-joda/core": "^5.6.3",

View File

@@ -0,0 +1,53 @@
<svg width="169" height="146" viewBox="0 0 169 146" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.4" d="M129.725 83.5475C123.348 107.696 98.6012 122.103 74.4526 115.725C50.3039 109.348 35.8975 84.6014 42.2749 60.4528C48.6524 36.3041 73.3987 21.8977 97.5473 28.2752C121.696 34.6526 136.102 59.3989 129.725 83.5475Z" fill="#1C1E27" stroke="#E93ED1" stroke-linejoin="round"/>
<g filter="url(#filter0_d_3247_30504)">
<path d="M127.096 42.8848C130.859 48.1869 133.626 54.2556 135.113 60.8241L134.684 60.9214C135.393 64.0538 135.809 67.3012 135.9 70.6344C135.991 73.9675 135.754 77.2329 135.217 80.3994L135.651 80.473C134.525 87.1131 132.095 93.324 128.627 98.8241L128.254 98.5893C124.76 104.131 120.203 108.944 114.861 112.737L115.116 113.096C109.814 116.859 103.745 119.626 97.1762 121.113L97.079 120.684C93.9466 121.393 90.6991 121.809 87.366 121.9C84.0328 121.991 80.7675 121.754 77.601 121.217L77.5274 121.651C70.8873 120.525 64.6763 118.095 59.1763 114.627L59.4111 114.254C53.8696 110.76 49.056 106.203 45.2638 100.861L44.9048 101.116C41.1411 95.8135 38.3747 89.7448 36.8874 83.1762L37.3167 83.079C36.6075 79.9466 36.1916 76.6991 36.1004 73.366C36.0091 70.0328 36.2468 66.7675 36.7836 63.6009L36.3496 63.5273C37.4753 56.8873 39.9057 50.6763 43.3737 45.1763L43.7461 45.4111C47.2404 39.8695 51.7975 35.0559 57.1396 31.2638L56.8848 30.9048C62.1869 27.141 68.2556 24.3746 74.8242 22.8873L74.9214 23.3167C78.0538 22.6074 81.3013 22.1916 84.6344 22.1003C87.9676 22.0091 91.2329 22.2467 94.3994 22.7836L94.473 22.3495C101.113 23.4753 107.324 25.9056 112.824 29.3737L112.589 29.7461C118.131 33.2404 122.944 37.7975 126.737 43.1396L127.096 42.8848Z" stroke="#9470FF" stroke-width="0.880475" stroke-linejoin="round" stroke-dasharray="21.13 21.13" shape-rendering="crispEdges"/>
</g>
<line x1="165.701" y1="72.5" x2="141.883" y2="72.5" stroke="#FD7278" stroke-dasharray="2 2"/>
<line x1="42.6736" y1="36.2307" x2="26.1508" y2="19.0765" stroke="#3991FF" stroke-dasharray="2 2"/>
<line y1="-0.5" x2="23.8174" y2="-0.5" transform="matrix(0.73486 -0.678218 -0.678218 -0.73486 130.917 35.3833)" stroke="#3991FF" stroke-dasharray="2 2"/>
<line x1="132.256" y1="118.383" x2="148.779" y2="135.537" stroke="#3991FF" stroke-dasharray="2 2"/>
<line y1="-0.5" x2="23.8174" y2="-0.5" transform="matrix(-0.73486 0.678218 0.678218 0.73486 44.0134 119.23)" stroke="#3991FF" stroke-dasharray="2 2"/>
<g filter="url(#filter1_dii_3247_30504)">
<path d="M74.9999 70.625C80.0599 70.625 84.1666 66.425 84.1666 61.25C84.1666 56.075 80.0599 51.875 74.9999 51.875C69.9399 51.875 65.8333 56.075 65.8333 61.25C65.8333 66.425 69.9399 70.625 74.9999 70.625Z" fill="#ED3ED5"/>
<path d="M102.5 76.25H93.3333C91.3166 76.25 89.6666 77.9375 89.6666 80V89.375C89.6666 91.4375 91.3166 93.125 93.3333 93.125H102.5C104.517 93.125 106.167 91.4375 106.167 89.375V80C106.167 77.9375 104.517 76.25 102.5 76.25Z" fill="#ED3ED5"/>
<path d="M96.4683 64.4375C97.2016 64.7937 97.9899 65 98.8333 65C101.858 65 104.333 62.4687 104.333 59.375C104.333 56.2813 101.858 53.75 98.8333 53.75C95.8083 53.75 93.3333 56.2813 93.3333 59.375C93.3333 60.2375 93.5349 61.0437 93.8833 61.7937L75.5316 80.5625C74.7983 80.2062 74.0099 80 73.1666 80C70.1416 80 67.6666 82.5313 67.6666 85.625C67.6666 88.7187 70.1416 91.25 73.1666 91.25C76.1916 91.25 78.6666 88.7187 78.6666 85.625C78.6666 84.7625 78.4649 83.9562 78.1166 83.2062L96.4683 64.4375Z" fill="#ED3ED5"/>
</g>
<line x1="86.5" y1="126.021" x2="86.5" y2="134.802" stroke="#FD7278" stroke-dasharray="2 2"/>
<line x1="86.5" y1="7.27393" x2="86.5" y2="16.0542" stroke="#FD7278" stroke-dasharray="2 2"/>
<line x1="30.1165" y1="72.5" x2="6.29907" y2="72.5" stroke="#FD7278" stroke-dasharray="2 2"/>
<defs>
<filter id="filter0_d_3247_30504" x="32.9997" y="21.6411" width="106.001" height="106.001" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="2.64143"/>
<feGaussianBlur stdDeviation="1.32071"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.432266 0 0 0 0 0.00354165 0 0 0 0 0.846458 0 0 0 1 0"/>
<feBlend mode="screen" in2="BackgroundImageFix" result="effect1_dropShadow_3247_30504"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_3247_30504" result="shape"/>
</filter>
<filter id="filter1_dii_3247_30504" x="50.8333" y="36.875" width="70.3333" height="71.25" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="7.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.950882 0 0 0 0 0.165557 0 0 0 0 0.859261 0 0 0 0.62 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_3247_30504"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_3247_30504" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="4"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.108171 0 0 0 0 0.108171 0 0 0 0 0.108171 0 0 0 0.35 0"/>
<feBlend mode="normal" in2="shape" result="effect2_innerShadow_3247_30504"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="2"/>
<feGaussianBlur stdDeviation="3"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.45 0"/>
<feBlend mode="plus-lighter" in2="effect2_innerShadow_3247_30504" result="effect3_innerShadow_3247_30504"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@@ -0,0 +1,53 @@
<svg width="169" height="146" viewBox="0 0 169 146" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.4" d="M129.725 83.5475C123.348 107.696 98.6012 122.103 74.4526 115.725C50.3039 109.348 35.8975 84.6014 42.2749 60.4528C48.6524 36.3041 73.3987 21.8977 97.5473 28.2752C121.696 34.6526 136.102 59.3989 129.725 83.5475Z" fill="#D1CFE9" stroke="#E93ED1" stroke-linejoin="round"/>
<g filter="url(#filter0_d_3247_30783)">
<path d="M127.096 42.8848C130.859 48.1869 133.626 54.2556 135.113 60.8241L134.684 60.9214C135.393 64.0538 135.809 67.3012 135.9 70.6344C135.991 73.9675 135.754 77.2329 135.217 80.3994L135.651 80.473C134.525 87.1131 132.095 93.324 128.627 98.8241L128.254 98.5893C124.76 104.131 120.203 108.944 114.861 112.737L115.116 113.096C109.814 116.859 103.745 119.626 97.1762 121.113L97.079 120.684C93.9466 121.393 90.6991 121.809 87.366 121.9C84.0328 121.991 80.7675 121.754 77.601 121.217L77.5274 121.651C70.8873 120.525 64.6763 118.095 59.1763 114.627L59.4111 114.254C53.8696 110.76 49.056 106.203 45.2638 100.861L44.9048 101.116C41.1411 95.8135 38.3747 89.7448 36.8874 83.1762L37.3167 83.079C36.6075 79.9466 36.1916 76.6991 36.1004 73.366C36.0091 70.0328 36.2468 66.7675 36.7836 63.6009L36.3496 63.5273C37.4753 56.8873 39.9057 50.6763 43.3737 45.1763L43.7461 45.4111C47.2404 39.8695 51.7975 35.0559 57.1396 31.2638L56.8848 30.9048C62.1869 27.141 68.2556 24.3746 74.8242 22.8873L74.9214 23.3167C78.0538 22.6074 81.3013 22.1916 84.6344 22.1003C87.9676 22.0091 91.2329 22.2467 94.3994 22.7836L94.473 22.3495C101.113 23.4753 107.324 25.9056 112.824 29.3737L112.589 29.7461C118.131 33.2404 122.944 37.7975 126.737 43.1396L127.096 42.8848Z" stroke="#9470FF" stroke-width="0.880475" stroke-linejoin="round" stroke-dasharray="21.13 21.13" shape-rendering="crispEdges"/>
</g>
<line x1="165.701" y1="72.5" x2="141.883" y2="72.5" stroke="#FD7278" stroke-dasharray="2 2"/>
<line x1="42.6736" y1="36.2307" x2="26.1508" y2="19.0765" stroke="#3991FF" stroke-dasharray="2 2"/>
<line y1="-0.5" x2="23.8174" y2="-0.5" transform="matrix(0.73486 -0.678218 -0.678218 -0.73486 130.917 35.3833)" stroke="#3991FF" stroke-dasharray="2 2"/>
<line x1="132.256" y1="118.383" x2="148.779" y2="135.537" stroke="#3991FF" stroke-dasharray="2 2"/>
<line y1="-0.5" x2="23.8174" y2="-0.5" transform="matrix(-0.73486 0.678218 0.678218 0.73486 44.0134 119.23)" stroke="#3991FF" stroke-dasharray="2 2"/>
<g filter="url(#filter1_dii_3247_30783)">
<path d="M74.9999 70.625C80.0599 70.625 84.1666 66.425 84.1666 61.25C84.1666 56.075 80.0599 51.875 74.9999 51.875C69.9399 51.875 65.8333 56.075 65.8333 61.25C65.8333 66.425 69.9399 70.625 74.9999 70.625Z" fill="#ED3ED5"/>
<path d="M102.5 76.25H93.3333C91.3166 76.25 89.6666 77.9375 89.6666 80V89.375C89.6666 91.4375 91.3166 93.125 93.3333 93.125H102.5C104.517 93.125 106.167 91.4375 106.167 89.375V80C106.167 77.9375 104.517 76.25 102.5 76.25Z" fill="#ED3ED5"/>
<path d="M96.4683 64.4375C97.2016 64.7937 97.9899 65 98.8333 65C101.858 65 104.333 62.4687 104.333 59.375C104.333 56.2813 101.858 53.75 98.8333 53.75C95.8083 53.75 93.3333 56.2813 93.3333 59.375C93.3333 60.2375 93.5349 61.0437 93.8833 61.7937L75.5316 80.5625C74.7983 80.2062 74.0099 80 73.1666 80C70.1416 80 67.6666 82.5313 67.6666 85.625C67.6666 88.7187 70.1416 91.25 73.1666 91.25C76.1916 91.25 78.6666 88.7187 78.6666 85.625C78.6666 84.7625 78.4649 83.9562 78.1166 83.2062L96.4683 64.4375Z" fill="#ED3ED5"/>
</g>
<line x1="86.5" y1="126.021" x2="86.5" y2="134.802" stroke="#FD7278" stroke-dasharray="2 2"/>
<line x1="86.5" y1="7.27393" x2="86.5" y2="16.0542" stroke="#FD7278" stroke-dasharray="2 2"/>
<line x1="30.1165" y1="72.5" x2="6.29907" y2="72.5" stroke="#FD7278" stroke-dasharray="2 2"/>
<defs>
<filter id="filter0_d_3247_30783" x="32.9997" y="21.6411" width="106.001" height="106.001" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="2.64143"/>
<feGaussianBlur stdDeviation="1.32071"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.432266 0 0 0 0 0.00354165 0 0 0 0 0.846458 0 0 0 1 0"/>
<feBlend mode="screen" in2="BackgroundImageFix" result="effect1_dropShadow_3247_30783"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_3247_30783" result="shape"/>
</filter>
<filter id="filter1_dii_3247_30783" x="50.8333" y="36.875" width="70.3333" height="71.25" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="7.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.950882 0 0 0 0 0.165557 0 0 0 0 0.859261 0 0 0 0.05 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_3247_30783"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_3247_30783" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="4"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.108171 0 0 0 0 0.108171 0 0 0 0 0.108171 0 0 0 0.35 0"/>
<feBlend mode="normal" in2="shape" result="effect2_innerShadow_3247_30783"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="2"/>
<feGaussianBlur stdDeviation="3"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.45 0"/>
<feBlend mode="plus-lighter" in2="effect2_innerShadow_3247_30783" result="effect3_innerShadow_3247_30783"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@@ -10,10 +10,15 @@
>
<template #label>
<component :is="embedActiveTab || tab.disabled || tab.locked ? 'a' : 'router-link'" @click="embeddedTabChange(tab)" :to="embedActiveTab ? undefined : to(tab)" :data-test-id="tab.name">
<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>
<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>
</component>
</template>
</el-tab-pane>
@@ -133,6 +138,7 @@
},
to(tab) {
if (this.activeTab === tab) {
this.setActiveName()
return this.$route;
} else {
return {
@@ -224,4 +230,3 @@
flex-direction: column;
}
</style>

View File

@@ -80,12 +80,14 @@
</bulk-select>
</template>
<el-table-column
v-if="visibleColumns.triggerId"
prop="triggerId"
sortable="custom"
:sort-orders="['ascending', 'descending']"
:label="$t('id')"
/>
<el-table-column
v-if="visibleColumns.flowId"
prop="flowId"
sortable="custom"
:sort-orders="['ascending', 'descending']"
@@ -105,6 +107,7 @@
</template>
</el-table-column>
<el-table-column
v-if="visibleColumns.namespace"
prop="namespace"
sortable="custom"
:sort-orders="['ascending', 'descending']"
@@ -115,7 +118,7 @@
</template>
</el-table-column>
<el-table-column :label="$t('current execution')">
<el-table-column v-if="visibleColumns.executionId" :label="$t('current execution')">
<template #default="scope">
<router-link
v-if="scope.row.executionId"
@@ -126,7 +129,7 @@
</template>
</el-table-column>
<el-table-column :label="$t('state')">
<el-table-column v-if="visibleColumns.executionCurrentState" :label="$t('state')">
<template #default="scope">
<status
v-if="scope.row.executionCurrentState"
@@ -135,7 +138,7 @@
/>
</template>
</el-table-column>
<el-table-column prop="workerId" :label="$t('workerId')">
<el-table-column v-if="visibleColumns.workerId" prop="workerId" :label="$t('workerId')">
<template #default="scope">
<id
:value="scope.row.workerId"
@@ -143,22 +146,22 @@
/>
</template>
</el-table-column>
<el-table-column :label="$t('date')">
<el-table-column v-if="visibleColumns.date" :label="$t('date')">
<template #default="scope">
<date-ago :inverted="true" :date="scope.row.date" />
</template>
</el-table-column>
<el-table-column :label="$t('updated date')">
<el-table-column v-if="visibleColumns.updatedDate" :label="$t('updated date')">
<template #default="scope">
<date-ago :inverted="true" :date="scope.row.updatedDate" />
</template>
</el-table-column>
<el-table-column :label="$t('next execution date')">
<el-table-column v-if="visibleColumns.nextExecutionDate" :label="$t('next execution date')">
<template #default="scope">
<date-ago :inverted="true" :date="scope.row.nextExecutionDate" />
</template>
</el-table-column>
<el-table-column :label="$t('evaluation lock date')">
<el-table-column v-if="visibleColumns.evaluateRunningDate" :label="$t('evaluation lock date')">
<template #default="scope">
<date-ago :inverted="true" :date="scope.row.evaluateRunningDate" />
</template>
@@ -475,6 +478,25 @@
const disabled = this.state === "DISABLED" ? true : false;
return all.filter(trigger => trigger.disabled === disabled);
},
visibleColumns() {
const columns = [
{prop: "triggerId", label: this.$t("id")},
{prop: "flowId", label: this.$t("flow")},
{prop: "namespace", label: this.$t("namespace")},
{prop: "executionId", label: this.$t("current execution")},
{prop: "executionCurrentState", label: this.$t("state")},
{prop: "workerId", label: this.$t("workerId")},
{prop: "date", label: this.$t("date")},
{prop: "updatedDate", label: this.$t("updated date")},
{prop: "nextExecutionDate", label: this.$t("next execution date")},
{prop: "evaluateRunningDate", label: this.$t("evaluation lock date")},
];
return columns.reduce((acc, column) => {
acc[column.prop] = this.triggersMerged.some(trigger => trigger[column.prop]);
return acc;
}, {});
}
}
};

View File

@@ -6,7 +6,7 @@
width="222.67px"
height="125px"
loading="lazy"
:src="$store.getters['doc/resourceUrl']('/v1/docs/tutorial/logos/logo-dark-version.png')"
:src="$store.getters['doc/resourceUrl']('/docs/tutorial/logos/logo-dark-version.png')"
alt="Dark version logo"
>
<p class="title">
@@ -23,7 +23,7 @@
width="222.67px"
height="125px"
loading="lazy"
:src="$store.getters['doc/resourceUrl']('/v1/docs/tutorial/logos/logo-light-version.png')"
:src="$store.getters['doc/resourceUrl']('/docs/tutorial/logos/logo-light-version.png')"
alt="Light version logo"
>
<p class="title">
@@ -40,7 +40,7 @@
width="222.67px"
height="125px"
loading="lazy"
:src="$store.getters['doc/resourceUrl']('/v1/docs/tutorial/logos/logo-monogram-version.png')"
:src="$store.getters['doc/resourceUrl']('/docs/tutorial/logos/logo-monogram-version.png')"
alt="Monogram version logo"
>
<p class="title">

View File

@@ -48,7 +48,7 @@
<el-col :xs="24" :sm="8" :lg="4">
<refresh-button
class="float-right"
@refresh="fetchAll()"
@refresh="refresh()"
:can-auto-refresh="canAutoRefresh"
/>
</el-col>
@@ -61,6 +61,7 @@
<Card
:icon="CheckBold"
:label="t('dashboard.success_ratio')"
:tooltip="t('dashboard.success_ratio_tooltip')"
:value="stats.success"
:redirect="{
name: 'executions/list',
@@ -77,6 +78,7 @@
<Card
:icon="Alert"
:label="t('dashboard.failure_ratio')"
:tooltip="t('dashboard.failure_ratio_tooltip')"
:value="stats.failed"
:redirect="{
name: 'executions/list',
@@ -140,7 +142,10 @@
v-model="descriptionDialog"
:title="$t('description')"
>
<Markdown :source="description" class="p-4 description" />
<Markdown
:source="description"
class="p-4 description"
/>
</el-dialog>
</span>
@@ -197,7 +202,6 @@
import {useI18n} from "vue-i18n";
import moment from "moment";
import _cloneDeep from "lodash/cloneDeep";
import {apiUrl} from "override/utils/route";
import State from "../../utils/state";
@@ -228,6 +232,7 @@
import BookOpenOutline from "vue-material-design-icons/BookOpenOutline.vue";
import permission from "../../models/permission.js";
import action from "../../models/action.js";
import {storageKeys} from "../../utils/constants";
const router = useRouter();
const route = useRoute();
@@ -235,6 +240,7 @@
const {t} = useI18n({useScope: "global"});
const user = store.getters["auth/user"];
const defaultNamespace = localStorage.getItem(storageKeys.DEFAULT_NAMESPACE) || null;
const props = defineProps({
embed: {
type: Boolean,
@@ -254,6 +260,10 @@
required: false,
default: null,
},
restoreURL:{
type: Boolean,
default: true,
}
});
const descriptionDialog = ref(false);
@@ -271,6 +281,13 @@
scope: ["USER"],
});
const refresh = async () => {
await updateParams({
startDate: filters.value.startDate,
endDate: moment().toISOString(true),
});
fetchAll();
};
const canAutoRefresh = ref(false);
const toggleAutoRefresh = (event) => {
canAutoRefresh.value = event;
@@ -290,29 +307,45 @@
const executions = ref({raw: {}, all: {}, yesterday: {}, today: {}});
const stats = computed(() => {
const counts = executions?.value?.all?.executionCounts || {};
const total = Object.values(counts).reduce((sum, count) => sum + count, 0);
const terminatedStates = State.getTerminatedStates();
const statesToCount = Object.fromEntries(
Object.entries(counts).filter(([key]) =>
terminatedStates.includes(key),
),
);
function percentage(count, total) {
return total ? ((count / total) * 100).toFixed(2) : "0.00";
}
const total = Object.values(statesToCount).reduce(
(sum, count) => sum + count,
0,
);
const successStates = ["SUCCESS", "CANCELLED", "WARNING"];
const failedStates = ["FAILED", "KILLED", "RETRIED"];
const sumStates = (states) =>
states.reduce((sum, state) => sum + (statesToCount[state] || 0), 0);
const successRatio =
total > 0 ? (sumStates(successStates) / total) * 100 : 0;
const failedRatio = total > 0 ? (sumStates(failedStates) / total) * 100 : 0;
return {
total,
success: `${percentage(counts[State.SUCCESS] || 0, total)}%`,
failed: `${percentage(counts[State.FAILED] || 0, total)}%`,
success: `${successRatio.toFixed(2)}%`,
failed: `${failedRatio.toFixed(2)}%`,
};
});
const transformer = (data) => {
return data.reduce((accumulator, value) => {
if (!accumulator) accumulator = _cloneDeep(value);
else {
for (const key in value.executionCounts) {
accumulator.executionCounts[key] += value.executionCounts[key];
}
accumulator = accumulator || {executionCounts: {}, duration: {}};
for (const key in value.duration) {
accumulator.duration[key] += value.duration[key];
}
for (const key in value.executionCounts) {
accumulator.executionCounts[key] =
(accumulator.executionCounts[key] || 0) +
value.executionCounts[key];
}
for (const key in value.duration) {
accumulator.duration[key] =
(accumulator.duration[key] || 0) + value.duration[key];
}
return accumulator;
@@ -427,7 +460,15 @@
});
onBeforeMount(() => {
filters.value.namespace = route.query.namespace ?? null;
if (!route.query.namespace && props.restoreURL) {
router.replace({query: {...route.query, namespace: defaultNamespace}});
filters.value.namespace = route.query.namespace || defaultNamespace;
}
else {
filters.value.namespace = null
}
updateParams();
});
</script>

View File

@@ -2,7 +2,14 @@
<div class="p-4 card">
<div class="d-flex pb-2 justify-content-between">
<div class="d-flex align-items-center">
<component :is="icon" class="me-2 fs-4 icons" />
<el-tooltip
v-if="tooltip"
:content="tooltip"
popper-class="dashboard-card-tooltip"
>
<component :is="icon" class="me-2 fs-4 icons" />
</el-tooltip>
<component v-else :is="icon" class="me-2 fs-4 icons" />
<p class="m-0 fs-6 label">
{{ label }}
@@ -31,6 +38,10 @@
type: String,
required: true,
},
tooltip: {
type: String,
default: undefined,
},
value: {
type: [String, Number],
required: true,
@@ -63,3 +74,9 @@
}
}
</style>
<style lang="scss">
.dashboard-card-tooltip {
width: 300px;
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<div class="p-4">
<div class="d-flex flex justify-content-between pb-4">
<div>
<div class="p-4 responsive-container">
<div class="d-flex flex-wrap justify-content-between pb-4 info-container">
<div class="info-block">
<p class="m-0 fs-6">
<span class="fw-bold">{{ t("executions") }}</span>
<span class="fw-light small">
@@ -13,8 +13,8 @@
</p>
</div>
<div>
<div class="d-flex justify-content-end align-items-center">
<div class="switch-container">
<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"
@@ -35,7 +35,7 @@
</template>
<script setup>
import {computed, ref} from "vue";
import {computed, ref, onMounted, onUnmounted} from "vue";
import {useI18n} from "vue-i18n";
import moment from "moment";
@@ -50,6 +50,7 @@
import Check from "vue-material-design-icons/Check.vue";
const {t} = useI18n({useScope: "global"});
const isSmallScreen = ref(window.innerWidth < 610);
const props = defineProps({
data: {
@@ -106,9 +107,20 @@
};
});
onMounted(() => {
const handleResize = () => {
isSmallScreen.value = window.innerWidth < 610;
};
window.addEventListener("resize", handleResize);
onUnmounted(() => {
window.removeEventListener("resize", handleResize);
});
});
const options = computed(() =>
defaultConfig({
barThickness: 12,
barThickness: isSmallScreen.value ? 8 : 12,
skipNull: true,
borderSkipped: false,
borderColor: "transparent",
@@ -141,7 +153,7 @@
display: true,
stacked: true,
ticks: {
maxTicksLimit: 8,
maxTicksLimit: isSmallScreen.value ? 5 : 8,
callback: function (value) {
const label = this.getLabelForValue(value);
const date = moment(new Date(label));
@@ -156,7 +168,7 @@
},
y: {
title: {
display: true,
display: !isSmallScreen.value,
text: t("executions"),
},
grid: {
@@ -166,12 +178,12 @@
position: "left",
stacked: true,
ticks: {
maxTicksLimit: 8,
maxTicksLimit: isSmallScreen.value ? 5 : 8,
},
},
yB: {
title: {
display: duration.value,
display: duration.value && !isSmallScreen.value,
text: t("duration"),
},
grid: {
@@ -180,7 +192,7 @@
display: duration.value,
position: "right",
ticks: {
maxTicksLimit: 8,
maxTicksLimit: isSmallScreen.value ? 5 : 8,
callback: function (value) {
return `${this.getLabelForValue(value)}s`;
},
@@ -193,22 +205,65 @@
const duration = ref(true);
</script>
Copy code
<style lang="scss" scoped>
@import "@kestra-io/ui-libs/src/scss/variables";
$height: 200px;
.tall {
height: $height;
max-height: $height;
height: $height;
max-height: $height;
}
.small {
font-size: $font-size-xs;
color: $gray-700;
font-size: $font-size-xs;
color: $gray-700;
html.dark & {
color: $gray-300;
}
html.dark & {
color: $gray-300;
}
}
</style>
@media (max-width: 610px) {
.responsive-container {
padding: 2px;
}
.info-container {
flex-direction: column;
text-align: center;
}
.info-block {
margin-bottom: 15px;
}
.switch-container {
display: flex;
justify-content: center;
width: 100%;
}
.switch-content {
justify-content: center;
}
.fs-2 {
font-size: 1.5rem;
}
.fs-6 {
font-size: 0.875rem;
}
.small {
font-size: 0.75rem;
}
.pe-2 {
padding-right: 0.5rem;
}
}
</style>

View File

@@ -120,7 +120,7 @@
y: {
title: {
display: true,
text: t("executions"),
text: t("logs"),
},
grid: {
display: false,

View File

@@ -167,5 +167,12 @@ code {
.inprogress {
--el-table-tr-bg-color: var(--bs-body-bg) !important;
background: var(--bs-body-bg);
& a {
color: #8e71f7;
html.dark & {
color: #e0e0fc;
}
}
}
</style>

View File

@@ -7,7 +7,7 @@
<div class="pt-4">
<el-table
:data="executions.results"
class="inprogress"
class="nextscheduled"
:height="240"
>
<el-table-column class-name="next-toggle" width="50">
@@ -28,7 +28,10 @@
v-else
:model-value="!scope.row.disabled"
@change="
toggleState(scope.row.triggerContext);
toggleState(
scope.row.triggerContext,
!scope.row.disabled,
);
scope.row.disabled = !scope.row.disabled;
"
:active-icon="Check"
@@ -194,11 +197,8 @@
() => loadExecutions(),
);
const toggleState = (trigger) => {
store.dispatch("trigger/update", {
...trigger,
disabled: !trigger.disabled,
});
const toggleState = (trigger, disabled) => {
store.dispatch("trigger/update", {...trigger, disabled});
};
onBeforeMount(() => {
@@ -211,9 +211,16 @@ code {
color: var(--bs-code-color);
}
.inprogress {
.nextscheduled {
--el-table-tr-bg-color: var(--bs-body-bg) !important;
background: var(--bs-body-bg);
& a {
color: #8e71f7;
html.dark & {
color: #e0e0fc;
}
}
}
.next-toggle {

View File

@@ -4,7 +4,7 @@
:persistent="false"
transition=""
:hide-after="0"
:content="$t('change status tooltip')"
:content="$t('change state tooltip')"
raw-content
:placement="tooltipPosition"
>
@@ -15,7 +15,7 @@
:disabled="!enabled"
class="ms-0 me-1"
>
{{ $t('change status') }}
{{ $t('change state') }}
</component>
</el-tooltip>
@@ -25,7 +25,7 @@
</template>
<template #default>
<p v-html="$t('change execution status confirm', {id: execution.id})" />
<p v-html="$t('change execution state confirm', {id: execution.id})" />
<p>
Current status is : <status size="small" class="me-1" :status="execution.state.current" />
@@ -186,4 +186,4 @@
padding-left: 10px;
}
}
</style>
</style>

View File

@@ -5,7 +5,7 @@
@click="visible = !visible"
:disabled="!enabled"
>
<span v-if="component !== 'el-button'">{{ $t('change status') }}</span>
<span v-if="component !== 'el-button'">{{ $t('change_status') }}</span>
<el-dialog v-if="enabled && visible" v-model="visible" :id="uuid" destroy-on-close :append-to-body="true">
<template #header>
@@ -13,7 +13,7 @@
</template>
<template #default>
<p v-html="$t('change status confirm', {id: execution.id, task: taskRun.taskId})" />
<p v-html="$t('change state confirm', {id: execution.id, task: taskRun.taskId})" />
<p>
Current status is : <status size="small" class="me-1" :status="taskRun.state.current" />

View File

@@ -44,8 +44,9 @@
</el-form-item>
<el-form-item v-if="$route.name !== 'flows/update'">
<namespace-select
:value="selectedNamespace"
data-type="flow"
:value="$route.query.namespace"
:disabled="!!namespace"
@update:model-value="onDataTableValue('namespace', $event)"
/>
</el-form-item>
@@ -133,17 +134,10 @@
</el-form-item>
</template>
<template #top v-if="showStatChart()">
<state-global-chart
v-if="daily"
class="mb-4"
:ready="dailyReady"
:data="daily"
:start-date="startDate"
:end-date="endDate"
:namespace="namespace"
:flow-id="flowId"
/>
<template #top>
<el-card v-if="showStatChart()" shadow="never" class="mb-4">
<ExecutionsBar v-if="daily" :data="daily" :total="executionsCount" />
</el-card>
</template>
<template #table>
@@ -456,7 +450,6 @@
import Filters from "../saved-filters/Filters.vue";
import StatusFilterButtons from "../layout/StatusFilterButtons.vue"
import ScopeFilterButtons from "../layout/ScopeFilterButtons.vue"
import StateGlobalChart from "../../components/stats/StateGlobalChart.vue";
import Kicon from "../Kicon.vue"
import Labels from "../layout/Labels.vue"
import RestoreUrl from "../../mixins/restoreUrl";
@@ -471,6 +464,7 @@
import {ElMessageBox, ElSwitch, ElFormItem, ElAlert, ElCheckbox} from "element-plus";
import DateAgo from "../layout/DateAgo.vue";
import {h, ref} from "vue";
import ExecutionsBar from "../../components/dashboard/components/charts/executions/Bar.vue"
import {filterLabels} from "./utils"
@@ -488,13 +482,13 @@
Filters,
StatusFilterButtons,
ScopeFilterButtons,
StateGlobalChart,
Kicon,
Labels,
Id,
TriggerFlow,
TopNavBar,
LabelInput
LabelInput,
ExecutionsBar
},
emits: ["state-count"],
props: {
@@ -614,11 +608,6 @@
selectedStatus: undefined
};
},
beforeCreate(){
if(!this.$route.query.scope) {
this.$route.query.scope = this.namespace === "system" ? ["SYSTEM"] : ["USER"];
}
},
created() {
// allow to have different storage key for flow executions list
if (this.$route.name === "flows/update") {
@@ -694,6 +683,26 @@
};
});
},
executionsCount() {
return [...this.daily].reduce((a, b) => {
return a + Object.values(b.executionCounts).reduce((a, b) => a + b, 0);
}, 0);
},
selectedNamespace(){
return this.namespace !== null && this.namespace !== undefined ? this.namespace : this.$route.query?.namespace;
}
},
beforeRouteEnter(to, from, next) {
const defaultNamespace = localStorage.getItem(storageKeys.DEFAULT_NAMESPACE);
const query = {...to.query};
if (defaultNamespace) {
query.namespace = defaultNamespace;
} if (!query.scope) {
query.scope = defaultNamespace === "system" ? ["SYSTEM"] : ["USER"];
}
next(vm => {
vm.$router?.replace({query});
});
},
methods: {
executionParams(row) {
@@ -857,7 +866,7 @@
);
},
changeStatusToast() {
return this.$t("bulk change execution status", {"executionCount": this.queryBulkAction ? this.total : this.selection.length});
return this.$t("bulk change state", {"executionCount": this.queryBulkAction ? this.total : this.selection.length});
},
deleteExecutions() {
const includeNonTerminated = ref(false);

View File

@@ -68,6 +68,7 @@
:target-execution="execution"
:target-flow="flow"
:show-logs="taskTypeByTaskRunId[item.id] !== 'io.kestra.plugin.core.flow.ForEachItem' && taskTypeByTaskRunId[item.id] !== 'io.kestra.core.tasks.flows.ForEachItem'"
class="mh-100"
/>
</div>
</div>

View File

@@ -26,6 +26,7 @@
:total-count="countByLogLevel[logLevel]"
@previous="previousLogForLevel(logLevel)"
@next="nextLogForLevel(logLevel)"
@close="logCursor = undefined"
/>
</el-form-item>
<el-form-item>
@@ -37,7 +38,7 @@
<el-tooltip
:content="!raw_view ? $t('logs_view.raw_details') : $t('logs_view.compact_details')"
>
<el-button @click="setRawView()">
<el-button @click="toggleViewType">
{{ !raw_view ? $t('logs_view.raw') : $t('logs_view.compact') }}
</el-button>
</el-tooltip>
@@ -70,15 +71,36 @@
:target-flow="flow"
:show-progress-bar="false"
/>
<el-card v-else>
<template v-for="log in logs" :key="`${log.timestamp}-${log.taskRun}`">
<log-line
:level="level"
filter=""
:log="log"
title
/>
</template>
<el-card v-else class="attempt-wrapper">
<DynamicScroller
ref="logScroller"
:items="temporalLogs"
:min-item-size="50"
key-field="index"
class="log-lines"
:buffer="200"
:prerender="20"
>
<template #default="{item, active}">
<DynamicScrollerItem
:item="item"
:active="active"
:size-dependencies="[item.message]"
:data-index="item.index"
>
<log-line
@click="logCursor = item.index.toString()"
class="line"
:class="{['log-bg-' + cursorLogLevel?.toLowerCase()]: cursorLogLevel === item.level, 'opacity-40': cursorLogLevel && cursorLogLevel !== item.level}"
:cursor="item.index.toString() === logCursor"
:level="level"
:filter="filter"
:log="item"
title
/>
</DynamicScrollerItem>
</template>
</DynamicScroller>
</el-card>
</div>
</template>
@@ -91,6 +113,8 @@
import Kicon from "../Kicon.vue";
import LogLevelSelector from "../logs/LogLevelSelector.vue";
import LogLevelNavigator from "../logs/LogLevelNavigator.vue";
import {DynamicScroller, DynamicScrollerItem} from "vue-virtual-scroller";
import "vue-virtual-scroller/dist/vue-virtual-scroller.css"
import Collapse from "../layout/Collapse.vue";
import State from "../../utils/state";
import Utils from "../../utils/utils";
@@ -108,7 +132,9 @@
Download,
Magnify,
Collapse,
Restart
Restart,
DynamicScroller,
DynamicScrollerItem,
},
data() {
return {
@@ -125,10 +151,42 @@
this.level = (this.$route.query.level || localStorage.getItem("defaultLogLevel") || "INFO");
this.filter = (this.$route.query.q || undefined);
},
watch:{
level: {
handler() {
if (this.raw_view) {
this.$store.dispatch("execution/loadLogs", {
executionId: this.execution.id,
minLevel: this.level
})
}
}
},
logCursor(newValue) {
if (newValue !== undefined && this.raw_view) {
this.scrollToLog(newValue);
}
}
},
computed: {
State() {
return State
},
temporalLogs() {
if (!this.logs?.length) {
return [];
}
const filtered = this.logs.filter(log => {
if (!this.filter) return true;
return log.message?.toLowerCase().includes(this.filter.toLowerCase());
});
return filtered.map((logLine, index) => ({
...logLine,
index
}));
},
...mapState("execution", ["execution", "logs", "flow"]),
downloadName() {
return `kestra-execution-${this.$moment().format("YYYYMMDDHHmmss")}-${this.execution.id}.log`
@@ -140,13 +198,32 @@
return LogUtils.levelOrLower(this.level);
},
countByLogLevel() {
return Object.fromEntries(Object.entries(this.logIndicesByLevel).map(([level, indices]) => [level, indices.length]));
return Object.fromEntries(Object.entries(this.viewTypeAwareLogIndicesByLevel).map(([level, indices]) => [level, indices.length]));
},
cursorLogLevel() {
return Object.entries(this.logIndicesByLevel).find(([_, indices]) => indices.includes(this.logCursor))?.[0];
return Object.entries(this.viewTypeAwareLogIndicesByLevel).find(([_, indices]) => indices.includes(this.logCursor))?.[0];
},
cursorIdxForLevel() {
return this.logIndicesByLevel?.[this.cursorLogLevel]?.toSorted(this.sortLogsByViewOrder)?.indexOf(this.logCursor);
return this.viewTypeAwareLogIndicesByLevel?.[this.cursorLogLevel]?.toSorted(this.sortLogsByViewOrder)?.indexOf(this.logCursor);
},
temporalViewLogIndicesByLevel() {
const temporalViewLogIndicesByLevel = this.temporalLogs.reduce((acc, item) => {
if (!acc[item.level]) {
acc[item.level] = [];
}
acc[item.level].push(item.index.toString());
return acc;
}, {});
LogUtils.levelOrLower(undefined).forEach(level => {
if (!temporalViewLogIndicesByLevel[level]) {
temporalViewLogIndicesByLevel[level] = [];
}
});
return temporalViewLogIndicesByLevel
},
viewTypeAwareLogIndicesByLevel() {
return this.raw_view ? this.temporalViewLogIndicesByLevel : this.logIndicesByLevel;
}
},
methods: {
@@ -172,14 +249,9 @@
expandCollapseAll() {
this.$refs.logs.toggleExpandCollapseAll();
},
setRawView() {
toggleViewType() {
this.logCursor = undefined;
this.raw_view = !this.raw_view;
if(this.raw_view) {
this.$store.dispatch("execution/loadLogs", {
executionId: this.execution.id,
minLevel: this.level
})
}
},
sortLogsByViewOrder(a, b) {
const aSplit = a.split("/");
@@ -199,7 +271,7 @@
return Number.parseInt(taskRunIndexA) - Number.parseInt(taskRunIndexB);
},
previousLogForLevel(level) {
const logIndicesForLevel = this.logIndicesByLevel[level];
const logIndicesForLevel = this.viewTypeAwareLogIndicesByLevel[level];
if (this.logCursor === undefined) {
this.logCursor = logIndicesForLevel?.[logIndicesForLevel.length - 1];
return;
@@ -209,7 +281,7 @@
this.logCursor = sortedIndices?.[sortedIndices.indexOf(this.logCursor) - 1] ?? sortedIndices[sortedIndices.length - 1];
},
nextLogForLevel(level) {
const logIndicesForLevel = this.logIndicesByLevel[level];
const logIndicesForLevel = this.viewTypeAwareLogIndicesByLevel[level];
if (this.logCursor === undefined) {
this.logCursor = logIndicesForLevel?.[0];
return;
@@ -217,7 +289,54 @@
const sortedIndices = [...logIndicesForLevel, this.logCursor].filter(Utils.distinctFilter).sort(this.sortLogsByViewOrder);
this.logCursor = sortedIndices?.[sortedIndices.indexOf(this.logCursor) + 1] ?? sortedIndices[0];
},
scrollToLog(index) {
this.$refs.logScroller.scrollToItem(index);
}
}
};
</script>
<style lang="scss" scoped>
@import "@kestra-io/ui-libs/src/scss/variables";
.attempt-wrapper {
background-color: var(--bs-white);
:deep(.vue-recycle-scroller__item-view + .vue-recycle-scroller__item-view) {
border-top: 1px solid var(--bs-border-color);
}
html.dark & {
background-color: var(--bs-gray-100);
}
.attempt-wrapper & {
border-radius: .25rem;
}
}
.log-lines {
max-height: calc(100vh - 335px);
transition: max-height 0.2s ease-out;
margin-top: calc(var(--spacer) / 2);
.line {
padding: calc(var(--spacer) / 2);
&.cursor {
background-color: var(--bs-gray-300)
}
}
&::-webkit-scrollbar {
width: 5px;
}
&::-webkit-scrollbar-track {
background: var(--bs-gray-500);
}
&::-webkit-scrollbar-thumb {
background: var(--bs-primary);
}
}
</style>

View File

@@ -53,7 +53,7 @@
<p v-html="$t(replayOrRestart + ' confirm', {id: execution.id})" />
<el-form v-if="revisionsOptions && revisionsOptions.length > 1">
<p class="text-muted">
<p class="execution-description">
{{ $t("restart change revision") }}
</p>
<el-form-item :label="$t('revisions')">
@@ -227,3 +227,8 @@
},
};
</script>
<style scoped>
.execution-description {
color: var(--bs-gray-700);
}
</style>

View File

@@ -12,9 +12,9 @@
<el-button-group v-else-if="isURI(value)">
<a class="el-button el-button--small el-button--primary" :href="value" target="_blank">
<OpenInNew />
<OpenInNew /> &nbsp;
{{ $t('open') }}
</a>
</a>
</el-button-group>
<span v-else-if="value === null">

View File

@@ -373,4 +373,9 @@
.bordered {
border: 1px solid var(--bs-border-color)
}
.bordered > .el-collapse-item{
margin-bottom :0px !important
}
</style>

View File

@@ -62,8 +62,9 @@
} else if (this.$route.query.blueprintId && this.$route.query.blueprintSource) {
this.source = await this.queryBlueprint(this.$route.query.blueprintId)
} else {
const selectedNamespace = this.$route.query.namespace || "company.team";
this.source = `id: myflow
namespace: company.team
namespace: ${selectedNamespace}
tasks:
- id: hello
type: io.kestra.plugin.core.log.Log

View File

@@ -1,13 +0,0 @@
<template>
<logs-wrapper :restore-url="false" embed />
</template>
<script>
import LogsWrapper from "../logs/LogsWrapper.vue";
export default {
components: {
LogsWrapper,
},
};
</script>

View File

@@ -0,0 +1,75 @@
<template>
<div class="no-dependencies-container">
<div>
<img :src="flowImage" alt="No dependencies">
</div>
<div class="no-dependencies-message">
<p>{{ $t("flow-no-dependencies") }}</p>
</div>
<div class="no-dependencies-doc-message" :class="themeClass">
<p>
{{ $t("read-more") }}
<a
:href="dependenciesDocsUrl"
target="_blank"
rel="noopener noreferrer"
class="no-dependencies-doc-message doc-link"
>
{{ $t("flow-dependencies") }}
</a>
{{ $t("in-our-documentation") }}
</p>
</div>
</div>
</template>
<script>
import flowImageDark from "../../assets/onboarding/onboarding-flow-no-dependency-dark.svg"
import flowImageLight from "../../assets/onboarding/onboarding-flow-no-dependency-light.svg"
export default {
name: "NoDependencies",
computed: {
flowImage() {
return (localStorage.getItem("theme") || "light") === "light" ? flowImageLight : flowImageDark;
},
themeClass() {
return (localStorage.getItem("theme") || "light") === "light" ? "theme-light" : "theme-dark";
},
dependenciesDocsUrl() {
return "https://kestra.io/docs/ui/flows#dependencies";
},
}
};
</script>
<style scoped>
.no-dependencies-container {
padding: 180px;
text-align: center;
}
.no-dependencies-message {
font-weight: bold;
font-size: var(--el-font-size-small);
color: var(--el-text-color-regular);
margin-bottom: -10px;
}
.no-dependencies-doc-message {
font-weight: 200;
font-size: var(--el-font-size-extra-small);
}
.theme-light {
color: #000;
}
.theme-dark {
color: #fff;
}
.doc-link {
text-decoration: underline;
font-weight: bold;
color: var(--el-text-color-primary);
}
.doc-link:hover {
cursor: pointer;
}
</style>

View File

@@ -13,7 +13,7 @@
<script>
import Topology from "./Topology.vue";
import FlowRevisions from "./FlowRevisions.vue";
import FlowLogs from "./FlowLogs.vue";
import LogsWrapper from "../logs/LogsWrapper.vue"
import FlowExecutions from "./FlowExecutions.vue";
import RouteContext from "../../mixins/routeContext";
import {mapState} from "vuex";
@@ -22,6 +22,7 @@
import Tabs from "../Tabs.vue";
import Overview from "./Overview.vue";
import FlowDependencies from "./FlowDependencies.vue";
import FlowNoDependencies from "./FlowNoDependencies.vue";
import FlowMetrics from "./FlowMetrics.vue";
import FlowEditor from "./FlowEditor.vue";
import FlowTriggers from "./FlowTriggers.vue";
@@ -201,7 +202,11 @@
name: "triggers",
component: FlowTriggers,
title: this.$t("triggers"),
props: {
showTooltip: !this.flow.triggers || this.flow.triggers.length === 0
},
disabled: !this.flow.triggers,
hideTitle: !this.flow.triggers
});
}
@@ -216,8 +221,13 @@
) {
tabs.push({
name: "logs",
component: FlowLogs,
component: LogsWrapper,
title: this.$t("logs"),
props: {
showFilters: true,
restoreurl: false,
},
containerClass: "full-container p-4"
});
}
@@ -247,7 +257,7 @@
) {
tabs.push({
name: "dependencies",
component: FlowDependencies,
component: this.routeFlowDependencies,
title: this.$t("dependencies"),
count: this.dependenciesCount,
});
@@ -317,6 +327,9 @@
this.flow.namespace,
);
},
routeFlowDependencies() {
return this.dependenciesCount > 0 ? FlowDependencies : FlowNoDependencies;
}
},
unmounted() {
this.$store.commit("flow/setFlow", undefined);
@@ -331,4 +344,4 @@
.body-color {
color: var(--bs-body-color);
}
</style>
</style>

View File

@@ -6,7 +6,7 @@
</el-alert>
<el-form label-position="top" :model="inputs" ref="form" @submit.prevent="false">
<inputs-form :initial-inputs="flow.inputs" :flow="flow" v-model="inputs" :execute-clicked="executeClicked" />
<inputs-form :initial-inputs="flow.inputs" :flow="flow" v-model="inputs" :execute-clicked="executeClicked" @confirm="onSubmit($refs.form)" />
<el-collapse class="mt-4" v-model="collapseName">
<el-collapse-item :title="$t('advanced configuration')" name="advanced">
@@ -50,7 +50,7 @@
@click="onSubmit($refs.form); executeClicked = true;"
type="primary"
native-type="submit"
:disabled="flow.disabled || haveBadLabels"
:disabled="!flowCanBeExecuted"
>
{{ $t('launch execution') }}
</el-button>
@@ -111,6 +111,9 @@
haveBadLabels() {
return this.executionLabels.some(label => (label.key && !label.value) || (!label.key && label.value));
},
flowCanBeExecuted() {
return this.flow && !this.flow.disabled && !this.haveBadLabels;
}
},
methods: {
getExecutionLabels() {
@@ -152,7 +155,7 @@
return inputs;
},
onSubmit(formRef) {
if (formRef) {
if (formRef && this.flowCanBeExecuted) {
formRef.validate((valid) => {
if (!valid) {
return false;
@@ -165,9 +168,11 @@
newTab: this.newTab,
id: this.flow.id,
namespace: this.flow.namespace,
labels: this.executionLabels
.filter(label => label.key && label.value)
.map(label => `${label.key}:${label.value}`),
labels: [...new Set(
this.executionLabels
.filter(label => label.key && label.value)
.map(label => `${label.key}:${label.value}`)
)],
scheduleDate: this.$moment(this.scheduleDate).tz(localStorage.getItem(TIMEZONE_STORAGE_KEY) ?? moment.tz.guess()).toISOString(true),
nextStep: true
})
@@ -175,7 +180,6 @@
});
}
},
state(input) {
const required = input.required === undefined ? true : input.required;

View File

@@ -1,5 +1,5 @@
<template>
<top-nav-bar :title="routeInfo.title">
<top-nav-bar v-if="topbar" :title="routeInfo.title">
<template #additional-right>
<ul>
<li>
@@ -27,7 +27,7 @@
</router-link>
</li>
<li>
<router-link :to="{name: 'flows/create'}" v-if="canCreate">
<router-link :to="{name: 'flows/create', query: {namespace: $route.query.namespace}}" v-if="canCreate">
<el-button :icon="Plus" type="primary">
{{ $t('create') }}
</el-button>
@@ -36,7 +36,7 @@
</ul>
</template>
</top-nav-bar>
<section data-component="FILENAME_PLACEHOLDER" class="container" v-if="ready">
<section data-component="FILENAME_PLACEHOLDER" :class="{'container': topbar}" v-if="ready">
<div>
<data-table
@page-changed="onPageChanged"
@@ -49,8 +49,9 @@
</el-form-item>
<el-form-item>
<namespace-select
:value="selectedNamespace"
data-type="flow"
:value="$route.query.namespace"
:disabled="!!namespace"
@update:model-value="onDataTableValue('namespace', $event)"
/>
</el-form-item>
@@ -67,18 +68,22 @@
@update:model-value="onDataTableValue('labels', $event)"
/>
</el-form-item>
<el-form-item>
<el-switch
:model-value="showChart"
@update:model-value="onShowChartChange"
:active-text="$t('show chart')"
/>
</el-form-item>
<el-form-item>
<filters :storage-key="storageKeys.FLOWS_FILTERS" />
</el-form-item>
</template>
<template #top>
<state-global-chart
class="mb-4"
v-if="daily"
:ready="dailyReady"
:data="daily"
/>
<el-card v-if="showStatChart()" shadow="never" class="mb-4">
<ExecutionsBar :data="daily" :total="executionsCount" />
</el-card>
</template>
<template #table>
@@ -109,10 +114,10 @@
<el-button v-if="canDelete" @click="deleteFlows" :icon="TrashCan">
{{ $t('delete') }}
</el-button>
<el-button v-if="canUpdate" @click="enableFlows" :icon="FileDocumentCheckOutline">
<el-button v-if="canUpdate && anyFlowDisabled()" @click="enableFlows" :icon="FileDocumentCheckOutline">
{{ $t('enable') }}
</el-button>
<el-button v-if="canUpdate" @click="disableFlows" :icon="FileDocumentRemoveOutline">
<el-button v-if="canUpdate && anyFlowEnabled()" @click="disableFlows" :icon="FileDocumentRemoveOutline">
{{ $t('disable') }}
</el-button>
</bulk-select>
@@ -245,7 +250,6 @@
import DataTable from "../layout/DataTable.vue";
import SearchField from "../layout/SearchField.vue";
import StateChart from "../stats/StateChart.vue";
import StateGlobalChart from "../stats/StateGlobalChart.vue";
import Status from "../Status.vue";
import TriggerAvatar from "./TriggerAvatar.vue";
import MarkdownTooltip from "../layout/MarkdownTooltip.vue"
@@ -254,6 +258,7 @@
import Upload from "vue-material-design-icons/Upload.vue";
import LabelFilter from "../labels/LabelFilter.vue";
import ScopeFilterButtons from "../layout/ScopeFilterButtons.vue"
import ExecutionsBar from "../../components/dashboard/components/charts/executions/Bar.vue"
import {storageKeys} from "../../utils/constants";
export default {
@@ -265,7 +270,6 @@
DateAgo,
SearchField,
StateChart,
StateGlobalChart,
Status,
TriggerAvatar,
MarkdownTooltip,
@@ -274,7 +278,19 @@
Upload,
LabelFilter,
ScopeFilterButtons,
TopNavBar
TopNavBar,
ExecutionsBar
},
props: {
topbar: {
type: Boolean,
default: true
},
namespace: {
type: String,
required: false,
default: undefined
},
},
data() {
return {
@@ -285,6 +301,7 @@
lastExecutionByFlowReady: false,
dailyReady: false,
file: undefined,
showChart: ["true", null].includes(localStorage.getItem(storageKeys.SHOW_FLOWS_CHART)),
};
},
computed: {
@@ -318,20 +335,45 @@
},
canUpdate() {
return this.user && this.user.isAllowed(permission.FLOW, action.UPDATE, this.$route.query.namespace);
},
executionsCount() {
return [...this.daily].reduce((a, b) => {
return a + Object.values(b.executionCounts).reduce((a, b) => a + b, 0);
}, 0);
},
selectedNamespace(){
return this.namespace !== null && this.namespace !== undefined ? this.namespace : this.$route.query?.namespace;
}
},
beforeCreate(){
if(!this.$route.query.scope) {
this.$route.query.scope = ["USER"]
beforeRouteEnter(to, from, next) {
const defaultNamespace = localStorage.getItem(storageKeys.DEFAULT_NAMESPACE);
const query = {...to.query};
if (defaultNamespace) {
query.namespace = defaultNamespace;
} if (!query.scope) {
query.scope = ["USER"];
}
next(vm => {
vm.$router?.replace({query});
});
},
methods: {
selectionMapper(element) {
return {
id: element.id,
namespace: element.namespace
namespace: element.namespace,
enabled: !element.disabled
}
},
showStatChart() {
return this.daily && this.showChart;
},
onShowChartChange(value) {
this.showChart = value;
localStorage.setItem(storageKeys.SHOW_FLOWS_CHART, value);
if(this.showStatChart())
this.loadStats();
},
exportFlows() {
this.$toast().confirm(
this.$t("flow export", {"flowCount": this.queryBulkAction ? this.total : this.selection.length}),
@@ -386,6 +428,12 @@
}
)
},
anyFlowDisabled() {
return this.selection.some(flow => !flow.enabled);
},
anyFlowEnabled() {
return this.selection.some(flow => flow.enabled);
},
enableFlows() {
this.$toast().confirm(
this.$t("flow enable", {"flowCount": this.queryBulkAction ? this.total : this.selection.length}),
@@ -483,10 +531,10 @@
return _merge(base, queryFilter)
},
loadData(callback) {
loadStats() {
this.dailyReady = false;
if (this.user.hasAny(permission.EXECUTION)) {
if (this.user.hasAny(permission.EXECUTION) && this.showStatChart) {
this.$store
.dispatch("stat/daily", this.loadQuery({
startDate: this.$moment(this.startDate).add(-1, "day").startOf("day").toISOString(true),
@@ -496,6 +544,9 @@
this.dailyReady = true;
});
}
},
loadData(callback) {
this.loadStats();
this.$store
.dispatch("flow/findFlows", this.loadQuery({
@@ -540,7 +591,7 @@
rowClasses(row) {
return row && row.row && row.row.disabled ? "disabled" : "";
}
}
}
};
</script>

View File

@@ -26,7 +26,7 @@
<template #table>
<div v-if="search === undefined || search.length === 0">
<el-alert type="info" class="mb-3" :closable="false">
{{ $t('no result') }}
{{ $t('empty search') }}
</el-alert>
</div>
@@ -115,6 +115,7 @@
})
.finally(callback)
} else {
this.$store.commit("flow/setTotal", 0);
this.$store.commit("flow/setSearch", undefined);
callback();
}

View File

@@ -139,7 +139,7 @@
<code>disabled</code>
</template>
<div>
<el-switch active-color="green" v-model="newMetadata.disabled" />
<el-switch active-color="green" v-model="newMetadata.disabled" @update:model-value="(value) => newMetadata.disabled = value" />
</div>
</el-form-item>
</el-form>

View File

@@ -54,6 +54,7 @@
<el-button
:icon="Minus"
@click="deleteInput(index)"
:disabled="index === 0 && newInputs.length === 1"
/>
</el-button-group>
</div>

View File

@@ -57,6 +57,7 @@
<el-button
:icon="Minus"
@click="deleteInput(index)"
:disabled="index === 0 && newVariables.length === 1"
/>
</el-button-group>
</div>
@@ -117,8 +118,8 @@
this.newVariables[index][1] = event;
}
},
deleteInput(key) {
delete this.newVariables[key];
deleteInput(index) {
this.newVariables.splice(index, 1);
},
addVariable() {
this.newVariables.push(["", undefined]);

View File

@@ -1,5 +1,5 @@
<template>
<Dashboard flow :flow-i-d="flow ? flow.id : undefined" embed />
<Dashboard :restore-u-r-l="false" flow :flow-i-d="flow ? flow.id : undefined" embed />
</template>
<script>

View File

@@ -284,9 +284,12 @@
})
if (this.input) {
this.editor.addCommand(KeyMod.CtrlCmd | KeyCode.KeyF, () => {});
this.editor.addCommand(KeyMod.CtrlCmd | KeyCode.KeyH, () => {});
this.editor.addCommand(KeyCode.F1, () => {});
if (!this.readOnly) {
this.editor.addCommand(KeyMod.CtrlCmd | KeyCode.KeyF, () => { });
}
}
if (this.original === undefined && this.navbar && this.fullHeight) {
@@ -332,7 +335,7 @@
if (!this.fullHeight) {
editor.onDidContentSizeChange(e => {
if(!this.$refs.container) return;
if(!this.$refs.container) return;
this.$refs.container.style.height = (e.contentHeight + this.customHeight) + "px";
});
}

View File

@@ -29,7 +29,6 @@
import TaskEditor from "../flows/TaskEditor.vue";
import MetadataEditor from "../flows/MetadataEditor.vue";
import Editor from "./Editor.vue";
import yamlUtils from "../../utils/yamlUtils";
import {SECTIONS} from "../../utils/constants.js";
import LowCodeEditor from "../inputs/LowCodeEditor.vue";
import {editorViewTypes} from "../../utils/constants";
@@ -406,7 +405,7 @@
};
const updatePluginDocumentation = (event) => {
const taskType = yamlUtils.getTaskType(
const taskType = YamlUtils.getTaskType(
event.model.getValue(),
event.position
);
@@ -706,7 +705,7 @@
};
const save = async (e) => {
if (!currentTab?.value?.dirty && !props.isCreating) {
if (!haveChange.value && !props.isCreating) {
return;
}
if (e) {
@@ -934,7 +933,8 @@
)
"
>
<el-button @click="toggleExplorerVisibility()" class="toggle-button">
<el-button @click="toggleExplorerVisibility()">
<span class="pe-2 toggle-button">{{ t("files") }}</span>
<component :is="explorerVisible ? MenuOpen : MenuClose" />
</el-button>
</el-tooltip>
@@ -1309,7 +1309,7 @@
}
.toggle-button {
color: $secondary;
font-size: var(--el-font-size-small);
}
.tabs {

View File

@@ -14,6 +14,7 @@
v-if="input.type === 'STRING' || input.type === 'URI'"
v-model="inputs[input.id]"
@update:model-value="onChange"
@confirm="onSubmit"
/>
<el-select
:full-height="false"
@@ -209,7 +210,7 @@
multiSelectInputs: {},
};
},
emits: ["update:modelValue"],
emits: ["update:modelValue", "confirm"],
created() {
this.inputsList.push(...(this.initialInputs ?? []));
this.validateInputs();
@@ -223,9 +224,10 @@
}, 500)
this._keyListener = function(e) {
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
// Ctrl/Control + Enter
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
this.onSubmit(this.$refs.form);
this.onSubmit();
}
};
@@ -248,6 +250,9 @@
onChange() {
this.$emit("update:modelValue", this.inputs);
},
onSubmit() {
this.$emit("confirm");
},
onMultiSelectChange(input, e) {
this.inputs[input] = JSON.stringify(e).toString();
this.onChange();

View File

@@ -302,6 +302,16 @@
}
})
}
// Expose paste function globally for testing
window.pasteToEditor = (textToPaste) => {
this.editor.executeEdits("", [
{
range: this.editor.getSelection(),
text: textToPaste,
},
]);
};
},
beforeUnmount: function () {
this.destroy();

View File

@@ -42,9 +42,13 @@
}
.el-form-item__content {
> * {
> *:not(.el-button-group) {
width: 200px;
}
> .el-button-group {
max-width: 200px;
}
}
}
}

View File

@@ -185,10 +185,11 @@
}
}
}
.markdown-tooltip {
*:last-child {
margin-bottom: 0;
}
line-height: 15px;
padding: 5px;
}
</style>

View File

@@ -14,10 +14,16 @@
</slot>
</h1>
</div>
<div class="d-flex side gap-2 flex-shrink-0">
<div class="d-flex side gap-2 flex-shrink-0 align-items-center">
<div class="d-none d-lg-flex align-items-center">
<global-search class="trigger-flow-guided-step" />
</div>
<div class="d-flex side gap-2 flex-shrink-0 align-items-center">
<el-button v-if="shouldDisplayDeleteButton && logs !== undefined && logs.length > 0" @click="deleteLogs()">
<TrashCan class="me-2" />
<span>{{ $t("delete logs") }}</span>
</el-button>
</div>
<slot name="additional-right" />
<div class="d-flex fixed-buttons">
<el-dropdown popper-class="">
@@ -100,6 +106,7 @@
import Update from "vue-material-design-icons/Update.vue";
import ProgressQuestion from "vue-material-design-icons/ProgressQuestion.vue";
import GlobalSearch from "./GlobalSearch.vue";
import TrashCan from "vue-material-design-icons/TrashCan.vue";
export default {
components: {
@@ -113,6 +120,7 @@
Update,
ProgressQuestion,
GlobalSearch,
TrashCan,
Impersonating
},
props: {
@@ -128,6 +136,7 @@
computed: {
...mapState("api", ["version"]),
...mapState("core", ["tutorialFlows"]),
...mapState("log", ["logs"]),
...mapGetters("core", ["guidedProperties"]),
...mapGetters("auth", ["user"]),
displayNavBar() {
@@ -136,7 +145,10 @@
tourEnabled(){
// Temporary solution to not showing the tour menu item for EE
return this.tutorialFlows?.length && !Object.keys(this.user).length
}
},
shouldDisplayDeleteButton() {
return this.$route.name === "flows/update" && this.$route.params?.tab === "logs"
},
},
methods: {
restartGuidedTour() {
@@ -144,6 +156,13 @@
this.$store.commit("core/setGuidedProperties", {tourStarted: false});
this.$tours["guidedTour"]?.start();
},
deleteLogs() {
this.$toast().confirm(
this.$t("delete_all_logs"),
() => this.$store.dispatch("log/deleteLogs", {namespace: this.namespace, flowId: this.flowId}),
() => {}
)
}
}
};

View File

@@ -1,5 +1,5 @@
<template>
<div class="d-flex ms-2 me-2 el-select__wrapper space-between" :class="{border: isSelected, ['log-border-' + level.toLowerCase()]: isSelected, 'shadow-none': isSelected}">
<div class="d-flex el-select__wrapper space-between" :class="{border: isSelected, ['log-border-' + level.toLowerCase()]: isSelected, 'shadow-none': isSelected}">
<div class="d-flex align-items-center gap-2">
<span :class="'circle log-bg-' + level.toLowerCase()" />
<span>({{ (cursorIdx === undefined ? "" : `${cursorIdx + 1} / `) + totalCount }}) {{ level }}</span>
@@ -7,12 +7,14 @@
<div class="d-flex align-items-center gap-2">
<chevron-up class="medium-icon nav-button" @click="forwardEvent('previous')" />
<chevron-down class="medium-icon nav-button" @click="forwardEvent('next')" />
<close class="medium-icon nav-button close-button" @click="forwardEvent('close')" v-if="isSelected" />
</div>
</div>
</template>
<script setup>
import ChevronUp from "vue-material-design-icons/ChevronUp.vue";
import ChevronDown from "vue-material-design-icons/ChevronDown.vue";
import Close from "vue-material-design-icons/Close.vue";
</script>
<script>
export default {
@@ -47,6 +49,7 @@
.el-select__wrapper.space-between {
justify-content: space-between;
cursor: unset;
white-space: nowrap;
&:hover {
box-shadow: 0 0 0 1px var(--el-border-color) inset;
@@ -66,5 +69,12 @@
.medium-icon {
font-size: 1.1rem;
}
.close-button {
color: var(--el-text-color-secondary);
&:hover {
color: var(--el-text-color-primary);
}
}
}
</style>

View File

@@ -1,8 +1,8 @@
<template>
<div class="py-2 line font-monospace" :class="{['log-border-' + log.level.toLowerCase()]: cursor}" v-if="filtered">
<div class="py-2 line font-monospace" :class="{['log-border-' + log.level.toLowerCase()]: cursor && log.level !== undefined}" v-if="filtered">
<span :class="levelClasses" class="border header-badge log-level el-tag noselect">{{ log.level }}</span>
<div class="log-content d-inline-block">
<span v-if="title" class="fw-bold">{{ (log.taskId ?? log.flowId ?? "").capitalize() }}</span>
<span v-if="title" class="fw-bold">{{ (log.taskId ?? log.flowId ?? "") }}</span>
<div
class="header"
:class="{'d-inline-block': metaWithValue.length === 0, 'me-3': metaWithValue.length === 0}"
@@ -116,7 +116,7 @@
return metaWithValue;
},
levelClasses() {
const lowerCaseLevel = this.log.level.toLowerCase();
const lowerCaseLevel = this.log?.level?.toLowerCase();
return `log-content-${lowerCaseLevel} log-border-${lowerCaseLevel} log-bg-${lowerCaseLevel}`;
},
filtered() {

View File

@@ -3,7 +3,7 @@
<section v-bind="$attrs" :class="{'container': !embed}" class="log-panel">
<div class="log-content">
<data-table @page-changed="onPageChanged" ref="dataTable" :total="total" :size="pageSize" :page="pageNumber" :embed="embed">
<template #navbar v-if="!embed">
<template #navbar v-if="!embed || showFilters">
<el-form-item>
<search-field />
</el-form-item>
@@ -29,6 +29,13 @@
@update:filter-value="onDataTableValue"
/>
</el-form-item>
<el-form-item>
<el-switch
:model-value="showChart"
@update:model-value="onShowChartChange"
:active-text="$t('show chart')"
/>
</el-form-item>
<el-form-item>
<filters :storage-key="storageKeys.LOGS_FILTERS" />
</el-form-item>
@@ -37,16 +44,11 @@
</el-form-item>
</template>
<template v-if="charts" #top>
<template v-if="showStatChart()" #top>
<el-card shadow="never" class="mb-3" v-loading="!statsReady">
<div class="state-global-charts">
<div>
<template v-if="hasStatsData">
<log-chart
v-if="statsReady"
:data="logDaily"
:namespace="namespace"
:flow-id="flowId"
/>
<Logs :data="logDaily" />
</template>
<template v-else>
<el-alert type="info" :closable="false" class="m-0">
@@ -55,11 +57,6 @@
</template>
</div>
</el-card>
<el-button v-if="shouldDisplayDeleteButton && logs !== undefined && logs.length > 0" @click="deleteLogs()" class="mb-3 delete-logs-btn">
<TrashCan class="me-2" />
<span>{{ $t("delete logs") }}</span>
</el-button>
</template>
<template #table>
@@ -100,16 +97,16 @@
import DataTable from "../../components/layout/DataTable.vue";
import RefreshButton from "../../components/layout/RefreshButton.vue";
import _merge from "lodash/merge";
import LogChart from "../stats/LogChart.vue";
import Logs from "../dashboard/components/charts/logs/Bar.vue";
import Filters from "../saved-filters/Filters.vue";
import {storageKeys} from "../../utils/constants";
import TrashCan from "vue-material-design-icons/TrashCan.vue";
export default {
mixins: [RouteContext, RestoreUrl, DataTableActions],
components: {
Filters,
DataTable, LogLine, NamespaceSelect, DateFilter, SearchField, LogLevelSelector, RefreshButton, TopNavBar, LogChart, TrashCan},
DataTable, LogLine, NamespaceSelect, DateFilter, SearchField, LogLevelSelector, RefreshButton, TopNavBar, Logs},
props: {
logLevel: {
type: String,
@@ -123,6 +120,10 @@
type: Boolean,
default: true
},
showFilters: {
type: Boolean,
default: false
},
filters: {
type: Object,
default: null
@@ -140,7 +141,8 @@
refreshDates: false,
statsReady: false,
statsData: [],
canAutoRefresh: false
canAutoRefresh: false,
showChart: ["true", null].includes(localStorage.getItem(storageKeys.SHOW_LOGS_CHART)),
};
},
computed: {
@@ -157,9 +159,6 @@
isFlowEdit() {
return this.$route.name === "flows/update"
},
shouldDisplayDeleteButton() {
return this.$route.name === "flows/update"
},
isNamespaceEdit() {
return this.$route.name === "namespaces/update"
},
@@ -199,10 +198,30 @@
return this.countStats > 0;
},
},
beforeRouteEnter(to, from, next) {
const defaultNamespace = localStorage.getItem(storageKeys.DEFAULT_NAMESPACE);
const query = {...to.query};
if (defaultNamespace) {
query.namespace = defaultNamespace;
}
next(vm => {
vm.$router?.replace({query});
});
},
methods: {
onDateFilterTypeChange(event) {
this.canAutoRefresh = event;
},
showStatChart() {
return this.charts && this.showChart;
},
onShowChartChange(value) {
this.showChart = value;
localStorage.setItem(storageKeys.SHOW_LOGS_CHART, value);
if (this.showStatChart()) {
this.loadStats();
}
},
refresh() {
this.refreshDates = !this.refreshDates;
this.load();
@@ -262,13 +281,6 @@
.then(() => {
this.statsReady = true;
});
},
deleteLogs() {
this.$toast().confirm(
this.$t("delete_all_logs"),
() => this.$store.dispatch("log/deleteLogs", {namespace: this.namespace, flowId: this.flowId}),
() => {}
)
}
},
};
@@ -302,8 +314,4 @@
}
}
}
.delete-logs-btn {
width: 200px;
}
</style>
</style>

View File

@@ -193,7 +193,7 @@
fullscreen: false,
followed: false,
shownAttemptsUid: [],
logs: [],
rawLogs: [],
timer: undefined,
timeout: undefined,
selectedAttemptNumberByTaskRunId: {},
@@ -218,7 +218,7 @@
this.$emit("opened-taskruns-count", openedTaskrunsCount);
},
level: function () {
this.logs = [];
this.rawLogs = [];
this.loadLogs(this.followedExecution.id);
},
execution: function () {
@@ -404,7 +404,7 @@
return LogUtils.levelOrLower(this.level);
},
filteredLogs() {
return this.logs.filter(log => this.levelOrLower.includes(log.level));
return this.rawLogs.filter(log => this.levelOrLower.includes(log.level));
}
},
methods: {
@@ -496,7 +496,7 @@
clearTimeout(this.timeout);
this.timeout = setTimeout(() => {
this.timer = moment()
this.logs = this.logs.concat(this.logsBuffer);
this.rawLogs = this.rawLogs.concat(this.logsBuffer);
this.logsBuffer = [];
this.scrollToBottomFailedTask();
}, 100);
@@ -505,7 +505,7 @@
if (moment().diff(this.timer, "seconds") > 0.5) {
clearTimeout(this.timeout);
this.timer = moment()
this.logs = this.logs.concat(this.logsBuffer);
this.rawLogs = this.rawLogs.concat(this.logsBuffer);
this.logsBuffer = [];
this.scrollToBottomFailedTask();
}
@@ -576,14 +576,14 @@
if (!this.showLogs) {
return;
}
this.$store.dispatch("execution/loadLogs", {
executionId,
params: {
minLevel: this.level
},
store: false
}
}).then(logs => {
this.logs = logs
this.rawLogs = logs
});
},
attempts(taskRun) {

View File

@@ -0,0 +1,15 @@
<template>
<Executions :restore-url="false" :namespace="$route.params.id || $route.query.id" :topbar="false" :hidden="['selection','inputs','flowRevision','taskRunList.taskId']" />
</template>
<script>
import Executions from "../executions/Executions.vue"
import {mapState} from "vuex";
export default {
components: {Executions},
computed: {
...mapState("namespace", ["namespace"]),
},
};
</script>

View File

@@ -0,0 +1,11 @@
<template>
<Flows :restore-url="false" :namespace="$route.params.id || $route.query.id" />
</template>
<script>
import Flows from "../flows/Flows.vue";
export default {
components: {Flows},
};
</script>

View File

@@ -48,8 +48,9 @@
import permission from "../../models/permission";
import action from "../../models/action";
import Overview from "./Overview.vue";
import Executions from "./Executions.vue";
import NamespaceKV from "./NamespaceKV.vue";
import NamespaceFlows from "./NamespaceFlows.vue";
import Flows from "./Flows.vue";
import EditorView from "../inputs/EditorView.vue";
import BlueprintsBrowser from "../../override/components/flows/blueprints/BlueprintsBrowser.vue";
import {apiUrl} from "override/utils/route";
@@ -135,15 +136,27 @@
},
{
name: "flows",
component: NamespaceFlows,
component: Flows,
title: this.$t("flows"),
props: {
tab: "flows",
topbar:false,
},
query: {
id: this.$route.query.id
}
},
{
name: "executions",
component: Executions,
props: {
embed: false,
},
title: this.$t("executions"),
query: {
id: this.$route.query.id
}
},
{
name: "dependencies",
component: NamespaceDependenciesWrapper,

View File

@@ -1,14 +1,27 @@
<template>
<el-table
<select-table
:data="kvs"
ref="table"
ref="selectTable"
:default-sort="{prop: 'id', order: 'ascending'}"
stripe
table-layout="auto"
fixed
@selection-change="handleSelectionChange"
>
<template #select-actions>
<bulk-select
:select-all="queryBulkAction"
:selections="selection"
@update:select-all="toggleAllSelection"
@unselect="toggleAllUnselected"
>
<el-button :icon="Delete" type="default" @click="removeKvs()">
{{ $t("delete") }}
</el-button>
</bulk-select>
</template>
<el-table-column prop="key" sortable="custom" :sort-orders="['ascending', 'descending']" :label="$t('key')">
<template #default="scope">
<template #default="scope">
<id :value="scope.row.key" :shrink="false" />
</template>
</el-table-column>
@@ -27,7 +40,7 @@
<el-button :icon="Delete" link @click="removeKv(scope.row.key)" />
</template>
</el-table-column>
</el-table>
</select-table>
<drawer
v-if="addKvDrawerVisible"
@@ -115,6 +128,8 @@
</template>
<script setup>
import BulkSelect from "../layout/BulkSelect.vue";
import SelectTable from "../layout/SelectTable.vue";
import Editor from "../inputs/Editor.vue";
import FileDocumentEdit from "vue-material-design-icons/FileDocumentEdit.vue";
import Delete from "vue-material-design-icons/Delete.vue";
@@ -127,8 +142,10 @@
import {mapState} from "vuex";
import Drawer from "../Drawer.vue";
import Id from "../Id.vue";
import SelectTableActions from "../../mixins/selectTableActions";
export default {
mixins: [SelectTableActions],
components: {
Id,
Drawer
@@ -195,6 +212,11 @@
if (!newValue) {
this.resetKv();
}
},
"kv.type"() {
if (this.$refs.form) {
this.$refs.form.clearValidate("value");
}
}
},
data() {
@@ -264,6 +286,19 @@
});
});
},
removeKvs() {
let request = {"keys":[]}
this.selection.forEach((obj)=>{
request.keys.push(obj.key)
})
this.$toast().confirm(this.$t("delete confirm multiple",{name: request.keys.length}), () => {
return this.$store
.dispatch("namespace/deleteKvs", {namespace: this.$route.params.id, request: request})
.then(() => {
this.$toast().deleted(request.keys.length);
});
});
},
saveKv(formRef) {
formRef.validate((valid) => {
if (!valid) {

View File

@@ -49,7 +49,7 @@
this.$store
.dispatch("namespace/loadNamespacesForDatatype", {dataType: this.dataType})
.then(() => {
this.groupedNamespaces = this.groupNamespaces(this.datatypeNamespaces);
this.groupedNamespaces = this.groupNamespaces(this.datatypeNamespaces).filter(namespace => namespace.code !== "system");
});
}
},

View File

@@ -24,10 +24,10 @@
v-for="(namespace, index) in hierarchy(namespaces)"
:key="index"
:span="24"
class="my-1 py-2 px-4 namespaces"
class="my-1 namespaces"
:class="{system: namespace.id === 'system'}"
>
<el-tree :data="[namespace]" default-expand-all :props="{class: 'tree'}" class="h-auto">
<el-tree :data="[namespace]" default-expand-all :props="{class: 'tree'}" class="h-auto py-2 px-4 rounded-full">
<template #default="{data}">
<router-link :to="{name: 'namespaces/update', params: {id: data.id, tab: data.system ? 'blueprints': ''}}" tag="div" class="node">
<div class="d-flex">
@@ -164,7 +164,6 @@ $system: #5BB8FF;
.namespaces {
border-radius: var(--bs-border-radius-lg);
border: 1px solid var(--bs-border-color);
background: var(--bs-body-bg);
&.system {
border-color: $system;
@@ -182,10 +181,14 @@ $system: #5BB8FF;
--el-tree-node-hover-bg-color: transparent;
}
.rounded-full {
border-radius: var(--bs-border-radius-lg);
}
.el-tree-node__content {
height: 2.25rem;
overflow: hidden;
background: var(--bs-body-bg);
background: transparent;
.icon {
color: $active;
@@ -204,7 +207,7 @@ $system: #5BB8FF;
}
&:hover {
background: var(--bs-body-bg);
background: transparent;
}
& .system {

View File

@@ -1,5 +1,5 @@
<template>
<Dashboard :namespace="$route.params.id || $route.query.id" embed />
<Dashboard :restore-u-r-l="false" :namespace="$route.params.id || $route.query.id" embed />
</template>
<script>

View File

@@ -305,6 +305,12 @@
this.expandParentIfNeeded();
},
watch: {
"$i18n.locale": {
deep: true,
handler(){
this.localMenu = this.disabledCurrentRoute(this.generateMenu());
}
},
menu: {
handler(newVal, oldVal) {
// Check if the active menu item has changed, if yes then update the menu

View File

@@ -77,8 +77,8 @@ export default {
return tab.name === name;
});
state.tabs[tabIdxToDirty].dirty = dirty;
state.current.dirty = dirty;
if(state.tabs[tabIdxToDirty]) state.tabs[tabIdxToDirty].dirty = dirty;
if(state.current) state.current.dirty = dirty;
}
},
closeTabs(state) {

View File

@@ -25,7 +25,8 @@ export default {
metrics: [],
aggregatedMetrics: undefined,
tasksWithMetrics: [],
executeFlow: false
executeFlow: false,
lastSaveFlow: undefined
},
actions: {
@@ -313,6 +314,7 @@ export default {
},
setFlow(state, flow) {
state.flow = flow;
state.lastSaveFlow = flow;
// if (state.flowGraph !== undefined && state.flowGraphParam && flow) {
// if (state.flowGraphParam.namespace !== flow.namespace || state.flowGraphParam.id !== flow.id) {
// state.flowGraph = undefined
@@ -389,6 +391,11 @@ export default {
}
},
getters: {
lastSaveFlow(state){
if(state.lastSavedFlow){
return state.lastSavedFlow;
}
},
flow(state) {
if (state.flow) {
return state.flow;

View File

@@ -75,6 +75,15 @@ export default {
return dispatch("kvsList", {id: payload.namespace})
});
},
deleteKvs({dispatch}, payload) {
return this.$http
.delete(`${apiUrl(this)}/namespaces/${payload.namespace}/kv`,{
data: payload.request
})
.then(() => {
return dispatch("kvsList", {id: payload.namespace})
});
},
// Create a directory
async createDirectory(_, payload) {

View File

@@ -1030,6 +1030,10 @@ form.ks-horizontal {
border-radius: var(--el-border-radius-base);
height: var(--el-component-size);
.el-radio-button {
display: inline-flex;
}
.el-radio-button__inner {
background-color: var(--input-bg);
padding: 4px 15px;

View File

@@ -32,7 +32,7 @@ html.dark {
#{--bs-link-color-rgb}: to-rgb($secondary);
#{--bs-tertiary-color}: #C3BBE3;
$levels: info, running, danger, warning, success;
$levels: info, running, danger, warning;
@each $level in $levels {
.bg-#{$level} {
#{--bs-bg-opacity}: 0.2;

View File

@@ -88,7 +88,7 @@
$content-running: #7400df;
$content-alert: #ab0009;
$content-warning: #c15300;
$content-success: #017f5c;
$content-success: #03DABA;
#{--background-failed}: #fed6d9;
#{--background-success}: #e4f9f3;
#{--content-information}: $content-information;
@@ -178,4 +178,4 @@ $logLevels: "trace", "debug", "info", "warn", "error";
.text-tertiary {
color: var(--bs-tertiary-color);
}
}

View File

@@ -0,0 +1,38 @@
import fs from "fs";
import path from "path";
import {fileURLToPath} from "url";
const getPath = (lang) => path.resolve(path.dirname(fileURLToPath(import.meta.url)), `./${lang}.json`);
const readJSON = (filePath) => JSON.parse(fs.readFileSync(filePath, "utf-8"));
const getNestedKeys = (obj, prefix = "") =>
Object.keys(obj).reduce((keys, key) => {
const fullKey = prefix ? `${prefix}.${key}` : key;
keys.push(fullKey);
if (
typeof obj[key] === "object" &&
obj[key] &&
!Array.isArray(obj[key])
) {
keys.push(...getNestedKeys(obj[key], fullKey));
}
return keys;
}, []);
// Use English as a base language
const content = getNestedKeys(readJSON(getPath("en"))["en"]);
const languages = ["de", "es", "fr", "hi", "it", "ja", "ko", "pl", "pt", "ru", "zh_CN"];
const paths = languages.map((lang) => getPath(lang));
languages.forEach((lang, i) => {
const current = getNestedKeys(readJSON(paths[i])[lang]);
const missing = content.filter((key) => !current.includes(key));
const extra = current.filter((key) => !content.includes(key));
console.log(`---\n\x1b[34mComparison with ${lang.toUpperCase()}\x1b[0m \n`);
console.log(missing.length ? `Missing keys: \x1b[31m${missing.join(", ")}\x1b[0m` : "No missing keys.");
console.log(extra.length ? `Extra keys: \x1b[32m${extra.join(", ")}\x1b[0m` : "No extra keys.");
console.log("---\n");
});

View File

@@ -510,7 +510,8 @@
"success": "Trigger ist entsperrt"
},
"restart trigger": {
"button": "Trigger neu starten"
"button": "Trigger neu starten",
"tooltip": "Den Trigger neu starten"
},
"date format": "Datumsformat",
"timezone": "Zeitzone",
@@ -849,7 +850,10 @@
"trigger_disabled": "Dieser Trigger kann nur durch Code aktiviert werden.",
"no_flow_description": "Dieser Flow hat keine Beschreibung.",
"description": "Beschreibung",
"not_auth": "Sie haben nicht die erforderlichen Berechtigungen, um dieses Widget anzuzeigen."
"not_auth": "Sie haben nicht die erforderlichen Berechtigungen, um dieses Widget anzuzeigen.",
"see_all": "Alle anzeigen",
"success_ratio_tooltip": "Erfolgsquote ist die Summe der SUCCESS, CANCELLED und WARNING Ausführungen, geteilt durch die Gesamtanzahl der Ausführungen in einem beendeten Zustand.",
"failure_ratio_tooltip": "Der Fehlerrate ist die Summe der FAILED, KILLED und RETRIED Ausführungen, geteilt durch die Gesamtanzahl der Ausführungen in einem beendeten Zustand."
},
"delete_log": "Sind Sie sicher, dass Sie das Log löschen möchten?",
"docs": "Dokumentation",
@@ -861,6 +865,16 @@
"active-slots": "Aktive Slots",
"concurrency": "Nebenläufigkeit",
"open sidebar": "Seitenleiste öffnen",
"close sidebar": "Seitenleiste schließen"
"close sidebar": "Seitenleiste schließen",
"change_status": "Status ändern",
"in-our-documentation": "in unserer Dokumentation.",
"read-more": "Mehr erfahren über",
"flow-dependencies": "Abhängigkeiten",
"flow-no-dependencies": "Ihr Flow hat keine Abhängigkeiten.",
"files": "Dateien",
"add-trigger-in-editor": "Fügen Sie zuerst einen Trigger zu Ihrem Flow hinzu",
"delete confirm multiple": "Sind Sie sicher, dass Sie <code>{name}</code> KV(s) löschen möchten?",
"show task condition": "Bedingung des Tasks anzeigen",
"empty search": "Suchergebnisse sind leer"
}
}

View File

@@ -20,6 +20,7 @@
"Default page": "Default page",
"confirmation": "Confirmation",
"delete confirm": "Are you sure to delete <code>{name}</code>?",
"delete confirm multiple": "Are you sure to delete <code>{name}</code> KV(s)?",
"outdated revision save confirmation": {
"confirm": "Do you want to overwrite it?",
"update": {
@@ -230,6 +231,7 @@
"automatic refresh": "Automatic refresh",
"toggle periodic refresh each 10 seconds": "Toggle periodic refresh every 10 seconds",
"trigger refresh": "Trigger refresh",
"add-trigger-in-editor": "Add a Trigger to your Flow first",
"refresh": "Refresh",
"topology-graph": {
"graph-orientation": "Graph orientation",
@@ -307,6 +309,7 @@
"templates": "Templates",
"templates deprecated": "Templates are deprecated. Please use subflows instead. See the <a href=\"https://kestra.io/docs/migration-guide/0.11.0/templates\" target=\"_blank\">Migrations section</a> explaining how you can migrate from templates to subflows.",
"no result": "No logs emitted so far",
"empty search": "Search results are empty",
"trigger": "Trigger",
"triggers": "Triggers",
"trigger details": "Trigger details",
@@ -428,6 +431,10 @@
"flow enable": "Are you sure you want to enable <code>{flowCount}</code> flow(s)?",
"flows disabled": "<code>{count}</code> Flow(s) disabled",
"flows enabled": "<code>{count}</code> Flow(s) enabled",
"flow-no-dependencies": "Your flow does not have any dependencies",
"flow-dependencies": "dependencies",
"read-more": "Read more about",
"in-our-documentation": "in our documentation.",
"dependencies": "Dependencies",
"see dependencies": "See dependencies",
"dependencies missing acls": "No permissions on this flow",
@@ -855,7 +862,9 @@
"trigger_check_warning": "Warning: Usage of the `trigger` variable detected, executing your flow manually won't fullfill the trigger variable.",
"dashboard": {
"success_ratio": "Success Ratio",
"success_ratio_tooltip": "Success Ratio is the sum of SUCCESS, CANCELLED and WARNING executions divided by the total number of executions in a terminated state.",
"failure_ratio": "Failure Ratio",
"failure_ratio_tooltip": "Failed ratio is the sum of FAILED, KILLED and RETRIED executions divided by the total number of executions in a terminated state.",
"per_day": " (per day)",
"per_namespace": " (per namespace)",
"total_executions": "Total Executions",
@@ -866,7 +875,8 @@
"trigger_disabled": "This trigger can only be enabled through code.",
"description": "Description",
"no_flow_description": "This flow has no description.",
"not_auth": "You don't have the necessary permissions to view this widget."
"not_auth": "You don't have the necessary permissions to view this widget.",
"see_all": "See all"
},
"docs": "Docs",
"active-slots": "Active slots",
@@ -877,6 +887,9 @@
"desc_no_limit": "Read more about <a href=\"https://kestra.io/docs/workflow-components/concurrency\" target=\"_blank\">Concurrency Limits</a> in our documentation."
},
"open sidebar": "open sidebar",
"close sidebar": "close sidebar"
"close sidebar": "close sidebar",
"change_status": "Change status",
"files": "Files",
"show task condition": "Show task condition"
}
}

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