Compare commits

...

61 Commits

Author SHA1 Message Date
github-actions[bot]
9859ea5eb6 chore(version): update to version '0.24.0' 2025-08-05 12:01:23 +00:00
Piyush Bhaskar
aca374a28f fix(flows): ensure plugin documentation change on flow switch (#10546)
Co-authored-by: Barthélémy Ledoux <bledoux@kestra.io>
2025-08-05 15:21:21 +05:30
Barthélémy Ledoux
c413ba95e1 fix(flows): add conditional rendering for restart button based on execution (#10570) 2025-08-05 10:22:53 +02:00
Barthélémy Ledoux
9c6b92619e fix: restore InputForm (#10568) 2025-08-05 09:45:10 +02:00
brian.mulier
8173e8df51 fix(namespaces): autocomplete in kv & secrets
related to kestra-io/kestra-ee#4559
2025-08-04 20:30:06 +02:00
brian.mulier
5c95505911 fix(executions): avoid SSE error in follow execution dependencies
closes #10560
2025-08-04 20:23:40 +02:00
Barthélémy Ledoux
33f0b533bb fix(flows)*: load flow for execution needs to be stored most of the time (#10566) 2025-08-04 18:55:57 +02:00
brian.mulier
23e35a7f97 chore(version): upgrade version to 0.24.0-rc2-SNAPSHOT 2025-08-04 16:19:44 +02:00
Abhilash T
0357321c58 fix: Updated InputsForm.vue to clear Radio Button Selection (#9654)
Co-authored-by: Miloš Paunović <paun992@hotmail.com>
Co-authored-by: Bart Ledoux <bledoux@kestra.io>
2025-08-04 16:03:55 +02:00
Barthélémy Ledoux
5c08403398 fix(flows): no-code - when changing type message avoid warning (#10498) 2025-08-04 15:57:23 +02:00
Barthélémy Ledoux
a63cb71218 fix: remove debugging value from playground (#10541) 2025-08-04 15:57:05 +02:00
brian.mulier
317885b91c fix(executions): restore execution redirect & subflow logs view from parent
closes #10528
closes #10551
2025-08-04 15:47:49 +02:00
Piyush Bhaskar
87637302e4 chore(core): remove variable and directly assign. (#10554) 2025-08-04 18:51:14 +05:30
Piyush Bhaskar
056faaaf9f fix(core): proper state detection from parsed data (#10527) 2025-08-04 18:50:53 +05:30
Miloš Paunović
54c74a1328 chore(namespaces): add the needed prop for loading all namespaces inside a selector (#10544) 2025-08-04 12:45:06 +02:00
Miloš Paunović
fae0c88c5e fix(namespaces): amend problems with namespace secrets and kv pairs (#10543)
Closes https://github.com/kestra-io/kestra-ee/issues/4584.
2025-08-04 12:20:37 +02:00
YannC.
db5d83d1cb fix: add missing webhook releases secrets for github releases 2025-08-01 23:22:18 +02:00
brian.mulier
066b947762 fix(core): remove icon for inputs in no-code
closes #10520
2025-08-01 16:32:55 +02:00
Piyush Bhaskar
b6597475b1 fix(namespaces): fixes loading of additional ns (#10518) 2025-08-01 17:01:53 +05:30
brian.mulier
f2610baf15 fix(executions): avoid race condition leading to never-ending follow with non-terminal state 2025-08-01 13:12:59 +02:00
brian.mulier
b619bf76d8 fix(core): ensure instances can read all messages when no consumer group / queue type 2025-08-01 13:12:59 +02:00
Loïc Mathieu
117f453a77 feat(flows): warn on runnable only properties on non-runnable tasks
Closes #9967
Closes #10500
2025-08-01 12:53:24 +02:00
Piyush Bhaskar
053d6276ff fix(executions): do not rely on monaco to get value (#10515) 2025-08-01 13:26:25 +05:30
Barthélémy Ledoux
3870eca70b fix(flows): playground need to use ui-libs (#10506) 2025-08-01 09:06:51 +02:00
Piyush Bhaskar
afd7c216f9 fix(flows): route to flow page (#10514) 2025-08-01 12:13:08 +05:30
Piyush Bhaskar
59a17e88e7 fix(executions): properly handle methods and computed for tabs (#10513) 2025-08-01 12:12:54 +05:30
Piyush Bhaskar
99f8dca1c2 fix(editor): adjust padding for editor (#10497)
* fix(editor): adjust padding for editor

* fix: make padding 16px
2025-08-01 12:12:38 +05:30
YannC
1068c9fe51 fix: handle empty flows list in lastExecutions correctly (#10493) 2025-08-01 07:21:16 +02:00
YannC
ea6d30df7c fix(ui): load correctly filters + refresh dashboard on filter change (#10504) 2025-08-01 07:16:34 +02:00
Loïc Mathieu
04ba7363c2 fix(ci): workflow build artifact doesn't need the plugin version 2025-07-31 14:32:57 +02:00
Loïc Mathieu
281a987944 chore(version): upgrade version to 0.24.0-rc1-SNAPSHOT 2025-07-31 14:20:07 +02:00
github-actions[bot]
c9ce54b0be chore(core): localize to languages other than english (#10494)
Extended localization support by adding translations for multiple languages using English as the base. This enhances accessibility and usability for non-English-speaking users while keeping English as the source reference.

Co-authored-by: GitHub Action <actions@github.com>
2025-07-31 14:19:14 +02:00
github-actions[bot]
ccd9baef3c chore(core): localize to languages other than english (#10489)
Extended localization support by adding translations for multiple languages using English as the base. This enhances accessibility and usability for non-English-speaking users while keeping English as the source reference.

Co-authored-by: GitHub Action <actions@github.com>
2025-07-31 14:19:04 +02:00
Barthélémy Ledoux
97869b9c75 fix(flows): forget all old taskRunId when a new execution (#10487) 2025-07-31 14:17:14 +02:00
Barthélémy Ledoux
1c681c1492 fix(flows): wait longer for widgets to be rendered (#10485) 2025-07-31 14:17:06 +02:00
Barthélémy Ledoux
de2a446f93 fix(flows): load flows documentation when coming back to no-code root (#10374) 2025-07-31 14:17:00 +02:00
Barthélémy Ledoux
d778947017 fix(flows): add the load errors to the flow errors (#10483) 2025-07-31 14:16:47 +02:00
Barthélémy Ledoux
3f97845fdd fix(flows): hide executionkind meta in the logs (#10482) 2025-07-31 14:16:41 +02:00
Barthélémy Ledoux
631cd169a1 fix(executions): do not rely on monaco to get value (#10467) 2025-07-31 14:16:33 +02:00
Barthélémy Ledoux
1648fa076c fix(flows): playground - implement new designs (#10459)
Co-authored-by: brian.mulier <bmmulier@hotmail.fr>
2025-07-31 14:16:26 +02:00
Barthélémy Ledoux
474806882e fix(flows): playground align restart button button (#10415) 2025-07-31 14:16:17 +02:00
Barthélémy Ledoux
65467bd118 fix(flows): playground clear current execution when clearExecutions() (#10414) 2025-07-31 14:16:04 +02:00
YannC
387bbb80ac feat(ui): added http method autocompletion (#10492) 2025-07-31 13:29:22 +02:00
Loïc Mathieu
19d4c64f19 fix(executions): Don't create outputs from the Subflow task when we didn't wait
As, well, if we didn't wait for the subflow execution, we cannot have access to its outputs.
2025-07-31 13:07:26 +02:00
Loïc Mathieu
809c0a228c feat(system): improve performance of computeSchedulable
- Store flowIds in a list to avoid computing the multiple times
- Storeg triggers by ID in a map to avoid iterating the list of triggers for each flow
2025-07-31 12:34:30 +02:00
Piyush Bhaskar
6a045900fb fix(core): remove top spacing from no execution page and removing the redundant code (#10445)
Co-authored-by: Miloš Paunović <paun992@hotmail.com>
2025-07-31 13:28:08 +05:30
Piyush Bhaskar
4ada5fe8f3 fix(executions): make columns that are not links normal text (#10460)
* fix(executions): make it normal text

* fix(executions): use monospace font only
2025-07-31 13:27:45 +05:30
github-actions[bot]
998087ca30 chore(core): localize to languages other than english (#10471)
Extended localization support by adding translations for multiple languages using English as the base. This enhances accessibility and usability for non-English-speaking users while keeping English as the source reference.

Co-authored-by: GitHub Action <actions@github.com>
2025-07-31 08:25:16 +02:00
Malaydewangan09
146338e48f feat(plugins): add script plugins 2025-07-30 23:34:55 +05:30
brian.mulier
de177b925e chore(deps): hardcode vue override version 2025-07-30 19:26:31 +02:00
brian.mulier
04bfb19095 fix(core): avoid follow execution from being discarded too early
closes #10472
closes #7623
2025-07-30 19:26:31 +02:00
brian-mulier-p
c913c48785 fix(core): redesign playground run task button (#10423)
closes #10389
2025-07-30 15:27:49 +02:00
François Delbrayelle
0d5b593d42 fix(): fix icons 2025-07-30 14:55:33 +02:00
weibo1
83f92535c5 feat: Trigger Initialization Method Performance Optimization 2025-07-30 14:54:08 +02:00
Loïc Mathieu
fd6a0a6c11 fix(ci): bad SNAPSHOT repo URL 2025-07-30 12:57:28 +02:00
Loïc Mathieu
104c4c97b4 fix(ci): don't publish docker in build-artifact 2025-07-30 12:05:30 +02:00
Loïc Mathieu
21cd21269f fix(ci): add missing build artifact job 2025-07-30 11:50:26 +02:00
Loïc Mathieu
679befa2fe build(ci): allow downloading the exe from the workflow and not the release
This would allow running the workflow even if the release step fail
2025-07-30 11:24:21 +02:00
YannC
8a0ecdeb8a fix(dashboard): pageSize & pageNumber is now correctly pass when fetching a chart (#10413) 2025-07-30 08:45:51 +02:00
YannC.
ee8762e138 fix(ci): correctly pass GH token to release workflow 2025-07-29 15:04:18 +02:00
github-actions[bot]
d16324f265 chore(version): update to version 'v0.24.0-rc0-SNAPSHOT'. 2025-07-29 12:14:49 +00:00
86 changed files with 1124 additions and 582 deletions

View File

@@ -20,6 +20,15 @@ on:
required: false required: false
type: string type: string
default: "LATEST" default: "LATEST"
force-download-artifact:
description: 'Force download artifact'
required: false
type: string
default: "true"
options:
- "true"
- "false"
env: env:
PLUGIN_VERSION: ${{ github.event.inputs.plugin-version != null && github.event.inputs.plugin-version || 'LATEST' }} PLUGIN_VERSION: ${{ github.event.inputs.plugin-version != null && github.event.inputs.plugin-version || 'LATEST' }}
jobs: jobs:
@@ -38,9 +47,18 @@ jobs:
id: plugins id: plugins
with: with:
plugin-version: ${{ env.PLUGIN_VERSION }} plugin-version: ${{ env.PLUGIN_VERSION }}
# ********************************************************************************************************************
# Build
# ********************************************************************************************************************
build-artifacts:
name: Build Artifacts
if: ${{ github.event.inputs.force-download-artifact == 'true' }}
uses: ./.github/workflows/workflow-build-artifacts.yml
docker: docker:
name: Publish Docker name: Publish Docker
needs: [ plugins ] needs: [ plugins, build-artifacts ]
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
@@ -69,18 +87,31 @@ jobs:
fi fi
if [[ "${{ env.PLUGIN_VERSION }}" == *"-SNAPSHOT" ]]; then if [[ "${{ env.PLUGIN_VERSION }}" == *"-SNAPSHOT" ]]; then
echo "plugins=--repositories=https://central.sonatype.com/repository/maven-snapshots/ ${{ matrix.image.plugins }}" >> $GITHUB_OUTPUT; echo "plugins=--repositories=https://central.sonatype.com/repository/maven-snapshots ${{ matrix.image.plugins }}" >> $GITHUB_OUTPUT;
else else
echo "plugins=${{ matrix.image.plugins }}" >> $GITHUB_OUTPUT echo "plugins=${{ matrix.image.plugins }}" >> $GITHUB_OUTPUT
fi fi
# Download release
- name: Download release # [workflow_dispatch]
# Download executable from GitHub Release
- name: Artifacts - Download release (workflow_dispatch)
id: download-github-release
if: github.event_name == 'workflow_dispatch' && github.event.inputs.force-download-artifact == 'false'
uses: robinraju/release-downloader@v1.12 uses: robinraju/release-downloader@v1.12
with: with:
tag: ${{steps.vars.outputs.tag}} tag: ${{steps.vars.outputs.tag}}
fileName: 'kestra-*' fileName: 'kestra-*'
out-file-path: build/executable out-file-path: build/executable
# [workflow_call]
# Download executable from artifact
- name: Artifacts - Download executable
if: github.event_name != 'workflow_dispatch' || steps.download-github-release.outcome == 'skipped'
uses: actions/download-artifact@v4
with:
name: exe
path: build/executable
- name: Copy exe to image - name: Copy exe to image
run: | run: |
cp build/executable/* docker/app/kestra && chmod +x docker/app/kestra cp build/executable/* docker/app/kestra && chmod +x docker/app/kestra

View File

@@ -43,7 +43,8 @@ jobs:
SONATYPE_GPG_KEYID: ${{ secrets.SONATYPE_GPG_KEYID }} SONATYPE_GPG_KEYID: ${{ secrets.SONATYPE_GPG_KEYID }}
SONATYPE_GPG_PASSWORD: ${{ secrets.SONATYPE_GPG_PASSWORD }} SONATYPE_GPG_PASSWORD: ${{ secrets.SONATYPE_GPG_PASSWORD }}
SONATYPE_GPG_FILE: ${{ secrets.SONATYPE_GPG_FILE }} SONATYPE_GPG_FILE: ${{ secrets.SONATYPE_GPG_FILE }}
GH_PERSONAL_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
SLACK_RELEASES_WEBHOOK_URL: ${{ secrets.SLACK_RELEASES_WEBHOOK_URL }}
end: end:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:

View File

@@ -1,23 +1,7 @@
name: Build Artifacts name: Build Artifacts
on: on:
workflow_call: workflow_call: {}
inputs:
plugin-version:
description: "Kestra version"
default: 'LATEST'
required: true
type: string
outputs:
docker-tag:
value: ${{ jobs.build.outputs.docker-tag }}
description: "The Docker image Tag for Kestra"
docker-artifact-name:
value: ${{ jobs.build.outputs.docker-artifact-name }}
description: "The GitHub artifact containing the Kestra docker image name."
plugins:
value: ${{ jobs.build.outputs.plugins }}
description: "The Kestra plugins list used for the build."
jobs: jobs:
build: build:
@@ -82,55 +66,6 @@ jobs:
run: | run: |
cp build/executable/* docker/app/kestra && chmod +x docker/app/kestra cp build/executable/* docker/app/kestra && chmod +x docker/app/kestra
# Docker Tag
- name: Setup - Docker vars
id: vars
shell: bash
run: |
TAG=${GITHUB_REF#refs/*/}
if [[ $TAG = "master" ]]
then
TAG="latest";
elif [[ $TAG = "develop" ]]
then
TAG="develop";
elif [[ $TAG = v* ]]
then
TAG="${TAG}";
else
TAG="build-${{ github.run_id }}";
fi
echo "tag=${TAG}" >> $GITHUB_OUTPUT
echo "artifact=docker-kestra-${TAG}" >> $GITHUB_OUTPUT
# Docker setup
- name: Docker - Setup QEMU
uses: docker/setup-qemu-action@v3
- name: Docker - Fix Qemu
shell: bash
run: |
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes -c yes
- name: Docker - Setup Buildx
uses: docker/setup-buildx-action@v3
# Docker Build
- name: Docker - Build & export image
uses: docker/build-push-action@v6
if: "!startsWith(github.ref, 'refs/tags/v')"
with:
context: .
push: false
file: Dockerfile
tags: |
kestra/kestra:${{ steps.vars.outputs.tag }}
build-args: |
KESTRA_PLUGINS=${{ steps.plugins.outputs.plugins }}
APT_PACKAGES=${{ env.DOCKER_APT_PACKAGES }}
PYTHON_LIBRARIES=${{ env.DOCKER_PYTHON_LIBRARIES }}
outputs: type=docker,dest=/tmp/${{ steps.vars.outputs.artifact }}.tar
# Upload artifacts # Upload artifacts
- name: Artifacts - Upload JAR - name: Artifacts - Upload JAR
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
@@ -143,10 +78,3 @@ jobs:
with: with:
name: exe name: exe
path: build/executable/ path: build/executable/
- name: Artifacts - 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

View File

@@ -1,14 +1,18 @@
name: Github - Release name: Github - Release
on: on:
workflow_dispatch:
workflow_call: workflow_call:
secrets: secrets:
GH_PERSONAL_TOKEN: GH_PERSONAL_TOKEN:
description: "The Github personal token." description: "The Github personal token."
required: true required: true
push: SLACK_RELEASES_WEBHOOK_URL:
tags: description: "The Slack webhook URL."
- '*' required: true
jobs: jobs:
publish: publish:

View File

@@ -41,8 +41,6 @@ jobs:
name: Build Artifacts name: Build Artifacts
if: ${{ github.event.inputs.force-download-artifact == 'true' }} if: ${{ github.event.inputs.force-download-artifact == 'true' }}
uses: ./.github/workflows/workflow-build-artifacts.yml uses: ./.github/workflows/workflow-build-artifacts.yml
with:
plugin-version: ${{ github.event.inputs.plugin-version != null && github.event.inputs.plugin-version || 'LATEST' }}
# ******************************************************************************************************************** # ********************************************************************************************************************
# Docker # Docker
# ******************************************************************************************************************** # ********************************************************************************************************************

View File

@@ -42,12 +42,16 @@ on:
SONATYPE_GPG_FILE: SONATYPE_GPG_FILE:
description: "The Sonatype GPG file." description: "The Sonatype GPG file."
required: true required: true
GH_PERSONAL_TOKEN:
description: "GH personnal Token."
required: true
SLACK_RELEASES_WEBHOOK_URL:
description: "Slack webhook for releases channel."
required: true
jobs: jobs:
build-artifacts: build-artifacts:
name: Build - Artifacts name: Build - Artifacts
uses: ./.github/workflows/workflow-build-artifacts.yml uses: ./.github/workflows/workflow-build-artifacts.yml
with:
plugin-version: ${{ github.event.inputs.plugin-version != null && github.event.inputs.plugin-version || 'LATEST' }}
Docker: Docker:
name: Publish Docker name: Publish Docker
@@ -77,4 +81,5 @@ jobs:
if: startsWith(github.ref, 'refs/tags/v') if: startsWith(github.ref, 'refs/tags/v')
uses: ./.github/workflows/workflow-github-release.yml uses: ./.github/workflows/workflow-github-release.yml
secrets: secrets:
GH_PERSONAL_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }} GH_PERSONAL_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
SLACK_RELEASES_WEBHOOK_URL: ${{ secrets.SLACK_RELEASES_WEBHOOK_URL }}

View File

@@ -87,13 +87,18 @@
#plugin-powerbi:io.kestra.plugin:plugin-powerbi:LATEST #plugin-powerbi:io.kestra.plugin:plugin-powerbi:LATEST
#plugin-pulsar:io.kestra.plugin:plugin-pulsar:LATEST #plugin-pulsar:io.kestra.plugin:plugin-pulsar:LATEST
#plugin-redis:io.kestra.plugin:plugin-redis:LATEST #plugin-redis:io.kestra.plugin:plugin-redis:LATEST
#plugin-scripts:io.kestra.plugin:plugin-script-bun:LATEST
#plugin-scripts:io.kestra.plugin:plugin-script-deno:LATEST
#plugin-scripts:io.kestra.plugin:plugin-script-go:LATEST #plugin-scripts:io.kestra.plugin:plugin-script-go:LATEST
#plugin-scripts:io.kestra.plugin:plugin-script-groovy:LATEST #plugin-scripts:io.kestra.plugin:plugin-script-groovy:LATEST
#plugin-scripts:io.kestra.plugin:plugin-script-jbang:LATEST #plugin-scripts:io.kestra.plugin:plugin-script-jbang:LATEST
#plugin-scripts:io.kestra.plugin:plugin-script-julia:LATEST #plugin-scripts:io.kestra.plugin:plugin-script-julia:LATEST
#plugin-scripts:io.kestra.plugin:plugin-script-jython:LATEST #plugin-scripts:io.kestra.plugin:plugin-script-jython:LATEST
#plugin-scripts:io.kestra.plugin:plugin-script-lua:LATEST
#plugin-scripts:io.kestra.plugin:plugin-script-nashorn:LATEST #plugin-scripts:io.kestra.plugin:plugin-script-nashorn:LATEST
#plugin-scripts:io.kestra.plugin:plugin-script-node:LATEST #plugin-scripts:io.kestra.plugin:plugin-script-node:LATEST
#plugin-scripts:io.kestra.plugin:plugin-script-perl:LATEST
#plugin-scripts:io.kestra.plugin:plugin-script-php:LATEST
#plugin-scripts:io.kestra.plugin:plugin-script-powershell:LATEST #plugin-scripts:io.kestra.plugin:plugin-script-powershell:LATEST
#plugin-scripts:io.kestra.plugin:plugin-script-python:LATEST #plugin-scripts:io.kestra.plugin:plugin-script-python:LATEST
#plugin-scripts:io.kestra.plugin:plugin-script-r:LATEST #plugin-scripts:io.kestra.plugin:plugin-script-r:LATEST

View File

@@ -27,7 +27,7 @@ public interface QueueInterface<T> extends Closeable, Pauseable {
void delete(String consumerGroup, T message) throws QueueException; void delete(String consumerGroup, T message) throws QueueException;
default Runnable receive(Consumer<Either<T, DeserializationException>> consumer) { default Runnable receive(Consumer<Either<T, DeserializationException>> consumer) {
return receive((String) null, consumer); return receive(null, consumer, false);
} }
default Runnable receive(String consumerGroup, Consumer<Either<T, DeserializationException>> consumer) { default Runnable receive(String consumerGroup, Consumer<Either<T, DeserializationException>> consumer) {

View File

@@ -161,7 +161,7 @@ public interface ExecutionRepositoryInterface extends SaveRepositoryInterface<Ex
} }
List<Execution> lastExecutions( List<Execution> lastExecutions(
@Nullable String tenantId, String tenantId,
@Nullable List<FlowFilter> flows @Nullable List<FlowFilter> flows
); );
} }

View File

@@ -8,6 +8,7 @@ import io.kestra.core.events.CrudEventType;
import io.kestra.core.exceptions.DeserializationException; import io.kestra.core.exceptions.DeserializationException;
import io.kestra.core.exceptions.InternalException; import io.kestra.core.exceptions.InternalException;
import io.kestra.core.metrics.MetricRegistry; import io.kestra.core.metrics.MetricRegistry;
import io.kestra.core.models.HasUID;
import io.kestra.core.models.conditions.Condition; import io.kestra.core.models.conditions.Condition;
import io.kestra.core.models.conditions.ConditionContext; import io.kestra.core.models.conditions.ConditionContext;
import io.kestra.core.models.executions.Execution; import io.kestra.core.models.executions.Execution;
@@ -318,7 +319,7 @@ public abstract class AbstractScheduler implements Scheduler, Service {
} }
synchronized (this) { // we need a sync block as we read then update so we should not do it in multiple threads concurrently synchronized (this) { // we need a sync block as we read then update so we should not do it in multiple threads concurrently
List<Trigger> triggers = triggerState.findAllForAllTenants(); Map<String, Trigger> triggers = triggerState.findAllForAllTenants().stream().collect(Collectors.toMap(HasUID::uid, Function.identity()));
flows flows
.stream() .stream()
@@ -328,7 +329,8 @@ public abstract class AbstractScheduler implements Scheduler, Service {
.flatMap(flow -> flow.getTriggers().stream().filter(trigger -> trigger instanceof WorkerTriggerInterface).map(trigger -> new FlowAndTrigger(flow, trigger))) .flatMap(flow -> flow.getTriggers().stream().filter(trigger -> trigger instanceof WorkerTriggerInterface).map(trigger -> new FlowAndTrigger(flow, trigger)))
.distinct() .distinct()
.forEach(flowAndTrigger -> { .forEach(flowAndTrigger -> {
Optional<Trigger> trigger = triggers.stream().filter(t -> t.uid().equals(Trigger.uid(flowAndTrigger.flow(), flowAndTrigger.trigger()))).findFirst(); // must have one or none String triggerUid = Trigger.uid(flowAndTrigger.flow(), flowAndTrigger.trigger());
Optional<Trigger> trigger = Optional.ofNullable(triggers.get(triggerUid));
if (trigger.isEmpty()) { if (trigger.isEmpty()) {
RunContext runContext = runContextFactory.of(flowAndTrigger.flow(), flowAndTrigger.trigger()); RunContext runContext = runContextFactory.of(flowAndTrigger.flow(), flowAndTrigger.trigger());
ConditionContext conditionContext = conditionService.conditionContext(runContext, flowAndTrigger.flow(), null); ConditionContext conditionContext = conditionService.conditionContext(runContext, flowAndTrigger.flow(), null);
@@ -467,9 +469,12 @@ public abstract class AbstractScheduler implements Scheduler, Service {
private List<FlowWithTriggers> computeSchedulable(List<FlowWithSource> flows, List<Trigger> triggerContextsToEvaluate, ScheduleContextInterface scheduleContext) { private List<FlowWithTriggers> computeSchedulable(List<FlowWithSource> flows, List<Trigger> triggerContextsToEvaluate, ScheduleContextInterface scheduleContext) {
List<String> flowToKeep = triggerContextsToEvaluate.stream().map(Trigger::getFlowId).toList(); List<String> flowToKeep = triggerContextsToEvaluate.stream().map(Trigger::getFlowId).toList();
List<String> flowIds = flows.stream().map(FlowId::uidWithoutRevision).toList();
Map<String, Trigger> triggerById = triggerContextsToEvaluate.stream().collect(Collectors.toMap(HasUID::uid, Function.identity()));
// delete trigger which flow has been deleted
triggerContextsToEvaluate.stream() triggerContextsToEvaluate.stream()
.filter(trigger -> !flows.stream().map(FlowId::uidWithoutRevision).toList().contains(FlowId.uid(trigger))) .filter(trigger -> !flowIds.contains(FlowId.uid(trigger)))
.forEach(trigger -> { .forEach(trigger -> {
try { try {
this.triggerState.delete(trigger); this.triggerState.delete(trigger);
@@ -491,12 +496,8 @@ public abstract class AbstractScheduler implements Scheduler, Service {
.map(abstractTrigger -> { .map(abstractTrigger -> {
RunContext runContext = runContextFactory.of(flow, abstractTrigger); RunContext runContext = runContextFactory.of(flow, abstractTrigger);
ConditionContext conditionContext = conditionService.conditionContext(runContext, flow, null); ConditionContext conditionContext = conditionService.conditionContext(runContext, flow, null);
Trigger triggerContext = null; Trigger triggerContext;
Trigger lastTrigger = triggerContextsToEvaluate Trigger lastTrigger = triggerById.get(Trigger.uid(flow, abstractTrigger));
.stream()
.filter(triggerContextToFind -> triggerContextToFind.uid().equals(Trigger.uid(flow, abstractTrigger)))
.findFirst()
.orElse(null);
// If a trigger is not found in triggers to evaluate, then we ignore it // If a trigger is not found in triggers to evaluate, then we ignore it
if (lastTrigger == null) { if (lastTrigger == null) {
return null; return null;

View File

@@ -9,6 +9,7 @@ import io.kestra.core.models.flows.FlowInterface;
import io.kestra.core.models.flows.FlowWithException; import io.kestra.core.models.flows.FlowWithException;
import io.kestra.core.models.flows.FlowWithSource; import io.kestra.core.models.flows.FlowWithSource;
import io.kestra.core.models.flows.GenericFlow; import io.kestra.core.models.flows.GenericFlow;
import io.kestra.core.models.tasks.RunnableTask;
import io.kestra.core.models.topologies.FlowTopology; import io.kestra.core.models.topologies.FlowTopology;
import io.kestra.core.models.triggers.AbstractTrigger; import io.kestra.core.models.triggers.AbstractTrigger;
import io.kestra.core.models.validations.ModelValidator; import io.kestra.core.models.validations.ModelValidator;
@@ -51,7 +52,6 @@ import java.util.stream.StreamSupport;
@Singleton @Singleton
@Slf4j @Slf4j
public class FlowService { public class FlowService {
@Inject @Inject
Optional<FlowRepositoryInterface> flowRepository; Optional<FlowRepositoryInterface> flowRepository;
@@ -236,6 +236,7 @@ public class FlowService {
} }
List<String> warnings = new ArrayList<>(checkValidSubflows(flow, tenantId)); List<String> warnings = new ArrayList<>(checkValidSubflows(flow, tenantId));
List<io.kestra.plugin.core.trigger.Flow> flowTriggers = ListUtils.emptyOnNull(flow.getTriggers()).stream() List<io.kestra.plugin.core.trigger.Flow> flowTriggers = ListUtils.emptyOnNull(flow.getTriggers()).stream()
.filter(io.kestra.plugin.core.trigger.Flow.class::isInstance) .filter(io.kestra.plugin.core.trigger.Flow.class::isInstance)
.map(io.kestra.plugin.core.trigger.Flow.class::cast) .map(io.kestra.plugin.core.trigger.Flow.class::cast)
@@ -246,6 +247,21 @@ public class FlowService {
} }
}); });
// add warning for runnable properties (timeout, workerGroup, taskCache) when used not in a runnable
flow.allTasksWithChilds().forEach(task -> {
if (!(task instanceof RunnableTask<?>)) {
if (task.getTimeout() != null) {
warnings.add("The task '" + task.getId() + "' cannot use the 'timeout' property as it's only relevant for runnable tasks.");
}
if (task.getTaskCache() != null) {
warnings.add("The task '" + task.getId() + "' cannot use the 'taskCache' property as it's only relevant for runnable tasks.");
}
if (task.getWorkerGroup() != null) {
warnings.add("The task '" + task.getId() + "' cannot use the 'workerGroup' property as it's only relevant for runnable tasks.");
}
}
});
return warnings; return warnings;
} }

View File

@@ -54,9 +54,10 @@ public class FlowValidator implements ConstraintValidator<FlowValidation, Flow>
violations.add("Namespace '" + value.getNamespace() + "' does not exist but is required to exist before a flow can be created in it."); violations.add("Namespace '" + value.getNamespace() + "' does not exist but is required to exist before a flow can be created in it.");
} }
List<Task> allTasks = value.allTasksWithChilds();
// tasks unique id // tasks unique id
List<String> taskIds = value.allTasksWithChilds() List<String> taskIds = allTasks.stream()
.stream()
.map(Task::getId) .map(Task::getId)
.toList(); .toList();
@@ -72,8 +73,8 @@ public class FlowValidator implements ConstraintValidator<FlowValidation, Flow>
violations.add("Duplicate trigger id with name [" + String.join(", ", duplicateIds) + "]"); violations.add("Duplicate trigger id with name [" + String.join(", ", duplicateIds) + "]");
} }
value.allTasksWithChilds() allTasks.stream()
.stream().filter(task -> task instanceof ExecutableTask<?> executableTask .filter(task -> task instanceof ExecutableTask<?> executableTask
&& value.getId().equals(executableTask.subflowId().flowId()) && value.getId().equals(executableTask.subflowId().flowId())
&& value.getNamespace().equals(executableTask.subflowId().namespace())) && value.getNamespace().equals(executableTask.subflowId().namespace()))
.forEach(task -> violations.add("Recursive call to flow [" + value.getNamespace() + "." + value.getId() + "]")); .forEach(task -> violations.add("Recursive call to flow [" + value.getNamespace() + "." + value.getId() + "]"));
@@ -102,7 +103,7 @@ public class FlowValidator implements ConstraintValidator<FlowValidation, Flow>
.map(input -> Pattern.compile("\\{\\{\\s*inputs." + input.getId() + "\\s*\\}\\}")) .map(input -> Pattern.compile("\\{\\{\\s*inputs." + input.getId() + "\\s*\\}\\}"))
.collect(Collectors.toList()); .collect(Collectors.toList());
List<String> invalidTasks = value.allTasks() List<String> invalidTasks = allTasks.stream()
.filter(task -> checkObjectFieldsWithPatterns(task, inputsWithMinusPatterns)) .filter(task -> checkObjectFieldsWithPatterns(task, inputsWithMinusPatterns))
.map(task -> task.getId()) .map(task -> task.getId())
.collect(Collectors.toList()); .collect(Collectors.toList());
@@ -112,12 +113,12 @@ public class FlowValidator implements ConstraintValidator<FlowValidation, Flow>
" [" + String.join(", ", invalidTasks) + "]"); " [" + String.join(", ", invalidTasks) + "]");
} }
List<Pattern> outputsWithMinusPattern = value.allTasks() List<Pattern> outputsWithMinusPattern = allTasks.stream()
.filter(output -> Optional.ofNullable(output.getId()).orElse("").contains("-")) .filter(output -> Optional.ofNullable(output.getId()).orElse("").contains("-"))
.map(output -> Pattern.compile("\\{\\{\\s*outputs\\." + output.getId() + "\\.[^}]+\\s*\\}\\}")) .map(output -> Pattern.compile("\\{\\{\\s*outputs\\." + output.getId() + "\\.[^}]+\\s*\\}\\}"))
.collect(Collectors.toList()); .collect(Collectors.toList());
invalidTasks = value.allTasks() invalidTasks = allTasks.stream()
.filter(task -> checkObjectFieldsWithPatterns(task, outputsWithMinusPattern)) .filter(task -> checkObjectFieldsWithPatterns(task, outputsWithMinusPattern))
.map(task -> task.getId()) .map(task -> task.getId())
.collect(Collectors.toList()); .collect(Collectors.toList());

View File

@@ -19,7 +19,6 @@ import lombok.experimental.SuperBuilder;
@NoArgsConstructor @NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_DEFAULT) @JsonInclude(JsonInclude.Include.NON_DEFAULT)
@EqualsAndHashCode @EqualsAndHashCode
//@TriggersDataFilterValidation
@Schema( @Schema(
title = "Display Execution data in a dashboard chart.", title = "Display Execution data in a dashboard chart.",
description = "Execution data can be displayed in charts broken out by Namespace and filtered by State, for example." description = "Execution data can be displayed in charts broken out by Namespace and filtered by State, for example."

View File

@@ -208,48 +208,50 @@ public class Subflow extends Task implements ExecutableTask<Subflow.Output>, Chi
return Optional.empty(); return Optional.empty();
} }
boolean isOutputsAllowed = runContext
.<Boolean>pluginConfiguration(PLUGIN_FLOW_OUTPUTS_ENABLED)
.orElse(true);
final Output.OutputBuilder builder = Output.builder() final Output.OutputBuilder builder = Output.builder()
.executionId(execution.getId()) .executionId(execution.getId())
.state(execution.getState().getCurrent()); .state(execution.getState().getCurrent());
final Map<String, Object> subflowOutputs = Optional
.ofNullable(flow.getOutputs())
.map(outputs -> outputs
.stream()
.collect(Collectors.toMap(
io.kestra.core.models.flows.Output::getId,
io.kestra.core.models.flows.Output::getValue)
)
)
.orElseGet(() -> isOutputsAllowed ? this.getOutputs() : null);
VariablesService variablesService = ((DefaultRunContext) runContext).getApplicationContext().getBean(VariablesService.class); VariablesService variablesService = ((DefaultRunContext) runContext).getApplicationContext().getBean(VariablesService.class);
if (subflowOutputs != null) { if (this.wait) { // we only compute outputs if we wait for the subflow
try { boolean isOutputsAllowed = runContext
Map<String, Object> outputs = runContext.render(subflowOutputs); .<Boolean>pluginConfiguration(PLUGIN_FLOW_OUTPUTS_ENABLED)
FlowInputOutput flowInputOutput = ((DefaultRunContext)runContext).getApplicationContext().getBean(FlowInputOutput.class); // this is hacking .orElse(true);
if (flow.getOutputs() != null && flowInputOutput != null) {
outputs = flowInputOutput.typedOutputs(flow, execution, outputs);
}
builder.outputs(outputs);
} catch (Exception e) {
runContext.logger().warn("Failed to extract outputs with the error: '{}'", e.getLocalizedMessage(), e);
var state = State.Type.fail(this);
Variables variables = variablesService.of(StorageContext.forTask(taskRun), builder.build());
taskRun = taskRun
.withState(state)
.withAttempts(Collections.singletonList(TaskRunAttempt.builder().state(new State().withState(state)).build()))
.withOutputs(variables);
return Optional.of(SubflowExecutionResult.builder() final Map<String, Object> subflowOutputs = Optional
.executionId(execution.getId()) .ofNullable(flow.getOutputs())
.state(State.Type.FAILED) .map(outputs -> outputs
.parentTaskRun(taskRun) .stream()
.build()); .collect(Collectors.toMap(
io.kestra.core.models.flows.Output::getId,
io.kestra.core.models.flows.Output::getValue)
)
)
.orElseGet(() -> isOutputsAllowed ? this.getOutputs() : null);
if (subflowOutputs != null) {
try {
Map<String, Object> outputs = runContext.render(subflowOutputs);
FlowInputOutput flowInputOutput = ((DefaultRunContext)runContext).getApplicationContext().getBean(FlowInputOutput.class); // this is hacking
if (flow.getOutputs() != null && flowInputOutput != null) {
outputs = flowInputOutput.typedOutputs(flow, execution, outputs);
}
builder.outputs(outputs);
} catch (Exception e) {
runContext.logger().warn("Failed to extract outputs with the error: '{}'", e.getLocalizedMessage(), e);
var state = State.Type.fail(this);
Variables variables = variablesService.of(StorageContext.forTask(taskRun), builder.build());
taskRun = taskRun
.withState(state)
.withAttempts(Collections.singletonList(TaskRunAttempt.builder().state(new State().withState(state)).build()))
.withOutputs(variables);
return Optional.of(SubflowExecutionResult.builder()
.executionId(execution.getId())
.state(State.Type.FAILED)
.parentTaskRun(taskRun)
.build());
}
} }
} }

View File

@@ -0,0 +1,11 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1765_9330)">
<path d="M244.592 215.915C251.569 208.938 262.881 208.938 269.858 215.915L298.537 244.595C305.514 251.572 305.514 262.883 298.537 269.86L269.858 298.54C262.881 305.517 251.569 305.517 244.592 298.54L215.913 269.86C208.936 262.883 208.936 251.572 215.913 244.595L244.592 215.915Z" />
<path d="M376.685 215.687C383.537 208.835 394.646 208.835 401.498 215.687L430.63 244.818C437.482 251.67 437.482 262.78 430.63 269.632L401.498 298.763C394.646 305.615 383.537 305.615 376.685 298.763L347.553 269.632C340.701 262.78 340.701 251.67 347.553 244.818L376.685 215.687Z" />
<path d="M244.818 83.8243C251.671 76.9722 262.78 76.9722 269.632 83.8243L298.763 112.956C305.616 119.808 305.616 130.917 298.763 137.769L269.632 166.901C262.78 173.753 251.671 173.753 244.818 166.901L215.687 137.769C208.835 130.917 208.835 119.808 215.687 112.956L244.818 83.8243Z" />
<path d="M232.611 178.663C239.588 185.64 239.588 196.951 232.611 203.928L203.931 232.608C196.955 239.585 185.643 239.585 178.666 232.608L149.986 203.928C143.01 196.952 143.01 185.64 149.986 178.663L178.666 149.983C185.643 143.006 196.955 143.006 203.931 149.983L232.611 178.663Z" />
<path d="M166.901 244.818C173.753 251.67 173.753 262.78 166.901 269.632L137.77 298.763C130.918 305.615 119.808 305.615 112.956 298.763L83.8246 269.632C76.9725 262.78 76.9725 251.67 83.8246 244.818L112.956 215.687C119.808 208.835 130.918 208.835 137.77 215.687L166.901 244.818Z" />
<path d="M364.472 178.663C371.449 185.64 371.449 196.951 364.472 203.928L335.793 232.608C328.816 239.585 317.504 239.585 310.527 232.608L281.848 203.928C274.871 196.952 274.871 185.64 281.848 178.663L310.527 149.983C317.504 143.006 328.816 143.006 335.793 149.983L364.472 178.663Z" />
<path d="M285.45 367.015C301.037 382.602 301.037 407.873 285.45 423.46C269.863 439.047 244.591 439.047 229.004 423.46C213.417 407.873 213.417 382.602 229.004 367.015C244.591 351.428 269.863 351.428 285.45 367.015Z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,11 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1765_9330)">
<path d="M244.592 215.915C251.569 208.938 262.881 208.938 269.858 215.915L298.537 244.595C305.514 251.572 305.514 262.883 298.537 269.86L269.858 298.54C262.881 305.517 251.569 305.517 244.592 298.54L215.913 269.86C208.936 262.883 208.936 251.572 215.913 244.595L244.592 215.915Z" />
<path d="M376.685 215.687C383.537 208.835 394.646 208.835 401.498 215.687L430.63 244.818C437.482 251.67 437.482 262.78 430.63 269.632L401.498 298.763C394.646 305.615 383.537 305.615 376.685 298.763L347.553 269.632C340.701 262.78 340.701 251.67 347.553 244.818L376.685 215.687Z" />
<path d="M244.818 83.8243C251.671 76.9722 262.78 76.9722 269.632 83.8243L298.763 112.956C305.616 119.808 305.616 130.917 298.763 137.769L269.632 166.901C262.78 173.753 251.671 173.753 244.818 166.901L215.687 137.769C208.835 130.917 208.835 119.808 215.687 112.956L244.818 83.8243Z" />
<path d="M232.611 178.663C239.588 185.64 239.588 196.951 232.611 203.928L203.931 232.608C196.955 239.585 185.643 239.585 178.666 232.608L149.986 203.928C143.01 196.952 143.01 185.64 149.986 178.663L178.666 149.983C185.643 143.006 196.955 143.006 203.931 149.983L232.611 178.663Z" />
<path d="M166.901 244.818C173.753 251.67 173.753 262.78 166.901 269.632L137.77 298.763C130.918 305.615 119.808 305.615 112.956 298.763L83.8246 269.632C76.9725 262.78 76.9725 251.67 83.8246 244.818L112.956 215.687C119.808 208.835 130.918 208.835 137.77 215.687L166.901 244.818Z" />
<path d="M364.472 178.663C371.449 185.64 371.449 196.951 364.472 203.928L335.793 232.608C328.816 239.585 317.504 239.585 310.527 232.608L281.848 203.928C274.871 196.952 274.871 185.64 281.848 178.663L310.527 149.983C317.504 143.006 328.816 143.006 335.793 149.983L364.472 178.663Z" />
<path d="M285.45 367.015C301.037 382.602 301.037 407.873 285.45 423.46C269.863 439.047 244.591 439.047 229.004 423.46C213.417 407.873 213.417 382.602 229.004 367.015C244.591 351.428 269.863 351.428 285.45 367.015Z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -44,6 +44,7 @@ import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.*; import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import static io.kestra.core.models.flows.FlowScope.USER; import static io.kestra.core.models.flows.FlowScope.USER;
@@ -740,4 +741,16 @@ public abstract class AbstractExecutionRepositoryTest {
executions = executionRepository.find(Pageable.from(1, 10), MAIN_TENANT, filters); executions = executionRepository.find(Pageable.from(1, 10), MAIN_TENANT, filters);
assertThat(executions.size()).isEqualTo(0L); assertThat(executions.size()).isEqualTo(0L);
} }
@Test
protected void shouldReturnLastExecutionsWhenInputsAreNull() {
inject();
List<Execution> lastExecutions = executionRepository.lastExecutions(MAIN_TENANT, null);
assertThat(lastExecutions).isNotEmpty();
Set<String> flowIds = lastExecutions.stream().map(Execution::getFlowId).collect(Collectors.toSet());
assertThat(flowIds.size()).isEqualTo(lastExecutions.size());
}
} }

View File

@@ -372,4 +372,44 @@ class FlowServiceTest {
assertThat(exceptions.size()).isZero(); assertThat(exceptions.size()).isZero();
} }
@Test
void shouldReturnValidationForRunnablePropsOnFlowable() {
// Given
String source = """
id: dolphin_164914
namespace: company.team
tasks:
- id: for
type: io.kestra.plugin.core.flow.ForEach
values: [1, 2, 3]
workerGroup:
key: toto
timeout: PT10S
taskCache:
enabled: true
tasks:
- id: hello
type: io.kestra.plugin.core.log.Log
message: Hello World! 🚀
workerGroup:
key: toto
timeout: PT10S
taskCache:
enabled: true
""";
// When
List<ValidateConstraintViolation> results = flowService.validate("my-tenant", source);
// Then
assertThat(results).hasSize(1);
assertThat(results.getFirst().getWarnings()).hasSize(3);
assertThat(results.getFirst().getWarnings()).containsExactlyInAnyOrder(
"The task 'for' cannot use the 'timeout' property as it's only relevant for runnable tasks.",
"The task 'for' cannot use the 'taskCache' property as it's only relevant for runnable tasks.",
"The task 'for' cannot use the 'workerGroup' property as it's only relevant for runnable tasks."
);
}
} }

View File

@@ -4,16 +4,24 @@ import io.kestra.core.junit.annotations.KestraTest;
import io.kestra.core.junit.annotations.LoadFlows; import io.kestra.core.junit.annotations.LoadFlows;
import io.kestra.core.models.Label; import io.kestra.core.models.Label;
import io.kestra.core.models.executions.Execution; import io.kestra.core.models.executions.Execution;
import io.kestra.core.models.flows.State;
import io.kestra.core.queues.QueueException; import io.kestra.core.queues.QueueException;
import io.kestra.core.queues.QueueFactoryInterface;
import io.kestra.core.queues.QueueInterface;
import io.kestra.core.repositories.ExecutionRepositoryInterface; import io.kestra.core.repositories.ExecutionRepositoryInterface;
import io.kestra.core.runners.RunnerUtils; import io.kestra.core.runners.RunnerUtils;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.inject.Named;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
import static io.kestra.core.tenant.TenantService.MAIN_TENANT; import static io.kestra.core.tenant.TenantService.MAIN_TENANT;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertTrue;
@KestraTest(startRunner = true) @KestraTest(startRunner = true)
class SubflowRunnerTest { class SubflowRunnerTest {
@@ -24,6 +32,10 @@ class SubflowRunnerTest {
@Inject @Inject
private ExecutionRepositoryInterface executionRepository; private ExecutionRepositoryInterface executionRepository;
@Inject
@Named(QueueFactoryInterface.EXECUTION_NAMED)
protected QueueInterface<Execution> executionQueue;
@Test @Test
@LoadFlows({"flows/valids/subflow-inherited-labels-child.yaml", "flows/valids/subflow-inherited-labels-parent.yaml"}) @LoadFlows({"flows/valids/subflow-inherited-labels-child.yaml", "flows/valids/subflow-inherited-labels-parent.yaml"})
void inheritedLabelsAreOverridden() throws QueueException, TimeoutException { void inheritedLabelsAreOverridden() throws QueueException, TimeoutException {
@@ -50,4 +62,29 @@ class SubflowRunnerTest {
new Label("parentFlowLabel2", "value2") // inherited from the parent flow new Label("parentFlowLabel2", "value2") // inherited from the parent flow
); );
} }
@Test
@LoadFlows({"flows/valids/subflow-parent-no-wait.yaml", "flows/valids/subflow-child-with-output.yaml"})
void subflowOutputWithoutWait() throws QueueException, TimeoutException, InterruptedException {
AtomicReference<Execution> childExecution = new AtomicReference<>();
CountDownLatch countDownLatch = new CountDownLatch(1);
Runnable closing = executionQueue.receive(either -> {
if (either.isLeft() && either.getLeft().getFlowId().equals("subflow-child-with-output") && either.getLeft().getState().isTerminated()) {
childExecution.set(either.getLeft());
countDownLatch.countDown();
}
});
Execution parentExecution = runnerUtils.runOne(MAIN_TENANT, "io.kestra.tests", "subflow-parent-no-wait");
String childExecutionId = (String) parentExecution.findTaskRunsByTaskId("subflow").getFirst().getOutputs().get("executionId");
assertThat(childExecutionId).isNotBlank();
assertThat(parentExecution.getState().getCurrent()).isEqualTo(State.Type.SUCCESS);
assertThat(parentExecution.getTaskRunList()).hasSize(1);
assertTrue(countDownLatch.await(10, TimeUnit.SECONDS));
assertThat(childExecution.get().getId()).isEqualTo(childExecutionId);
assertThat(childExecution.get().getState().getCurrent()).isEqualTo(State.Type.SUCCESS);
assertThat(childExecution.get().getTaskRunList()).hasSize(1);
closing.run();
}
} }

View File

@@ -0,0 +1,12 @@
id: subflow-child-with-output
namespace: io.kestra.tests
tasks:
- id: return
type: io.kestra.plugin.core.debug.Return
format: "Some value"
outputs:
- id: flow_a_output
type: STRING
value: "{{ outputs.return.value }}"

View File

@@ -0,0 +1,9 @@
id: subflow-parent-no-wait
namespace: io.kestra.tests
tasks:
- id: subflow
type: io.kestra.plugin.core.flow.Subflow
namespace: io.kestra.tests
flowId: subflow-child-with-output
wait: false

View File

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

View File

@@ -869,8 +869,8 @@ public abstract class AbstractJdbcExecutionRepository extends AbstractJdbcReposi
@Override @Override
public List<Execution> lastExecutions( public List<Execution> lastExecutions(
@Nullable String tenantId, String tenantId,
List<FlowFilter> flows @Nullable List<FlowFilter> flows
) { ) {
return this.jdbcRepository return this.jdbcRepository
.getDslContextWrapper() .getDslContextWrapper()
@@ -892,14 +892,19 @@ public abstract class AbstractJdbcExecutionRepository extends AbstractJdbcReposi
.and(NORMAL_KIND_CONDITION) .and(NORMAL_KIND_CONDITION)
.and(field("end_date").isNotNull()) .and(field("end_date").isNotNull())
.and(DSL.or( .and(DSL.or(
flows ListUtils.emptyOnNull(flows).isEmpty() ?
.stream() DSL.trueCondition()
.map(flow -> DSL.and( :
field("namespace").eq(flow.getNamespace()), DSL.or(
field("flow_id").eq(flow.getId()) flows.stream()
)) .map(flow -> DSL.and(
.toList() field("namespace").eq(flow.getNamespace()),
)); field("flow_id").eq(flow.getId())
))
.toList()
)
)
);
Table<Record2<Object, Integer>> cte = subquery.asTable("cte"); Table<Record2<Object, Integer>> cte = subquery.asTable("cte");

14
ui/package-lock.json generated
View File

@@ -10,7 +10,7 @@
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@js-joda/core": "^5.6.5", "@js-joda/core": "^5.6.5",
"@kestra-io/ui-libs": "^0.0.228", "@kestra-io/ui-libs": "^0.0.232",
"@vue-flow/background": "^1.3.2", "@vue-flow/background": "^1.3.2",
"@vue-flow/controls": "^1.1.2", "@vue-flow/controls": "^1.1.2",
"@vue-flow/core": "^1.45.0", "@vue-flow/core": "^1.45.0",
@@ -1792,9 +1792,9 @@
} }
}, },
"node_modules/@eslint/plugin-kit": { "node_modules/@eslint/plugin-kit": {
"version": "0.3.3", "version": "0.3.4",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.3.tgz", "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz",
"integrity": "sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==", "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
@@ -3133,9 +3133,9 @@
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
}, },
"node_modules/@kestra-io/ui-libs": { "node_modules/@kestra-io/ui-libs": {
"version": "0.0.228", "version": "0.0.232",
"resolved": "https://registry.npmjs.org/@kestra-io/ui-libs/-/ui-libs-0.0.228.tgz", "resolved": "https://registry.npmjs.org/@kestra-io/ui-libs/-/ui-libs-0.0.232.tgz",
"integrity": "sha512-ZSUpBEhTJ7Ul0QtMU/ioDlgryoVwZv/BD1ko96q+m9sCA4Uab1yi2LUf+ZpEEzZWH8r37E/CNK6HNjG+tei7eA==", "integrity": "sha512-4Z1DNxWEZSEEy2Tv63uNf2remxb/IqVUY01/qCaeYjLcp5axrS7Dn43N8DspA4EPdlhe4JFq2RhG13Pom+JDQA==",
"dependencies": { "dependencies": {
"@nuxtjs/mdc": "^0.16.1", "@nuxtjs/mdc": "^0.16.1",
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",

View File

@@ -24,7 +24,7 @@
}, },
"dependencies": { "dependencies": {
"@js-joda/core": "^5.6.5", "@js-joda/core": "^5.6.5",
"@kestra-io/ui-libs": "^0.0.228", "@kestra-io/ui-libs": "^0.0.232",
"@vue-flow/background": "^1.3.2", "@vue-flow/background": "^1.3.2",
"@vue-flow/controls": "^1.1.2", "@vue-flow/controls": "^1.1.2",
"@vue-flow/core": "^1.45.0", "@vue-flow/core": "^1.45.0",
@@ -149,7 +149,7 @@
"@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7" "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7"
}, },
"el-table-infinite-scroll": { "el-table-infinite-scroll": {
"vue": "$vue" "vue": "^3.5.18"
}, },
"storybook": "$storybook" "storybook": "$storybook"
}, },

View File

@@ -49,6 +49,7 @@
ref="tabContent" ref="tabContent"
:is="activeTab.component" :is="activeTab.component"
@go-to-detail="blueprintId => selectedBlueprintId = blueprintId" @go-to-detail="blueprintId => selectedBlueprintId = blueprintId"
:namespace
:embed="activeTab.props && activeTab.props.embed !== undefined ? activeTab.props.embed : true" :embed="activeTab.props && activeTab.props.embed !== undefined ? activeTab.props.embed : true"
/> />
</section> </section>
@@ -163,16 +164,11 @@
}, },
getTabClasses(tab) { getTabClasses(tab) {
const isEnterpriseTab = tab.locked; const isEnterpriseTab = tab.locked;
const isGanttTab = tab.name === "gantt";
const ROUTES = ["/flows/edit/", "/namespaces/edit/"];
const EDIT_ROUTES = ROUTES.some(route => this.$route.path.startsWith(route));
const isOverviewTab = EDIT_ROUTES && tab.title === "Overview";
return { return {
"container": !isEnterpriseTab && !isOverviewTab, "container": !isEnterpriseTab,
"mt-4": !isEnterpriseTab && !isOverviewTab, "mt-4": !isEnterpriseTab,
"px-0": isEnterpriseTab && isOverviewTab, "px-0": isEnterpriseTab,
"gantt-container": isGanttTab
}; };
}, },
}, },

View File

@@ -1,6 +1,6 @@
<template> <template>
<div @click="handleClick" class="d-flex my-2 p-2 rounded element" :class="{'moved': moved}"> <div @click="handleClick" class="d-flex my-2 p-2 rounded element" :class="{'moved': moved}">
<div class="me-2 icon"> <div v-if="props.parentPathComplete !== 'inputs'" class="me-2 icon">
<TaskIcon :cls="element.type" :icons only-icon /> <TaskIcon :cls="element.type" :icons only-icon />
</div> </div>
@@ -85,6 +85,7 @@
<style scoped lang="scss"> <style scoped lang="scss">
@import "../../styles/code.scss"; @import "../../styles/code.scss";
@import "@kestra-io/ui-libs/src/scss/_color-palette";
.element { .element {
cursor: pointer; cursor: pointer;
@@ -107,7 +108,8 @@
} }
.playground-run-task{ .playground-run-task{
background-color: blue; color: $base-white;
background-color: $base-blue-400;
height: 16px; height: 16px;
width: 16px; width: 16px;
font-size: 4px; font-size: 4px;

View File

@@ -30,7 +30,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {onMounted, computed, inject, ref, provide} from "vue"; import {onMounted, computed, inject, ref, provide, onActivated} from "vue";
import {useI18n} from "vue-i18n"; import {useI18n} from "vue-i18n";
import {useStore} from "vuex"; import {useStore} from "vuex";
import {usePluginsStore} from "../../../stores/plugins"; import {usePluginsStore} from "../../../stores/plugins";
@@ -73,6 +73,10 @@
return !complexObject return !complexObject
} }
onActivated(() => {
pluginsStore.updateDocumentation();
});
function onTaskUpdateField(key: string, val: any) { function onTaskUpdateField(key: string, val: any) {
const realValue = val === null || val === undefined ? undefined : const realValue = val === null || val === undefined ? undefined :
// allow array to be created with null values (specifically for metadata) // allow array to be created with null values (specifically for metadata)
@@ -160,11 +164,8 @@
task: parsedFlow.value, task: parsedFlow.value,
}) })
const fieldsFromSchemaTop = computed(() => MAIN_KEYS.map(key => getFieldFromKey(key, "main"))) const fieldsFromSchemaTop = computed(() => MAIN_KEYS.map(key => getFieldFromKey(key, "main")))
const fieldsFromSchemaRest = computed(() => { const fieldsFromSchemaRest = computed(() => {
return Object.keys(pluginsStore.flowRootProperties ?? {}) return Object.keys(pluginsStore.flowRootProperties ?? {})
.filter((key) => !MAIN_KEYS.includes(key) && !HIDDEN_FIELDS.includes(key)) .filter((key) => !MAIN_KEYS.includes(key) && !HIDDEN_FIELDS.includes(key))

View File

@@ -18,7 +18,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {computed, onBeforeMount, ref} from "vue"; import {computed, onBeforeMount, ref, watch} from "vue";
import type {Dashboard, Chart} from "./composables/useDashboards"; import type {Dashboard, Chart} from "./composables/useDashboards";
import {ALLOWED_CREATION_ROUTES, getDashboard, processFlowYaml} from "./composables/useDashboards"; import {ALLOWED_CREATION_ROUTES, getDashboard, processFlowYaml} from "./composables/useDashboards";
@@ -104,6 +104,8 @@
if (props.isFlow && ID === "default") load("default", processFlowYaml(YAML_FLOW, route.params.namespace as string, route.params.id as string)); if (props.isFlow && ID === "default") load("default", processFlowYaml(YAML_FLOW, route.params.namespace as string, route.params.id as string));
else if (props.isNamespace && ID === "default") load("default", YAML_NAMESPACE); else if (props.isNamespace && ID === "default") load("default", YAML_NAMESPACE);
}); });
watch(route, async (_) => refreshCharts());
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@@ -92,6 +92,20 @@ export function defaultConfig(override, theme) {
); );
} }
export function extractState(value) {
if (!value || typeof value !== "string") return value;
if (value.includes(",")) {
const stateNames = State.arrayAllStates().map(state => state.name);
const matchedState = value.split(",")
.map(part => part.trim())
.find(part => stateNames.includes(part.toUpperCase()));
return matchedState || value;
}
return value;
}
export function chartClick(moment, router, route, event, parsedData, elements, type = "label") { export function chartClick(moment, router, route, event, parsedData, elements, type = "label") {
const query = {}; const query = {};
@@ -107,7 +121,7 @@ export function chartClick(moment, router, route, event, parsedData, elements, t
state = parsedData.labels[element.index]; state = parsedData.labels[element.index];
} }
if (state) { if (state) {
query.state = state; query.state = extractState(state);
query.scope = "USER"; query.scope = "USER";
query.size = 100; query.size = 100;
query.page = 1; query.page = 1;
@@ -137,7 +151,7 @@ export function chartClick(moment, router, route, event, parsedData, elements, t
} }
if (event.state) { if (event.state) {
query.state = event.state; query.state = extractState(event.state);
} }
if (route.query.namespace) { if (route.query.namespace) {

View File

@@ -131,7 +131,7 @@ export function useChartGenerator(props: {chart: Chart; filters: string[]; showD
const data = ref(); const data = ref();
const generate = async (id: string, pagination?: { pageNumber: number; pageSize: number }) => { const generate = async (id: string, pagination?: { pageNumber: number; pageSize: number }) => {
const filters = props.filters.concat(decodeSearchParams(route.query, undefined, []) ?? []); const filters = props.filters.concat(decodeSearchParams(route.query, undefined, []) ?? []);
const parameters: Parameters = {...(pagination ?? {}), ...(filters ?? {})}; const parameters: Parameters = {...(pagination ?? {}), filters: (filters ?? {})};
if (!props.showDefault) { if (!props.showDefault) {
data.value = await dashboardStore.generate(id, props.chart.id, parameters); data.value = await dashboardStore.generate(id, props.chart.id, parameters);

View File

@@ -1,24 +1,22 @@
<template> <template>
<div class="execution-pending"> <EmptyTemplate class="queued">
<EmptyTemplate class="queued"> <img src="../../assets/queued_visual.svg" alt="Queued Execution">
<img src="../../assets/queued_visual.svg" alt="Queued Execution"> <h5 class="mt-4 fw-bold">
<h5 class="mt-4 fw-bold"> {{ $t('execution_status') }}
{{ $t('execution_status') }} <span
<span class="ms-2 px-2 py-1 rounded fs-7 fw-normal"
class="ms-2 px-2 py-1 rounded fs-7 fw-normal" :style="getStyle(execution.state.current)"
:style="getStyle(execution.state.current)" >
> {{ execution.state.current }}
{{ execution.state.current }} </span>
</span> </h5>
</h5> <p class="mt-4 mb-0">
<p class="mt-4 mb-0"> {{ $t('no_tasks_running') }}
{{ $t('no_tasks_running') }} </p>
</p> <p>
<p> {{ $t('execution_starts_progress') }}
{{ $t('execution_starts_progress') }} </p>
</p> </EmptyTemplate>
</EmptyTemplate>
</div>
</template> </template>
<script setup> <script setup>

View File

@@ -59,18 +59,12 @@
this.previousExecutionId = this.$route.params.id this.previousExecutionId = this.$route.params.id
}, },
watch: { watch: {
$route(newValue, oldValue) { $route() {
this.executionsStore.taskRun = undefined; this.executionsStore.taskRun = undefined;
if (oldValue.name === newValue.name && this.previousExecutionId !== this.$route.params.id) { if (this.previousExecutionId !== this.$route.params.id) {
this.follow()
}
// if we change the execution id, we need to close the sse
if (this.executionsStore.execution && this.$route.params.id != this.executionsStore.execution.id) {
this.executionsStore.closeSSE();
window.removeEventListener("popstate", this.follow)
this.executionsStore.execution = undefined;
this.$store.commit("flow/setFlow", undefined); this.$store.commit("flow/setFlow", undefined);
this.$store.commit("flow/setFlowGraph", undefined); this.$store.commit("flow/setFlowGraph", undefined);
this.follow();
} }
}, },
}, },
@@ -80,13 +74,6 @@
this.executionsStore.followExecution(this.$route.params, this.$t); this.executionsStore.followExecution(this.$route.params, this.$t);
}, },
getTabs() { getTabs() {
},
},
computed: {
...mapState("auth", ["user"]),
...mapStores(useCoreStore, useExecutionsStore),
tabs() {
return [ return [
{ {
name: undefined, name: undefined,
@@ -135,6 +122,13 @@
locked: true locked: true
} }
]; ];
}
},
computed: {
...mapState("auth", ["user"]),
...mapStores(useCoreStore, useExecutionsStore),
tabs() {
return this.getTabs();
}, },
routeInfo() { routeInfo() {
const ns = this.$route.params.namespace; const ns = this.$route.params.namespace;
@@ -212,4 +206,4 @@
.full-space { .full-space {
flex: 1 1 auto; flex: 1 1 auto;
} }
</style> </style>

View File

@@ -260,7 +260,7 @@
class-name="shrink" class-name="shrink"
> >
<template #default="scope"> <template #default="scope">
<code>{{ scope.row.flowRevision }}</code> <code class="code-text">{{ scope.row.flowRevision }}</code>
</template> </template>
</el-table-column> </el-table-column>
@@ -293,7 +293,7 @@
</el-tooltip> </el-tooltip>
</template> </template>
<template #default="scope"> <template #default="scope">
<code> <code class="code-text">
{{ scope.row.taskRunList?.slice(-1)[0].taskId }} {{ scope.row.taskRunList?.slice(-1)[0].taskId }}
{{ {{
scope.row.taskRunList?.slice(-1)[0].attempts?.length > 1 ? `(${scope.row.taskRunList?.slice(-1)[0].attempts.length})` : "" scope.row.taskRunList?.slice(-1)[0].attempts?.length > 1 ? `(${scope.row.taskRunList?.slice(-1)[0].attempts.length})` : ""
@@ -1122,6 +1122,9 @@
color: #ffb703; color: #ffb703;
} }
} }
.code-text {
color: var(--ks-content-primary);
}
</style> </style>
<style lang="scss"> <style lang="scss">

View File

@@ -45,8 +45,8 @@
</el-tooltip> </el-tooltip>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button-group class="min-w-auto"> <el-button-group class="ks-b-group">
<restart :execution="executionsStore.execution" class="ms-0" @follow="forwardEvent('follow', $event)" /> <restart v-if="executionsStore.execution" :execution="executionsStore.execution" class="ms-0" @follow="forwardEvent('follow', $event)" />
<el-button @click="downloadContent()"> <el-button @click="downloadContent()">
<kicon :tooltip="$t('download logs')"> <kicon :tooltip="$t('download logs')">
<download /> <download />
@@ -60,7 +60,7 @@
</el-button-group> </el-button-group>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button-group class="min-w-auto"> <el-button-group class="ks-b-group">
<el-button @click="loadLogs()"> <el-button @click="loadLogs()">
<kicon :tooltip="$t('refresh')"> <kicon :tooltip="$t('refresh')">
<refresh /> <refresh />
@@ -361,4 +361,9 @@
align-items: flex-start; align-items: flex-start;
} }
} }
.ks-b-group {
min-width: auto!important;
max-width: max-content !important;
}
</style> </style>

View File

@@ -102,7 +102,8 @@
loadDefinition() { loadDefinition() {
this.executionsStore.loadFlowForExecution({ this.executionsStore.loadFlowForExecution({
flowId: this.execution.flowId, flowId: this.execution.flowId,
namespace: this.execution.namespace namespace: this.execution.namespace,
store: true
}); });
}, },
}, },

View File

@@ -37,13 +37,14 @@
</div> </div>
<div class="d-flex flex-column p-3 debug"> <div class="d-flex flex-column p-3 debug">
<editor <Editor
ref="debugEditor" ref="debugEditor"
:full-height="false" :full-height="false"
:custom-height="20" :custom-height="20"
:input="true" :input="true"
:navbar="false" :navbar="false"
:model-value="computedDebugValue" :model-value="computedDebugValue"
@update:model-value="editorValue = $event"
@confirm="onDebugExpression($event)" @confirm="onDebugExpression($event)"
class="w-100" class="w-100"
/> />
@@ -53,7 +54,7 @@
:icon="Refresh" :icon="Refresh"
@click=" @click="
onDebugExpression( onDebugExpression(
debugEditor.editor.getValue(), editorValue.length > 0 ? editorValue : computedDebugValue,
) )
" "
class="mt-3" class="mt-3"
@@ -61,7 +62,7 @@
{{ $t("eval.render") }} {{ $t("eval.render") }}
</el-button> </el-button>
<editor <Editor
v-if="debugExpression" v-if="debugExpression"
:read-only="true" :read-only="true"
:input="true" :input="true"
@@ -98,7 +99,7 @@
<VarValue <VarValue
v-if="selectedValue && displayVarValue()" v-if="selectedValue && displayVarValue()"
:value="selectedValue.uri ? selectedValue.uri : selectedValue" :value="selectedValue?.uri ? selectedValue?.uri : selectedValue"
:execution="execution" :execution="execution"
/> />
</div> </div>
@@ -129,8 +130,9 @@
}>(); }>();
const cascader = ref<any>(null); const cascader = ref<any>(null);
const debugEditor = ref<any>(null); const debugEditor = ref<InstanceType<typeof Editor>>();
const selected = ref<string[]>([]); const selected = ref<string[]>([]);
const editorValue = ref("");
const debugExpression = ref(""); const debugExpression = ref("");
const debugError = ref(""); const debugError = ref("");
const debugStackTrace = ref(""); const debugStackTrace = ref("");
@@ -425,4 +427,4 @@
font-size: var(--el-font-size-base); font-size: var(--el-font-size-base);
} }
} }
</style> </style>

View File

@@ -80,6 +80,7 @@
:input="true" :input="true"
:navbar="false" :navbar="false"
:model-value="computedDebugValue" :model-value="computedDebugValue"
@update:model-value="editorValue = $event"
@confirm="onDebugExpression($event)" @confirm="onDebugExpression($event)"
class="w-100" class="w-100"
/> />
@@ -88,8 +89,9 @@
type="primary" type="primary"
@click=" @click="
onDebugExpression( onDebugExpression(
debugEditor.editor.getValue(), editorValue.length > 0 ? editorValue : computedDebugValue,
) )
" "
class="mt-3" class="mt-3"
> >
@@ -163,8 +165,9 @@
import CopyToClipboard from "../../layout/CopyToClipboard.vue"; import CopyToClipboard from "../../layout/CopyToClipboard.vue";
import Editor from "../../inputs/Editor.vue"; import Editor from "../../inputs/Editor.vue";
const editorValue = ref("");
const debugCollapse = ref(""); const debugCollapse = ref("");
const debugEditor = ref(null); const debugEditor = ref<InstanceType<typeof Editor>>();
const debugExpression = ref(""); const debugExpression = ref("");
const computedDebugValue = computed(() => { const computedDebugValue = computed(() => {
const formatTask = (task) => { const formatTask = (task) => {
@@ -422,7 +425,7 @@
const displayVarValue = () => const displayVarValue = () =>
isFile(selectedValue.value) || isFile(selectedValue.value) ||
selectedValue.value !== debugExpression.value; selectedValue.value !== debugExpression.value;
const leftWidth = ref(70); const leftWidth = ref(70);
const startDragging = (event: MouseEvent) => { const startDragging = (event: MouseEvent) => {
const startX = event.clientX; const startX = event.clientX;

View File

@@ -33,9 +33,8 @@
</div> </div>
<div v-else class="empty-state"> <div v-else class="empty-state">
<img :src="EmptyVisualPlayground"> <img :src="EmptyVisualPlayground">
<p> <p>{{ t("playground.run_task_info") }}</p>
{{ t("playground.empty") }} <p>{{ t("playground.play_icon_info") }}</p>
</p>
</div> </div>
</div> </div>
<div class="run-history" :class="{'history-visible': historyVisible}"> <div class="run-history" :class="{'history-visible': historyVisible}">
@@ -51,7 +50,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {computed, ref, markRaw, watch, onUnmounted} from "vue"; import {computed, ref, markRaw, watch, onUnmounted, onMounted} from "vue";
import {useI18n} from "vue-i18n"; import {useI18n} from "vue-i18n";
import ChartTimelineIcon from "vue-material-design-icons/ChartTimeline.vue"; import ChartTimelineIcon from "vue-material-design-icons/ChartTimeline.vue";
import HistoryIcon from "vue-material-design-icons/History.vue"; import HistoryIcon from "vue-material-design-icons/History.vue";
@@ -100,6 +99,10 @@
const activeTab = ref(tabs.value[0]); const activeTab = ref(tabs.value[0]);
onMounted(() => {
playgroundStore.runFromQuery();
});
onUnmounted(() => { onUnmounted(() => {
executionsStore.closeSSE(); executionsStore.closeSSE();
}); });
@@ -216,7 +219,7 @@
border: none; border: none;
border-radius: 4px; border-radius: 4px;
&.activeTab { &.activeTab {
color: var(--ks-content-primary); color: $base-white;
background-color: $base-blue-500; background-color: $base-blue-500;
} }
} }
@@ -242,4 +245,4 @@
} }
} }
} }
</style> </style>

View File

@@ -428,7 +428,8 @@
), ),
loading: false, loading: false,
lastExecutionByFlowReady: false, lastExecutionByFlowReady: false,
latestExecutions: [] latestExecutions: [],
dblClickRouteName: "flows/update"
}; };
}, },
computed: { computed: {

View File

@@ -89,7 +89,7 @@
showKeyShortcuts(); showKeyShortcuts();
return; return;
} }
if(openTabs.value.includes(tabValue)){ if(openTabs.value.includes(tabValue)){
focusTab(tabValue) focusTab(tabValue)
return return
@@ -130,6 +130,7 @@
const [ const [
, ,
parentPath, parentPath,
_blockSchemaPath,
refPath, refPath,
] = args ] = args
const editKey = getEditTabKey({ const editKey = getEditTabKey({
@@ -271,13 +272,22 @@
justify-content: space-between; justify-content: space-between;
border-bottom: 1px solid var(--ks-border-primary); border-bottom: 1px solid var(--ks-border-primary);
background-image: linear-gradient( background-image: linear-gradient(
to right, to right,
colorPalette.$base-blue-500 0%, colorPalette.$base-blue-400 0%,
colorPalette.$base-blue-700 30%, colorPalette.$base-blue-500 35%,
transparent 50%, rgba(colorPalette.$base-blue-500, 0) 55%,
transparent 100% rgba(colorPalette.$base-blue-500, 0) 100%
); );
background-size: 220% 100%; .dark & {
background-image: linear-gradient(
to right,
colorPalette.$base-blue-500 0%,
colorPalette.$base-blue-700 35%,
rgba(colorPalette.$base-blue-700, .1) 55%,
rgba(colorPalette.$base-blue-700, 0) 100%
);
}
background-size: 250% 100%;
background-position: 100% 0; background-position: 100% 0;
transition: background-position .2s; transition: background-position .2s;
} }
@@ -317,7 +327,7 @@
.playgroundMode { .playgroundMode {
#{--el-color-primary}: colorPalette.$base-blue-500; #{--el-color-primary}: colorPalette.$base-blue-500;
color: colorPalette.$base-white; color: colorPalette.$base-white;
background-position: 0 0; background-position: 10% 0;
} }
.default-theme{ .default-theme{

View File

@@ -1,99 +1,79 @@
<template> <template>
<div class="main"> <div class="main">
<div class="section-1"> <div class="content">
<div class="section-1-main"> <div class="logo-section">
<div class="section-content"> <img :src="logo" alt="Kestra" class="logo" width="150px">
<img :src="logo" alt="Kestra" class="img-fluid" width="150px"> <img :src="logoDark" alt="Kestra" class="logo-dark" width="150px">
<img :src="logoDark" alt="Kestra" class="img-fluid img-fluid-dark" width="150px"> <h5 class="title">
<h5 class="section-1-title mt-4"> {{ $t("no-executions-view.title") }} <span class="highlight">Kestra</span>
{{ $t("no-executions-view.title") }} <span style="color: var(--ks-content-link)">Kestra</span> </h5>
</h5> <p class="description">
<p class="section-1-desc"> {{ $t("no-executions-view.sub_title") }}
{{ $t("no-executions-view.sub_title") }} </p>
</p> <div v-if="flow && !flow.deleted" class="trigger-wrapper">
<div v-if="flow && !flow.deleted" class="mt-2"> <TriggerFlow
<trigger-flow type="primary"
type="primary" :disabled="flow.disabled"
:disabled="flow.disabled" :flow-id="flow.id"
:flow-id="flow.id" :namespace="flow.namespace"
:namespace="flow.namespace" :flow-source="flow.source"
:flow-source="flow.source" />
/>
</div>
</div> </div>
<div class="mid-bar mb-3"> <el-divider />
<div class="title title--center-line" />
</div>
<div class="section-content">
<h6 class="section-1-title mt-2">
{{ $t("no-executions-view.guidance_desc") }}
</h6>
<p class="section-1-desc guidance">
{{ $t("no-executions-view.guidance_sub_desc") }}
</p>
</div>
<OverviewBottom />
</div> </div>
<div class="guidance-section">
<h6 class="guidance-title">
{{ $t("no-executions-view.guidance_desc") }}
</h6>
<p class="description guidance">
{{ $t("no-executions-view.guidance_sub_desc") }}
</p>
</div>
<OverviewBottom />
</div> </div>
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import {mapState} from "vuex"; import {computed} from "vue"
import OverviewBottom from "../onboarding/execution/OverviewBottom.vue"; import {useStore} from "vuex"
import TriggerFlow from "../flows/TriggerFlow.vue"; import OverviewBottom from "../onboarding/execution/OverviewBottom.vue"
import noexecutionimg from "../../assets/onboarding/noexecution.png"; import TriggerFlow from "../flows/TriggerFlow.vue"
import noexecutionimgDark from "../../assets/onboarding/noexecutionDark.png"; import noexecutionimg from "../../assets/onboarding/noexecution.png"
import RouteContext from "../../mixins/routeContext"; import noexecutionimgDark from "../../assets/onboarding/noexecutionDark.png"
import RestoreUrl from "../../mixins/restoreUrl";
import permission from "../../models/permission";
import action from "../../models/action";
export default { interface Props {
name: "ExecuteFlow", topbar?: boolean
mixins: [RouteContext, RestoreUrl], }
components: {
OverviewBottom, withDefaults(defineProps<Props>(), {
TriggerFlow, topbar: true,
}, })
props: {
topbar: { const store = useStore()
type: Boolean,
default: true, const flow = computed(() => store.state.flow.flow)
}, const logo = computed(() => noexecutionimg)
}, const logoDark = computed(() => noexecutionimgDark)
computed: {
...mapState("flow", ["flow"]),
...mapState("auth", ["user"]),
logo() {
return noexecutionimg;
},
logoDark() {
return noexecutionimgDark;
},
canExecute() {
return this.flow ? this.user.isAllowed(permission.EXECUTION, action.CREATE, this.flow.namespace) : false;
},
routeInfo() {
return {
title: this.$t("flows")
}
}
},
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.main { .main {
padding: 3rem 1rem 1rem; margin-top: -1.5rem;
background: radial-gradient(ellipse at top, rgba(102, 51, 255, 0.1) 0, rgba(102, 51, 255, 0) 20%); padding: 3rem 1rem 1rem;
background-color: var(--ks-background-body); background: radial-gradient(ellipse at top, rgba(102, 51, 255, 0.1) 0, rgba(102, 51, 255, 0) 20%);
background-color: var(--ks-background-body);
background-size: 5000px 300px; background-size: 5000px 300px;
background-position: top center; background-position: top center;
background-repeat: no-repeat; background-repeat: no-repeat;
height: 100%; height: 100%;
width: auto; width: auto;
container-type: inline-size; container-type: inline-size;
display: flex;
flex-grow: 1;
justify-content: center;
align-items: center;
@media (min-width: 768px) { @media (min-width: 768px) {
padding: 3rem 2rem 1rem; padding: 3rem 2rem 1rem;
@@ -106,51 +86,53 @@
@media (min-width: 1920px) { @media (min-width: 1920px) {
padding: 3rem 10rem 1rem; padding: 3rem 10rem 1rem;
} }
}
.img-fluid { .content {
max-width: 100%; width: 100%;
height: auto; display: flex;
html.dark & { flex-direction: column;
display: none align-items: center;
}
} h5, h6, p {
margin: 0;
}
.img-fluid-dark { .logo-section {
display: none;
html.dark & {
display: inline-block;
}
}
:deep(.el-button) {
font-weight: 500;
font-size: var(--el-font-size-lg);
padding: 1.25rem 3.2rem;
}
.main .section-1 {
display: flex;
flex-grow: 1;
justify-content: center;
align-items: center;
.section-1-main {
.section-content {
width: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
margin-top: 1rem;
.section-1-title { .logo {
max-width: 100%;
height: auto;
html.dark & {
display: none;
}
}
.logo-dark {
display: none;
html.dark & {
display: inline-block;
}
}
.title {
line-height: var(--el-font-line-height-primary); line-height: var(--el-font-line-height-primary);
text-align: center; text-align: center;
font-weight: 600; font-weight: 600;
color: var(--ks-content-primary); color: var(--ks-content-primary);
margin-top: 2rem !important;
.highlight {
color: var(--ks-content-link);
}
} }
.section-1-desc { .description {
margin-top: -10px;
line-height: var(--el-font-line-height-primary); line-height: var(--el-font-line-height-primary);
font-weight: 300; font-weight: 300;
font-size: var(--el-font-size-extra-small); font-size: var(--el-font-size-extra-small);
@@ -158,34 +140,47 @@
color: var(--ks-content-primary); color: var(--ks-content-primary);
} }
.guidance { .trigger-wrapper {
color: var(--ks-content-link); margin-top: 1.5rem;
} }
} }
.mid-bar { .guidance-section {
margin-top: 20px; display: flex;
flex-direction: column;
align-items: center;
.title { .guidance-title {
font-weight: 500; line-height: var(--el-font-line-height-primary);
color: var(--ks-content-secondary); text-align: center;
display: flex; font-weight: 600;
align-items: center; color: var(--ks-content-primary);
justify-content: center; margin-top: 0.5rem;
font-size: var(--el-font-size-extra-small ); }
&--center-line { .description {
padding: 0; line-height: var(--el-font-line-height-primary);
font-weight: 300;
font-size: var(--el-font-size-extra-small);
text-align: center;
color: var(--ks-content-primary);
&::before { &.guidance {
content: ""; color: var(--ks-content-link);
background-color: var(--ks-border-primary);
height: 1px;
width: 50%;
}
} }
} }
} }
} }
} }
:deep(.el-button) {
font-weight: 500;
font-size: var(--el-font-size-lg);
padding: 1.25rem 3rem;
}
:deep(.el-divider--horizontal) {
width: 90%;
border-color: var(--ks-border-secondary);
}
</style> </style>

View File

@@ -1,11 +1,6 @@
<template> <template>
<div v-if="playgroundStore.enabled && isTask && taskObject?.id" class="flow-playground"> <div v-if="playgroundStore.enabled && isTask && taskObject?.id" class="flow-playground">
<el-button <PlaygroundRunTaskButton :task-id="taskObject?.id" />
class="el-button--playground"
@click="playgroundStore.runUntilTask(taskObject?.id)"
>
{{ t('playground.run_task') }}
</el-button>
</div> </div>
<el-form label-position="top"> <el-form label-position="top">
<el-form-item> <el-form-item>
@@ -52,6 +47,7 @@
import {removeRefPrefix, usePluginsStore} from "../../stores/plugins"; import {removeRefPrefix, usePluginsStore} from "../../stores/plugins";
import {usePlaygroundStore} from "../../stores/playground"; import {usePlaygroundStore} from "../../stores/playground";
import {getValueAtJsonPath} from "../../utils/utils"; import {getValueAtJsonPath} from "../../utils/utils";
import PlaygroundRunTaskButton from "../inputs/PlaygroundRunTaskButton.vue";
const {t} = useI18n(); const {t} = useI18n();

View File

@@ -1,6 +1,6 @@
<template> <template>
<div class="trigger-flow-wrapper"> <div class="trigger-flow-wrapper">
<el-button v-if="playgroundStore.enabled" id="run-all-button" :icon="icon.Play" class="el-button--playground" :disabled="isDisabled()" @click="playgroundStore.runUntilTask()"> <el-button v-if="playgroundStore.enabled" id="run-all-button" :icon="icon.Play" class="el-button--playground" :disabled="isDisabled() || !playgroundStore.readyToStart" @click="playgroundStore.runUntilTask()">
{{ $t("playground.run_all_tasks") }} {{ $t("playground.run_all_tasks") }}
</el-button> </el-button>
<el-button v-else id="execute-button" :class="{'onboarding-glow': coreStore.guidedProperties.tourStarted}" :icon="icon.Flash" :type="type" :disabled="isDisabled()" @click="onClick()"> <el-button v-else id="execute-button" :class="{'onboarding-glow': coreStore.guidedProperties.tourStarted}" :icon="icon.Flash" :type="type" :disabled="isDisabled()" @click="onClick()">
@@ -153,7 +153,8 @@
async loadDefinition() { async loadDefinition() {
await this.executionsStore.loadFlowForExecution({ await this.executionsStore.loadFlowForExecution({
flowId: this.flowId, flowId: this.flowId,
namespace: this.namespace namespace: this.namespace,
store: true
}); });
}, },
reset() { reset() {

View File

@@ -94,11 +94,12 @@
}, },
inheritAttrs: false, inheritAttrs: false,
mixins: [Task], mixins: [Task],
emits: ["update:modelValue"], emits: ["update:modelValue", "update:selectedSchema"],
data() { data() {
return { return {
isOpen: false, isOpen: false,
selectedSchema: undefined, selectedSchema: undefined,
delayedSelectedSchema: undefined,
finishedMounting: false, finishedMounting: false,
}; };
}, },
@@ -150,10 +151,41 @@
} }
this.onAnyOfInput(this.modelValue || {type: val}); this.onAnyOfInput(this.modelValue || {type: val});
}, },
selectedSchema(val) {
this.$emit("update:selectedSchema", val);
this.$nextTick(() => {
this.delayedSelectedSchema = val;
});
},
}, },
methods: { methods: {
onSelectType(value) { onSelectType(value) {
// When switching form string to object/array,
// We try to parse the string as YAML
// If the value is not yaml it has no point on being kept.
if(typeof this.modelValue === "string" && (value === "object" || value === "array")) {
let parsedValue = {}
try{
parsedValue = YAML_UTILS.parse(this.modelValue) ?? {};
if(value === "array" && !Array.isArray(parsedValue)) {
parsedValue = [parsedValue];
}
} catch {
// eat an error
}
this.$emit("update:modelValue", parsedValue);
}
if(value === "string") {
if(Array.isArray(this.modelValue) && this.modelValue.length === 1) {
this.$emit("update:modelValue", this.modelValue[0]);
}else{
this.$emit("update:modelValue", YAML_UTILS.stringify(this.modelValue));
}
}
this.selectedSchema = value; this.selectedSchema = value;
// Set up default values // Set up default values
if ( if (
@@ -172,20 +204,7 @@
} }
this.onInput(defaultValues) this.onInput(defaultValues)
} }
this.delayedSelectedSchema = value;
// When switching form string to object/array,
// We try to parse the string as YAML
// If the value is not yaml it has no point on being kept.
if(typeof this.modelValue === "string" && (value === "object" || value === "array")) {
let parsedValue = {}
try{
parsedValue = YAML_UTILS.parse(this.modelValue) ?? {};
} catch {
// eat an error
}
this.$emit("update:modelValue", parsedValue);
}
}, },
onAnyOfInput(value) { onAnyOfInput(value) {
if(this.constantType?.length && typeof value === "object") { if(this.constantType?.length && typeof value === "object") {
@@ -233,7 +252,7 @@
}) : []; }) : [];
}, },
currentSchema() { currentSchema() {
const rawSchema = this.definitions[this.selectedSchema] ?? this.schemaByType[this.selectedSchema] const rawSchema = this.definitions[this.delayedSelectedSchema] ?? this.schemaByType[this.delayedSelectedSchema]
return consolidateAllOfSchemas(rawSchema, this.definitions); return consolidateAllOfSchemas(rawSchema, this.definitions);
}, },
schemaByType() { schemaByType() {
@@ -243,7 +262,7 @@
}, {}); }, {});
}, },
currentSchemaType() { currentSchemaType() {
return this.selectedSchema ? getTaskComponent(this.currentSchema) : undefined; return this.delayedSelectedSchema ? getTaskComponent(this.currentSchema) : undefined;
}, },
isSelectingPlugins() { isSelectingPlugins() {
return this.schemas.length > 4; return this.schemas.length > 4;

View File

@@ -95,7 +95,7 @@
); );
const handleInput = (value: string, index: number) => { const handleInput = (value: string, index: number) => {
emits("update:modelValue", items.value.toSpliced(index, 1, value)); emits("update:modelValue", [...items.value].splice(index, 1, value));
}; };
const newEmptyValue = computed(() => { const newEmptyValue = computed(() => {
@@ -114,7 +114,7 @@
emits("update:modelValue", undefined); emits("update:modelValue", undefined);
return; return;
} }
emits("update:modelValue", items.value.toSpliced(index, 1)); emits("update:modelValue", [...items.value].splice(index, 1));
}; };
const moveItem = (index: number, direction: "up" | "down") => { const moveItem = (index: number, direction: "up" | "down") => {

View File

@@ -21,7 +21,7 @@
</span> </span>
<ClearButton <ClearButton
v-if="isAnyOf && !isRequired && modelValue && Object.keys(modelValue).length > 0" v-if="isAnyOf && !isRequired && hasSelectedASchema"
@click="$emit('update:modelValue', undefined); taskComponent?.resetSelectType?.();" @click="$emit('update:modelValue', undefined); taskComponent?.resetSelectType?.();"
/> />
</div> </div>
@@ -64,11 +64,11 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {computed, ref} from "vue";
import {templateRef} from "@vueuse/core";
import Help from "vue-material-design-icons/Information.vue"; import Help from "vue-material-design-icons/Information.vue";
import Markdown from "../../layout/Markdown.vue"; import Markdown from "../../layout/Markdown.vue";
import TaskLabelWithBoolean from "./TaskLabelWithBoolean.vue"; import TaskLabelWithBoolean from "./TaskLabelWithBoolean.vue";
import {computed} from "vue";
import {templateRef} from "@vueuse/core";
import ClearButton from "./ClearButton.vue"; import ClearButton from "./ClearButton.vue";
import getTaskComponent from "./getTaskComponent"; import getTaskComponent from "./getTaskComponent";
@@ -93,12 +93,17 @@
return !props.disabled && props.required?.includes(props.fieldKey);// && props.schema.$required; return !props.disabled && props.required?.includes(props.fieldKey);// && props.schema.$required;
}) })
const hasSelectedASchema = ref(false)
const componentProps = computed(() => { const componentProps = computed(() => {
return { return {
modelValue: props.modelValue, modelValue: props.modelValue,
"onUpdate:modelValue": (value: Record<string, any> | string | number | boolean | Array<any>) => { "onUpdate:modelValue": (value: Record<string, any> | string | number | boolean | Array<any>) => {
emit("update:modelValue", value); emit("update:modelValue", value);
}, },
"onUpdate:selectedSchema": (value: any) => {
hasSelectedASchema.value = value !== undefined;
},
task: props.task, task: props.task,
root: props.root ? `${props.root}.${props.fieldKey}` : props.fieldKey, root: props.root ? `${props.root}.${props.fieldKey}` : props.fieldKey,
schema: props.schema, schema: props.schema,

View File

@@ -92,7 +92,6 @@
import {TabFocus} from "monaco-editor/esm/vs/editor/browser/config/tabFocus.js"; import {TabFocus} from "monaco-editor/esm/vs/editor/browser/config/tabFocus.js";
import MonacoEditor from "./MonacoEditor.vue"; import MonacoEditor from "./MonacoEditor.vue";
import type * as monaco from "monaco-editor/esm/vs/editor/editor.api"; import type * as monaco from "monaco-editor/esm/vs/editor/editor.api";
import {nextTick} from "process";
const {t} = useI18n() const {t} = useI18n()
@@ -568,11 +567,11 @@
const showWidgetContent = ref(false) const showWidgetContent = ref(false)
function addContentWidget(widget: { async function addContentWidget(widget: {
id: string; id: string;
position: monaco.IPosition; position: monaco.IPosition;
height: number height: number
marginLeft: number right: string
}) { }) {
if(!isCodeEditor(editor)) return if(!isCodeEditor(editor)) return
if(!monacoEditor.value) return if(!monacoEditor.value) return
@@ -591,16 +590,32 @@
}, },
getDomNode: () => { getDomNode: () => {
const content = widgetNode.querySelector(".editor-content-widget-content") as HTMLDivElement; const content = widgetNode.querySelector(".editor-content-widget-content") as HTMLDivElement;
widgetNode.style.marginLeft = widget.marginLeft / 2.2 + "rem";
if(content){ if(content){
content.style.height = (widget.height * 18) + "px"; content.style.height = widget.height + "rem";
} }
return widgetNode return widgetNode;
}, },
afterRender() {
const boundingClientRect = monacoEditor.value!.$el.querySelector(".ks-monaco-editor .monaco-scrollable-element").getBoundingClientRect();
// Since we must position the widget on the right side but our anchor is from the left, we add the width of the editor minus the right offset (150px is a rough estimate of the widget's width)
widgetNode.style.left = `calc(${boundingClientRect.width}px - 150px - ${widget.right})`;
}
}); });
nextTick(() => {
showWidgetContent.value = true; await waitForWidgetContentNode()
})
showWidgetContent.value = true
}
async function wait(time: number){
return new Promise(resolve => setTimeout(resolve, time));
}
async function waitForWidgetContentNode() {
await wait(30);
if (document.querySelector(".editor-content-widget-content") === null) {
return waitForWidgetContentNode();
}
} }
function removeContentWidget(id: string) { function removeContentWidget(id: string) {
@@ -637,9 +652,10 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
height: 100%;
width: 100%; .el-button-group {
padding: 0 4rem; display: inline-flex;
}
} }
:not(.namespace-defaults, .el-drawer__body) > .ks-editor { :not(.namespace-defaults, .el-drawer__body) > .ks-editor {

View File

@@ -30,12 +30,7 @@
<ContentSave v-if="!isCurrentTabFlow" @click="saveFileContent" /> <ContentSave v-if="!isCurrentTabFlow" @click="saveFileContent" />
</template> </template>
<template v-if="playgroundStore.enabled" #widget-content> <template v-if="playgroundStore.enabled" #widget-content>
<el-button <PlaygroundRunTaskButton :task-id="highlightedLines?.taskId" />
class="el-button--playground"
@click="playgroundStore.runUntilTask(highlightedLines?.taskId)"
>
{{ t('playground.run_task') }}
</el-button>
</template> </template>
</editor> </editor>
<transition name="el-zoom-in-center"> <transition name="el-zoom-in-center">
@@ -58,15 +53,12 @@
<script lang="ts" setup> <script lang="ts" setup>
import {computed, onActivated, onMounted, ref, provide, onBeforeUnmount} from "vue"; import {computed, onActivated, onMounted, ref, provide, onBeforeUnmount} from "vue";
import {useStore} from "vuex"; import {useStore} from "vuex";
import {useI18n} from "vue-i18n";
import Editor from "./Editor.vue"; import Editor from "./Editor.vue";
import ContentSave from "vue-material-design-icons/ContentSave.vue"; import ContentSave from "vue-material-design-icons/ContentSave.vue";
import {useRoute, useRouter} from "vue-router"; import {useRoute, useRouter} from "vue-router";
const {t} = useI18n();
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
@@ -79,6 +71,7 @@
import AcceptDecline from "./AcceptDecline.vue"; import AcceptDecline from "./AcceptDecline.vue";
import * as YAML_UTILS from "@kestra-io/ui-libs/flow-yaml-utils"; import * as YAML_UTILS from "@kestra-io/ui-libs/flow-yaml-utils";
import useFlowEditorRunTaskButton from "../../composables/playground/useFlowEditorRunTaskButton"; import useFlowEditorRunTaskButton from "../../composables/playground/useFlowEditorRunTaskButton";
import PlaygroundRunTaskButton from "./PlaygroundRunTaskButton.vue";
const store = useStore(); const store = useStore();
const miscStore = useMiscStore(); const miscStore = useMiscStore();
@@ -147,6 +140,7 @@
onBeforeUnmount(() => { onBeforeUnmount(() => {
window.removeEventListener("keydown", handleGlobalSave); window.removeEventListener("keydown", handleGlobalSave);
window.removeEventListener("keydown", toggleAiShortcut); window.removeEventListener("keydown", toggleAiShortcut);
pluginsStore.editorPlugin = undefined;
}); });
const editorRefElement = ref<InstanceType<typeof Editor>>(); const editorRefElement = ref<InstanceType<typeof Editor>>();
@@ -288,4 +282,4 @@
.actions { .actions {
bottom: 10%; bottom: 10%;
} }
</style> </style>

View File

@@ -1,5 +1,5 @@
<template> <template>
<el-switch v-model="playgroundStore.enabled" :active-text="t('playground.toggle')" class="toggle" /> <el-switch v-model="playgroundStore.enabled" :active-text="t('playground.toggle')" class="toggle" :class="{'is-active': playgroundStore.enabled}" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -14,5 +14,8 @@
<style lang="scss" scoped> <style lang="scss" scoped>
.toggle{ .toggle{
margin-right: 1rem; margin-right: 1rem;
&.is-active ::v-deep(.el-switch__label){
color: white;
}
} }
</style> </style>

View File

@@ -16,6 +16,7 @@
:execution="executionsStore.execution" :execution="executionsStore.execution"
:subflows-executions="executionsStore.subflowsExecutions" :subflows-executions="executionsStore.subflowsExecutions"
:playground-enabled="playgroundStore.enabled" :playground-enabled="playgroundStore.enabled"
:playground-ready-to-start="playgroundStore.readyToStart"
@toggle-orientation="toggleOrientation" @toggle-orientation="toggleOrientation"
@edit="onEditTask" @edit="onEditTask"
@delete="onDelete" @delete="onDelete"

View File

@@ -14,19 +14,32 @@
@expand-subflow="onExpandSubflow" @expand-subflow="onExpandSubflow"
@swapped-task="onSwappedTask" @swapped-task="onSwappedTask"
/> />
<div v-else-if="invalidGraph">
<el-alert
:title="t('topology-graph.invalid')"
type="error"
class="invalid-graph"
:closable="false"
>
{{ t('topology-graph.invalid_description') }}
</el-alert>
</div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import {computed, ref} from "vue"; import {computed, ref} from "vue";
import {useI18n} from "vue-i18n";
import {useStore} from "vuex"; import {useStore} from "vuex";
import {Utils} from "@kestra-io/ui-libs"; import {Utils} from "@kestra-io/ui-libs";
import LowCodeEditor from "./LowCodeEditor.vue"; import LowCodeEditor from "./LowCodeEditor.vue";
const store = useStore(); const store = useStore();
const {t} = useI18n();
const flowYaml = computed(() => store.state.flow.flowYaml); const flowYaml = computed(() => store.state.flow.flowYaml);
const flowGraph = computed(() => store.state.flow.flowGraph); const flowGraph = computed(() => store.state.flow.flowGraph);
const invalidGraph = computed(() => store.state.flow.invalidGraph);
const flowId = computed(() => store.state.flow.id); const flowId = computed(() => store.state.flow.id);
const namespace = computed(() => store.state.flow.namespace); const namespace = computed(() => store.state.flow.namespace);
const expandedSubflows = computed<string[]>(() => store.state.flow.expandedSubflows); const expandedSubflows = computed<string[]>(() => store.state.flow.expandedSubflows);
@@ -88,4 +101,8 @@
:deep(.vue-flow__panel.bottom) { :deep(.vue-flow__panel.bottom) {
bottom: 2rem !important; bottom: 2rem !important;
} }
.invalid-graph {
margin: 1rem;
width: auto;
}
</style> </style>

View File

@@ -429,7 +429,7 @@
codeEditor.removeContentWidget(datePickerWidget); codeEditor.removeContentWidget(datePickerWidget);
} }
watch(suggestWidget, (newVal) => { watch(suggestWidget, async (newVal) => {
const asCodeEditor = editorResolved.value?.getEditorType() === EditorType.ICodeEditor ? editorResolved.value as editor.ICodeEditor : undefined; const asCodeEditor = editorResolved.value?.getEditorType() === EditorType.ICodeEditor ? editorResolved.value as editor.ICodeEditor : undefined;
if (newVal !== undefined) { if (newVal !== undefined) {
@@ -481,7 +481,7 @@
}; };
} }
asCodeEditor.addContentWidget(datePickerWidget); await asCodeEditor.addContentWidget(datePickerWidget);
datePicker.value!.handleOpen(); datePicker.value!.handleOpen();
setTimeout(() => { setTimeout(() => {
datePicker.value!.focus(); datePicker.value!.focus();
@@ -662,11 +662,11 @@
showClasses: false, showClasses: false,
showWords: false showWords: false
}, },
...(isInFlowEditor && { ...(isInFlowEditor ? {
padding: { padding: {
top: 28 top: 16
} }
}), } : {}),
...props.options ...props.options
}; };

View File

@@ -0,0 +1,42 @@
<template>
<el-dropdown
split-button
@visible-change="playgroundStore.dropdownOpened = $event"
:button-props="{class: 'el-button--playground'}"
@click="playgroundStore.runUntilTask(taskId)"
:disabled="!playgroundStore.readyToStart"
>
<el-icon><Play /></el-icon>
<span>{{ t('playground.run_task') }}</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item :icon="Play" @click="playgroundStore.runUntilTask(taskId)">
{{ t('playground.run_this_task') }}
</el-dropdown-item>
<el-dropdown-item :icon="PlayBoxMultiple" @click="playgroundStore.runUntilTask(taskId, true)">
{{ t('playground.run_task_and_downstream') }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<script setup lang="ts">
import {useI18n} from "vue-i18n";
import {usePlaygroundStore} from "../../stores/playground";
import Play from "vue-material-design-icons/Play.vue";
import PlayBoxMultiple from "vue-material-design-icons/PlayBoxMultiple.vue";
const {t} = useI18n();
const playgroundStore = usePlaygroundStore();
defineProps<{
taskId?: string;
}>();
</script>
<style lang="scss" scoped>
.toggle{
margin-right: 1rem;
}
</style>

View File

@@ -100,8 +100,8 @@
<namespace-select <namespace-select
v-model="kv.namespace" v-model="kv.namespace"
:readonly="kv.update" :readonly="kv.update"
data-type="flow"
:include-system-namespace="true" :include-system-namespace="true"
all
/> />
</el-form-item> </el-form-item>

View File

@@ -105,6 +105,7 @@
"level", "level",
"index", "index",
"attemptNumber", "attemptNumber",
"executionKind"
]; ];
excludes.push.apply(excludes, this.excludeMetas); excludes.push.apply(excludes, this.excludeMetas);
for (const key in this.log) { for (const key in this.log) {

View File

@@ -123,6 +123,7 @@
import {apiUrl} from "override/utils/route"; import {apiUrl} from "override/utils/route";
import Utils from "../../utils/utils"; import Utils from "../../utils/utils";
import LogUtils from "../../utils/logs.js"; import LogUtils from "../../utils/logs.js";
import throttle from "lodash/throttle";
export default { export default {
name: "TaskRunDetails", name: "TaskRunDetails",
@@ -208,7 +209,9 @@
selectedLogLevel: undefined, selectedLogLevel: undefined,
childrenLogIndicesByLevelByChildUid: {}, childrenLogIndicesByLevelByChildUid: {},
logsScrollerRefs: {}, logsScrollerRefs: {},
subflowTaskRunDetailsRefs: {} subflowTaskRunDetailsRefs: {},
throttledExecutionUpdate: undefined,
targetExecution: undefined
}; };
}, },
watch: { watch: {
@@ -236,14 +239,6 @@
}, },
immediate: true immediate: true
}, },
"followedExecution.id": {
handler: function (executionId, oldExecutionId) {
if (executionId && executionId !== oldExecutionId) {
this.followExecution(executionId);
}
},
immediate: true
},
followedExecution: { followedExecution: {
handler: async function (newExecution, oldExecution) { handler: async function (newExecution, oldExecution) {
if (!newExecution) { if (!newExecution) {
@@ -265,15 +260,15 @@
{ {
namespace: newExecution.namespace, namespace: newExecution.namespace,
flowId: newExecution.flowId, flowId: newExecution.flowId,
revision: newExecution.flowRevision revision: newExecution.flowRevision,
store: false
} }
); );
} }
if (![State.RUNNING, State.PAUSED].includes(this.followedExecution.state.current)) { if (!State.isRunning(this.followedExecution.state.current)) {
// wait a bit to make sure we don't miss logs as log indexer is asynchronous // wait a bit to make sure we don't miss logs as log indexer is asynchronous
setTimeout(() => { setTimeout(() => {
this.closeExecutionSSE()
this.closeLogsSSE() this.closeLogsSSE()
}, 2000); }, 2000);
@@ -301,13 +296,21 @@
} }
}, },
mounted() { mounted() {
this.throttledExecutionUpdate = throttle((executionEvent) => {
this.targetExecution = JSON.parse(executionEvent.data);
}, 500);
if (this.targetExecutionId) {
this.followExecution(this.targetExecutionId);
}
this.autoExpandBasedOnSettings(); this.autoExpandBasedOnSettings();
}, },
computed: { computed: {
...mapState("auth", ["user"]), ...mapState("auth", ["user"]),
...mapStores(useCoreStore, useExecutionsStore), ...mapStores(useCoreStore, useExecutionsStore),
followedExecution() { followedExecution() {
return this.executionsStore.execution; return this.targetExecutionId === undefined ? this.executionsStore.execution : this.targetExecution;
}, },
Download() { Download() {
return Download return Download
@@ -346,7 +349,7 @@
return _groupBy(indexedLogs, indexedLog => this.attemptUid(indexedLog.taskRunId, indexedLog.attemptNumber)); return _groupBy(indexedLogs, indexedLog => this.attemptUid(indexedLog.taskRunId, indexedLog.attemptNumber));
}, },
autoExpandTaskrunStates() { autoExpandTaskRunStates() {
switch (localStorage.getItem("logDisplay") || logDisplayTypes.DEFAULT) { switch (localStorage.getItem("logDisplay") || logDisplayTypes.DEFAULT) {
case logDisplayTypes.ERROR: case logDisplayTypes.ERROR:
return [State.FAILED, State.RUNNING, State.PAUSED] return [State.FAILED, State.RUNNING, State.PAUSED]
@@ -411,9 +414,6 @@
}); });
this.logFileSizeByPath[path] = Utils.humanFileSize(axiosResponse.data.size); this.logFileSizeByPath[path] = Utils.humanFileSize(axiosResponse.data.size);
}, },
closeExecutionSSE() {
this.executionsStore.closeSSE();
},
closeLogsSSE() { closeLogsSSE() {
if (this.logsSSE) { if (this.logsSSE) {
this.logsSSE.close(); this.logsSSE.close();
@@ -428,7 +428,7 @@
} }
}, },
autoExpandBasedOnSettings() { autoExpandBasedOnSettings() {
if (this.autoExpandTaskrunStates.length === 0) { if (this.autoExpandTaskRunStates.length === 0) {
return; return;
} }
@@ -441,7 +441,7 @@
return; return;
} }
if (this.taskRunId === taskRun.id || this.autoExpandTaskrunStates.includes(taskRun.state.current)) { if (this.taskRunId === taskRun.id || this.autoExpandTaskRunStates.includes(taskRun.state.current)) {
this.showAttempt(this.attemptUid(taskRun.id, this.selectedAttemptNumberByTaskRunId[taskRun.id])); this.showAttempt(this.attemptUid(taskRun.id, this.selectedAttemptNumberByTaskRunId[taskRun.id]));
} }
}); });
@@ -458,10 +458,30 @@
this.logsWithIndexByAttemptUid[this.attemptUid(taskRun.id, this.selectedAttemptNumberByTaskRunId[taskRun.id])])) && this.logsWithIndexByAttemptUid[this.attemptUid(taskRun.id, this.selectedAttemptNumberByTaskRunId[taskRun.id])])) &&
this.showLogs this.showLogs
}, },
closeTargetExecutionSSE() {
if (this.executionSSE) {
this.executionSSE.close();
this.executionSSE = undefined;
}
},
followExecution(executionId) { followExecution(executionId) {
this.closeExecutionSSE(); this.closeTargetExecutionSSE();
this.executionsStore this.executionsStore
.followExecution({id: executionId}, this.$t) .followExecution({id: executionId, rawSSE: true})
.then(sse => {
this.executionSSE = sse;
this.executionSSE.onmessage = executionEvent => {
const isEnd = executionEvent && executionEvent.lastEventId === "end";
// we are receiving a first "fake" event to force initializing the connection: ignoring it
if (executionEvent.lastEventId !== "start") {
this.throttledExecutionUpdate(executionEvent);
}
if (isEnd) {
this.closeTargetExecutionSSE();
this.throttledExecutionUpdate.flush();
}
}
});
}, },
followLogs(executionId) { followLogs(executionId) {
this.executionsStore this.executionsStore
@@ -550,7 +570,7 @@
return `${taskRunId}-${attemptNumber}` return `${taskRunId}-${attemptNumber}`
}, },
scrollToBottomFailedTask() { scrollToBottomFailedTask() {
if (this.autoExpandTaskrunStates.includes(this.followedExecution.state.current)) { if (this.autoExpandTaskRunStates.includes(this.followedExecution?.state?.current)) {
this.currentTaskRuns.forEach((taskRun) => { this.currentTaskRuns.forEach((taskRun) => {
if (taskRun.state.current === State.FAILED || taskRun.state.current === State.RUNNING) { if (taskRun.state.current === State.FAILED || taskRun.state.current === State.RUNNING) {
const attemptNumber = taskRun.attempts ? taskRun.attempts.length - 1 : (this.forcedAttemptNumber ?? 0) const attemptNumber = taskRun.attempts ? taskRun.attempts.length - 1 : (this.forcedAttemptNumber ?? 0)
@@ -634,7 +654,6 @@
} }
}, },
beforeUnmount() { beforeUnmount() {
this.closeExecutionSSE();
this.closeLogsSSE() this.closeLogsSSE()
}, },
}; };
@@ -707,4 +726,4 @@
} }
} }
} }
</style> </style>

View File

@@ -2,11 +2,13 @@
<el-select <el-select
class="fit-text" class="fit-text"
:model-value="value" :model-value="value"
@update:model-value="onInput" @update:model-value="$emit('update:modelValue', $event)"
:disabled="readonly" :disabled="readonly"
clearable clearable
:placeholder="$t('Select namespace')" :placeholder="$t('Select namespace')"
:persistent="false" :persistent="false"
remote
:remote-method="onInput"
filterable filterable
:allow-create="allowCreate" :allow-create="allowCreate"
default-first-option default-first-option
@@ -25,14 +27,12 @@
import {mapStores} from "pinia"; import {mapStores} from "pinia";
import {useMiscStore} from "../../../stores/misc"; import {useMiscStore} from "../../../stores/misc";
import _uniqBy from "lodash/uniqBy"; import _uniqBy from "lodash/uniqBy";
import permission from "../../../models/permission";
import action from "../../../models/action";
export default { export default {
props: { props: {
dataType: { dataType: {
type: String, type: String,
required: true, default: undefined,
}, },
value: { value: {
type: String, type: String,
@@ -60,19 +60,7 @@
} }
}, },
emits: ["update:modelValue"], emits: ["update:modelValue"],
created() {
if (
this.user &&
this.user.hasAnyActionOnAnyNamespace(
permission.NAMESPACE,
action.READ,
)
) {
this.load();
}
},
computed: { computed: {
...mapState("namespace", ["datatypeNamespaces"]),
...mapState("auth", ["user"]), ...mapState("auth", ["user"]),
...mapStores(useMiscStore), ...mapStores(useMiscStore),
}, },
@@ -85,8 +73,7 @@
methods: { methods: {
onInput(value) { onInput(value) {
this.$emit("update:modelValue", value); this.$emit("update:modelValue", value);
this.localNamespaceInput = value; this.load(value);
this.load();
}, },
groupNamespaces(namespaces) { groupNamespaces(namespaces) {
let res = []; let res = [];
@@ -119,37 +106,29 @@
(ns) => namespaces.includes(ns.code) || this.isFilter, (ns) => namespaces.includes(ns.code) || this.isFilter,
); );
}, },
load() { async load(value) {
this.$store try {
.dispatch("namespace/loadNamespacesForDatatype", { let namespaces;
dataType: this.dataType if (this.all) {
}) namespaces = await this.$store.dispatch("namespace/autocomplete", {
.then(() => { q: value || "",
this.groupedNamespaces = this.groupNamespaces( ids: [],
this.datatypeNamespaces apiUrl: undefined
).filter( });
(namespace) => } else {
this.includeSystemNamespace || namespaces = await this.$store.dispatch("namespace/loadNamespacesForDatatype", {
namespace.code !== dataType: this.dataType
(this.miscStore.configs?.systemNamespace || "system") });
); }
});
if (this.all) { this.groupedNamespaces = this.groupNamespaces(namespaces)
// Then include datatype namespaces + all from namespaces tables .filter(namespace =>
this.$store.dispatch("namespace/autocomplete" + (this.value ? "?q=" + this.value : "")).then(namespaces => { this.includeSystemNamespace ||
const concatNamespaces = this.groupedNamespaces.concat(this.groupNamespaces( namespace.code !== (this.miscStore.configs?.systemNamespace || "system")
namespaces )
).filter( .sort((a, b) => a.code.localeCompare(b.code));
(namespace) => } catch (error) {
this.includeSystemNamespace || console.error("Error loading namespaces:", error);
namespace.code !==
(this.miscStore.configs?.systemNamespace || "system")
));
// Remove duplicates after merge
this.groupedNamespaces = _uniqBy(concatNamespaces, "code").filter(
(ns) => namespaces.includes(ns.code) || this.isFilter,
).sort((a,b) => a.code > b.code)
})
} }
} }
}, },

View File

@@ -71,6 +71,7 @@
v-else v-else
filterable filterable
:add-secret-modal-visible="addSecretModalVisible" :add-secret-modal-visible="addSecretModalVisible"
:namespace="props.namespace"
@update:add-secret-modal-visible="addSecretModalVisible = $event" @update:add-secret-modal-visible="addSecretModalVisible = $event"
/> />
</section> </section>
@@ -90,6 +91,13 @@
const miscStore = useMiscStore(); const miscStore = useMiscStore();
const props = defineProps({
namespace: {
type: String,
default: undefined
}
});
const addSecretModalVisible = ref(false); const addSecretModalVisible = ref(false);
const hasData = ref(undefined); const hasData = ref(undefined);

View File

@@ -91,9 +91,8 @@
<namespace-select <namespace-select
v-model="secret.namespace" v-model="secret.namespace"
:readonly="secret.update" :readonly="secret.update"
data-type="flow"
:include-system-namespace="true" :include-system-namespace="true"
:all="true" all
/> />
</el-form-item> </el-form-item>
<el-form-item :label="$t('secret.key')" prop="key"> <el-form-item :label="$t('secret.key')" prop="key">
@@ -469,4 +468,4 @@
} }
}, },
}; };
</script> </script>

View File

@@ -67,7 +67,7 @@ export default function useFlowEditorRunTaskButton(isCurrentTabFlow: Ref<boolean
} }
function addButtonToHoveredTask(taskCode?: {taskId: string, start: number, end: number, longestLineLength:number, firstLineLength: number}) { function addButtonToHoveredTask(taskCode?: {taskId: string, start: number, end: number, longestLineLength:number, firstLineLength: number}) {
if(!taskCode) { if(!taskCode || playgroundStore.dropdownOpened) {
return return
} }
@@ -79,16 +79,20 @@ export default function useFlowEditorRunTaskButton(isCurrentTabFlow: Ref<boolean
id: `task-hovered-${taskCode.taskId}`, id: `task-hovered-${taskCode.taskId}`,
position: { position: {
lineNumber: taskCode.start, lineNumber: taskCode.start,
column: taskCode.longestLineLength + 1 column: 0
}, },
height: (taskCode.end - taskCode.start) + 1, height: Math.max(taskCode.end - taskCode.start + 1, 1),
marginLeft: (taskCode.longestLineLength - taskCode.firstLineLength), right: "1rem",
}); });
} }
const highlightedTaskId = ref<string | undefined>(undefined); const highlightedTaskId = ref<string | undefined>(undefined);
watch(hoveredTaskProperties, (res) => { watch(hoveredTaskProperties, (res) => {
if (playgroundStore.dropdownOpened) {
return;
}
if(!res || !playgroundStore.enabled || !isCurrentTabFlow.value) { if(!res || !playgroundStore.enabled || !isCurrentTabFlow.value) {
highlightedLines.value = undefined; highlightedLines.value = undefined;
editorRefElement.value?.clearHighlights(); editorRefElement.value?.clearHighlights();
@@ -127,4 +131,4 @@ export default function useFlowEditorRunTaskButton(isCurrentTabFlow: Ref<boolean
playgroundStore, playgroundStore,
highlightedLines, highlightedLines,
} }
} }

View File

@@ -64,6 +64,7 @@ export class FlowAutoCompletion extends YamlAutoCompletion {
"randomInt(lower=${1:0}, upper=${2:10})", "randomInt(lower=${1:0}, upper=${2:10})",
"randomPort()", "randomPort()",
"tasksWithState(state=${1:'FAILED'})", "tasksWithState(state=${1:'FAILED'})",
"http(uri=${1:'https://example.com'}, method=${2:'GET'})",
]); ]);
} }

View File

@@ -323,20 +323,24 @@ export const useExecutionsStore = defineStore("executions", () => {
execution.value = _execution; execution.value = _execution;
}, 500); }, 500);
const followExecution = (options: { id: string }, translate: (itn: string) => string) => { const followExecution = (options: { id: string, rawSSE?: boolean }, translate: (itn: string) => string) => {
closeSSE(); if (!options.rawSSE) {
execution.value = undefined;
closeSSE();
}
const serverSentEventSource = new EventSource(`${apiUrl(store)}/executions/${options.id}/follow`, {withCredentials: true}); const serverSentEventSource = new EventSource(`${apiUrl(store)}/executions/${options.id}/follow`, {withCredentials: true});
if (options.rawSSE) {
return Promise.resolve(serverSentEventSource);
}
sse.value = serverSentEventSource; sse.value = serverSentEventSource;
serverSentEventSource.onmessage = (executionEvent) => { serverSentEventSource.onmessage = (executionEvent) => {
const isEnd = executionEvent && executionEvent.lastEventId === "end"; const isEnd = executionEvent && executionEvent.lastEventId === "end";
if (isEnd) {
closeSSE();
}
// we are receiving a first "fake" event to force initializing the connection: ignoring it // we are receiving a first "fake" event to force initializing the connection: ignoring it
if (executionEvent.lastEventId !== "start") { if (executionEvent.lastEventId !== "start") {
throttledExecutionUpdate(executionEvent); throttledExecutionUpdate(executionEvent);
} }
if (isEnd) { if (isEnd) {
closeSSE();
throttledExecutionUpdate.flush(); throttledExecutionUpdate.flush();
} }
} }
@@ -494,11 +498,13 @@ export const useExecutionsStore = defineStore("executions", () => {
) )
} }
const loadFlowForExecution = (options: { namespace: string; flowId: string; revision?: number }) => { const loadFlowForExecution = (options: { namespace: string; flowId: string; revision?: number, store: boolean }) => {
const revision = options.revision ? `?revision=${options.revision}` : ""; const revision = options.revision ? `?revision=${options.revision}` : "";
return store.$http.get(`${apiUrl(store)}/executions/flows/${options.namespace}/${options.flowId}${revision}`) return store.$http.get(`${apiUrl(store)}/executions/flows/${options.namespace}/${options.flowId}${revision}`)
.then(response => { .then(response => {
flow.value = response.data; if (options.store) {
flow.value = response.data;
}
return response.data; return response.data;
}); });
} }
@@ -738,4 +744,4 @@ export const useExecutionsStore = defineStore("executions", () => {
appendFollowedLogs, appendFollowedLogs,
getFlowExecutions, getFlowExecutions,
}; };
}); });

View File

@@ -24,6 +24,7 @@ export default {
total: 0, total: 0,
overallTotal: undefined, overallTotal: undefined,
flowGraph: undefined, flowGraph: undefined,
invalidGraph: false,
revisions: undefined, revisions: undefined,
flowValidation: undefined, flowValidation: undefined,
taskError: undefined, taskError: undefined,
@@ -324,18 +325,24 @@ export default {
coreStore.message = { coreStore.message = {
title: "Invalid source code", title: "Invalid source code",
message: response.data.exception, message: response.data.exception,
variant: "danger" variant: "error"
}; };
// add this error to the list of errors
commit("setFlowValidation", {
constraints: response.data.exception
});
delete response.data.exception; delete response.data.exception;
} }
if(options.store === false) { if(options.store === false) {
return response.data; return response.data;
} }
commit("setFlow", response.data); commit("setFlow", response.data);
commit("setFlowYaml", response.data.source); commit("setFlowYaml", response.data.source);
commit("setFlowYamlOrigin", response.data.source); commit("setFlowYamlOrigin", response.data.source);
commit("setFlowYamlBeforeAdd", response.data.source); commit("setFlowYamlBeforeAdd", response.data.source);
commit("setOverallTotal", 1) commit("setOverallTotal", 1)
return response.data; return response.data;
}) })
}, },
@@ -464,9 +471,12 @@ export default {
params["revision"] = flow.revision; params["revision"] = flow.revision;
} }
return this.$http.get(`${apiUrl(this)}/flows/${flow.namespace}/${flow.id}/graph`, {params}).then(response => { return this.$http.get(`${apiUrl(this)}/flows/${flow.namespace}/${flow.id}/graph`, {params}).then(response => {
commit("setInvalidGraph", false)
commit("setFlowGraph", response.data) commit("setFlowGraph", response.data)
return response.data; return response.data;
}) }).catch(() => {
commit("setInvalidGraph", true)
});
}, },
loadGraphFromSource({commit, state}, options) { loadGraphFromSource({commit, state}, options) {
const config = options.config ? {...options.config, ...textYamlHeader} : textYamlHeader; const config = options.config ? {...options.config, ...textYamlHeader} : textYamlHeader;
@@ -611,6 +621,9 @@ export default {
}, },
}, },
mutations: { mutations: {
setInvalidGraph(state, value) {
state.invalidGraph = value;
},
setFlows(state, flows) { setFlows(state, flows) {
state.flows = flows state.flows = flows
}, },

View File

@@ -24,6 +24,11 @@ export default {
addKvModalVisible: false addKvModalVisible: false
}, },
actions: { actions: {
async autocomplete({dispatch}, options) {
return (await dispatch("search", {
q: options.q
})).results.map(({id}) => id);
},
search({commit}, options) { search({commit}, options) {
const shouldCommit = options.commit !== false; const shouldCommit = options.commit !== false;
delete options.commit; delete options.commit;

View File

@@ -5,6 +5,10 @@ import {useUrlSearchParams} from "@vueuse/core"
import * as VueFlowUtils from "@kestra-io/ui-libs/vue-flow-utils" import * as VueFlowUtils from "@kestra-io/ui-libs/vue-flow-utils"
import {Execution, useExecutionsStore} from "./executions"; import {Execution, useExecutionsStore} from "./executions";
import Inputs from "../utils/inputs"; import Inputs from "../utils/inputs";
import {useRoute, useRouter} from "vue-router";
import {State} from "@kestra-io/ui-libs";
import {useToast} from "../utils/toast";
import {useI18n} from "vue-i18n";
interface ExecutionWithGraph extends Execution { interface ExecutionWithGraph extends Execution {
graph?: VueFlowUtils.FlowGraph; graph?: VueFlowUtils.FlowGraph;
@@ -24,6 +28,27 @@ export const usePlaygroundStore = defineStore("playground", () => {
} }
}) })
const route = useRoute();
const router = useRouter();
function navigateToEdit(runUntilTaskId?: string, runDownstreamTasks?: boolean) {
const flowParsed = store.state.flow.flow;
router.push({
name: "flows/update",
params: {
id: flowParsed.id,
namespace: flowParsed.namespace,
tab: "edit",
tenant: route.params.tenant,
},
query: {
playground: "on",
runUntilTaskId,
runDownstreamTasks: runDownstreamTasks ? "true" : undefined,
}
});
}
const executions = ref<ExecutionWithGraph[]>([]) const executions = ref<ExecutionWithGraph[]>([])
function addExecution(execution: ExecutionWithGraph, graph: VueFlowUtils.FlowGraph) { function addExecution(execution: ExecutionWithGraph, graph: VueFlowUtils.FlowGraph) {
execution.graph = graph execution.graph = graph
@@ -32,24 +57,27 @@ export const usePlaygroundStore = defineStore("playground", () => {
function clearExecutions() { function clearExecutions() {
executions.value = []; executions.value = [];
executionsStore.execution = undefined;
} }
const store = useStore(); const store = useStore();
const executionsStore = useExecutionsStore(); const executionsStore = useExecutionsStore();
const taskIdToTaskRunIdMap: Record<string, string> = {}; const taskIdToTaskRunIdMap: Map<string, string> = new Map();
async function replayOrTriggerExecution(taskId?: string, nextTasksIds?: string[], graph?: any) { async function replayOrTriggerExecution(taskId?: string, breakpoints?: string[], graph?: any) {
// if all tasks prior to current task in the graph are identical // if all tasks prior to current task in the graph are identical
// to the previous execution's revision, // to the previous execution's revision,
// we can skip them and start the execution at the current task using replayExecution() // we can skip them and start the execution at the current task using replayExecution()
if (taskId && executions.value.length && graph if (taskId && executions.value.length && graph
&& executions.value[0].graph && executions.value[0].graph
&& VueFlowUtils.areTasksIdenticalInGraphUntilTask(executions.value[0].graph, graph, taskId)) { && VueFlowUtils.areTasksIdenticalInGraphUntilTask(executions.value[0].graph, graph, taskId)
&& taskIdToTaskRunIdMap.has(taskId)) {
return await executionsStore.replayExecution({ return await executionsStore.replayExecution({
executionId: executions.value[0].id, executionId: executions.value[0].id,
taskRunId: taskIdToTaskRunIdMap[taskId], taskRunId: taskIdToTaskRunIdMap.get(taskId),
breakpoints: nextTasksIds, revision: store.state.flow.flow.revision,
breakpoints,
}); });
} }
@@ -64,7 +92,7 @@ export const usePlaygroundStore = defineStore("playground", () => {
namespace: store.state.flow.flow?.namespace, namespace: store.state.flow.flow?.namespace,
formData: defaultInputValues, formData: defaultInputValues,
kind: "PLAYGROUND", kind: "PLAYGROUND",
breakpoints: nextTasksIds, breakpoints,
}) })
} }
@@ -85,14 +113,86 @@ export const usePlaygroundStore = defineStore("playground", () => {
return {nextTasksIds, graph}; return {nextTasksIds, graph};
} }
async function runUntilTask(taskId?: string) { const latestExecution = computed(() => executions.value[0]);
await store.dispatch("flow/saveAll")
const nonFinalStates = [
State.KILLING,
State.RUNNING,
State.RESTARTED,
State.CREATED,
]
const executionState = computed(() => {
return latestExecution.value?.state.current;
})
const readyToStartPure = computed(() => {
return !latestExecution.value || !nonFinalStates.includes(executionState.value)
})
const readyToStart = ref(readyToStartPure.value);
watch(readyToStartPure, (newValue) => {
if(newValue) {
setTimeout(() => {
readyToStart.value = newValue;
}, 1000);
} else {
readyToStart.value = newValue
}
});
const toast = useToast();
function runFromQuery(){
if(route.query.runUntilTaskId) {
const {runUntilTaskId, runDownstreamTasks} = route.query;
runUntilTask(runUntilTaskId.toString(), Boolean(runDownstreamTasks));
// remove the query parameters to avoid running the same task again
router.replace({
name: route.name,
params: route.params,
query: {
...route.query,
runUntilTaskId: undefined,
runDownstreamTasks: undefined, // remove the query parameter
}
});
}
}
const {t} = useI18n();
async function runUntilTask(taskId?: string, runDownstreamTasks = false) {
if(readyToStart.value === false) {
console.warn("Playground is not ready to start, latest execution is still in progress");
return
}
readyToStart.value = false;
if(store.state.flow.isCreating){
toast.confirm(
t("playground.confirm_create"),
async () => {
await store.dispatch("flow/saveAll");
navigateToEdit(taskId, runDownstreamTasks);
}
);
return;
}
await store.dispatch("flow/saveAll")
// get the next task id to break on. If current task is provided to breakpoint, // get the next task id to break on. If current task is provided to breakpoint,
// the task specified by the user will not be executed. // the task specified by the user will not be executed.
const {nextTasksIds, graph} = await getNextTaskIds(taskId) ?? {}; const {nextTasksIds, graph} = await getNextTaskIds(runDownstreamTasks ? undefined : taskId) ?? {};
const {data: execution} = await replayOrTriggerExecution(taskId, runDownstreamTasks ? undefined : nextTasksIds, graph);
// don't keep taskRunIds from previous executions
// because of https://github.com/kestra-io/kestra/issues/10462
taskIdToTaskRunIdMap.clear();
const {data: execution} = await replayOrTriggerExecution(taskId, nextTasksIds, graph);
executionsStore.execution = execution; executionsStore.execution = execution;
addExecution(execution, graph); addExecution(execution, graph);
@@ -103,7 +203,7 @@ export const usePlaygroundStore = defineStore("playground", () => {
if(execution.taskRunList){ if(execution.taskRunList){
for(const taskRun of execution.taskRunList) { for(const taskRun of execution.taskRunList) {
// map taskId to taskRunId for later use in replayExecution() // map taskId to taskRunId for later use in replayExecution()
taskIdToTaskRunIdMap[taskRun.taskId] = taskRun.id; taskIdToTaskRunIdMap.set(taskRun.taskId, taskRun.id);
} }
} }
if (index !== -1) { if (index !== -1) {
@@ -120,11 +220,17 @@ export const usePlaygroundStore = defineStore("playground", () => {
} }
}) })
const dropdownOpened = ref<boolean>(false);
return { return {
enabled, enabled,
dropdownOpened,
readyToStart,
executions, executions,
latestExecution: computed(() => executions.value[0]), latestExecution,
clearExecutions, clearExecutions,
runUntilTask runUntilTask,
runFromQuery,
executionState
} }
}) })

View File

@@ -268,7 +268,7 @@ export const usePluginsStore = defineStore("plugins", {
}, },
async updateDocumentation(pluginElement: ({type: string, version?: string} & Record<string, any>) | undefined) { async updateDocumentation(pluginElement?: ({type: string, version?: string} & Record<string, any>) | undefined) {
if (!pluginElement?.type || !this.allTypes.includes(pluginElement.type)) { if (!pluginElement?.type || !this.allTypes.includes(pluginElement.type)) {
this.editorPlugin = undefined; this.editorPlugin = undefined;
this.currentlyLoading = undefined; this.currentlyLoading = undefined;

View File

@@ -85,13 +85,6 @@ main {
transition: padding 0.3s ease; transition: padding 0.3s ease;
padding: 0 24px; padding: 0 24px;
&.gantt-container {
&:has(> .execution-pending) {
margin: 0;
padding: 0;
}
}
&.full-height { &.full-height {
flex: 1; flex: 1;
display: flex; display: flex;

View File

@@ -28,11 +28,14 @@
} }
&.el-button--playground { &.el-button--playground {
#{--el-button-disabled-text-color}: $base-blue-50;
#{--el-button-text-color}: $base-white; #{--el-button-text-color}: $base-white;
#{--el-button-hover-text-color}: $base-white; #{--el-button-hover-text-color}: $base-white;
#{--el-button-bg-color}: $base-blue-500; #{--el-button-bg-color}: $base-blue-500;
#{--el-button-hover-bg-color}: $base-blue-400; #{--el-button-hover-bg-color}: $base-blue-400;
#{--el-button-active-bg-color}: $base-blue-600; #{--el-button-active-bg-color}: $base-blue-600;
#{--el-button-active-border-color}: $base-blue-700;
#{--el-button-outline-color}: $base-blue-700;
} }
&.el-button--success { &.el-button--success {

View File

@@ -1016,10 +1016,14 @@
"pause done": "Die Ausführung ist PAUSED.", "pause done": "Die Ausführung ist PAUSED.",
"pause title": "Ausführung <code>{id}</code> pausieren.<br/>Bitte beachten Sie, dass derzeit laufende Tasks weiterhin verarbeitet werden und die Ausführung manuell fortgesetzt werden muss.", "pause title": "Ausführung <code>{id}</code> pausieren.<br/>Bitte beachten Sie, dass derzeit laufende Tasks weiterhin verarbeitet werden und die Ausführung manuell fortgesetzt werden muss.",
"playground": { "playground": {
"empty": "Klicken Sie auf die Schaltfläche \"Run task\", um Ihren Workflow mit einer simulierten Ausführung zu testen.", "confirm_create": "Sie können den Playground nicht ausführen, während Sie einen flow erstellen. Das Starten eines Playground-Laufs wird den flow erstellen.",
"history": "Letzte 10 Ausführungen", "history": "Letzte 10 Ausführungen",
"play_icon_info": "Sie können auch das Play-Symbol in den No-Code- oder Topologie-Ansichten anklicken.",
"run_all_tasks": "Alle Tasks ausführen", "run_all_tasks": "Alle Tasks ausführen",
"run_task": "Task ausführen", "run_task": "Task ausführen",
"run_task_and_downstream": "Task & Downstream ausführen",
"run_task_info": "Bewegen Sie den Mauszeiger über eine beliebige Task im Flow-Code-Editor und klicken Sie auf die Schaltfläche \"Run task\", um Ihre Task zu testen.",
"run_this_task": "Führe diese Task aus",
"title": "Spielwiese", "title": "Spielwiese",
"toggle": "Spielplatz" "toggle": "Spielplatz"
}, },
@@ -1317,10 +1321,15 @@
"system_namespace_description": "Automatisieren Sie Wartungsaufgaben, von Fehlerwarnungen bis hin zu automatisierten Bereinigungen.", "system_namespace_description": "Automatisieren Sie Wartungsaufgaben, von Fehlerwarnungen bis hin zu automatisierten Bereinigungen.",
"tags": "Tags", "tags": "Tags",
"task": "Task", "task": "Task",
"task failed": "Task fehlgeschlagen",
"task id": "Task-ID", "task id": "Task-ID",
"task id already exists": "Task-Id existiert bereits", "task id already exists": "Task-Id existiert bereits",
"task is running": "Task läuft",
"task logs": "Task Logs", "task logs": "Task Logs",
"task run id": "TaskRun-ID", "task run id": "TaskRun-ID",
"task sent a warning": "Task hat eine Warnung gesendet",
"task was skipped": "Task wurde übersprungen",
"task was successful": "Task war erfolgreich",
"taskDefaults": "Task-Standards", "taskDefaults": "Task-Standards",
"taskRunners": "Task Runners", "taskRunners": "Task Runners",
"task_id_exists": "Task-ID existiert bereits", "task_id_exists": "Task-ID existiert bereits",
@@ -1356,6 +1365,8 @@
"topology": "Topologie", "topology": "Topologie",
"topology-graph": { "topology-graph": {
"graph-orientation": "Graph-Ausrichtung", "graph-orientation": "Graph-Ausrichtung",
"invalid": "Graph-Fehler",
"invalid_description": "Beim Laden des Graphen ist ein Fehler aufgetreten. Bitte überprüfen Sie den Quellcode auf Fehler.",
"zoom-fit": "Anpassen", "zoom-fit": "Anpassen",
"zoom-in": "Vergrößern", "zoom-in": "Vergrößern",
"zoom-out": "Verkleinern", "zoom-out": "Verkleinern",

View File

@@ -275,7 +275,9 @@
"zoom-in": "Zoom in", "zoom-in": "Zoom in",
"zoom-out": "Zoom out", "zoom-out": "Zoom out",
"zoom-reset": "Reset zoom", "zoom-reset": "Reset zoom",
"zoom-fit": "Fit" "zoom-fit": "Fit",
"invalid": "Graph error",
"invalid_description": "An error occurred while loading the graph. Please check the source code for errors."
}, },
"show task logs": "Show task logs", "show task logs": "Show task logs",
"show task condition": "Show task condition", "show task condition": "Show task condition",
@@ -1455,11 +1457,15 @@
}, },
"playground": { "playground": {
"title": "Playground", "title": "Playground",
"empty": "Click the \"Run task\" button to test your workflow with a simulated execution.", "run_task_info": "Hover over any task in the Flow Code editor and click the \"Run task\" button to test your task.",
"play_icon_info": "You can also hit the Play icon in the No-Code or Topology views.",
"toggle": "Playground", "toggle": "Playground",
"run_task": "Run Task", "run_task": "Run task",
"run_this_task": "Run this task",
"run_task_and_downstream": "Run task & downstream",
"run_all_tasks": "Run All Tasks", "run_all_tasks": "Run All Tasks",
"history": "Last 10 runs" "history": "Last 10 runs",
"confirm_create": "You cannot run the playground while creating a flow. Launching a playground run will create the flow."
}, },
"submit": "Submit", "submit": "Submit",
"to toggle": "to toggle", "to toggle": "to toggle",
@@ -1467,6 +1473,11 @@
"reject": "Reject", "reject": "Reject",
"last modified": "Last modified", "last modified": "Last modified",
"run task in playground": "Run task in playground", "run task in playground": "Run task in playground",
"task is running": "Task is running",
"task was successful": "Task was successful",
"task failed": "Task failed",
"task was skipped": "Task was skipped",
"task sent a warning": "Task sent a warning",
"defaultsToNamespaceFile": "Defaults to namespace file: <code>{name}</code>", "defaultsToNamespaceFile": "Defaults to namespace file: <code>{name}</code>",
"no_file_choosen": "No file chosen" "no_file_choosen": "No file chosen"
} }

View File

@@ -1016,10 +1016,14 @@
"pause done": "La ejecución está PAUSED", "pause done": "La ejecución está PAUSED",
"pause title": "Pausar la ejecución <code>{id}</code>.<br/>Tenga en cuenta que las tasks que se están ejecutando actualmente seguirán siendo procesadas, y la ejecución tendrá que reanudarse manualmente.", "pause title": "Pausar la ejecución <code>{id}</code>.<br/>Tenga en cuenta que las tasks que se están ejecutando actualmente seguirán siendo procesadas, y la ejecución tendrá que reanudarse manualmente.",
"playground": { "playground": {
"empty": "Haz clic en el botón \"Run task\" para probar tu flujo de trabajo con una ejecución simulada.", "confirm_create": "No puedes ejecutar el playground mientras creas un flow. Iniciar una ejecución de playground creará el flow.",
"history": "Últimas 10 ejecuciones", "history": "Últimas 10 ejecuciones",
"play_icon_info": "También puedes presionar el ícono de Play en las vistas No-Code o Topology.",
"run_all_tasks": "Ejecutar Todas las Tasks", "run_all_tasks": "Ejecutar Todas las Tasks",
"run_task": "Ejecutar Task", "run_task": "Ejecutar Task",
"run_task_and_downstream": "Ejecutar task y downstream",
"run_task_info": "Pasa el cursor sobre cualquier task en el editor de Flow Code y haz clic en el botón \"Run task\" para probar tu task.",
"run_this_task": "Ejecutar esta task",
"title": "Área de Pruebas", "title": "Área de Pruebas",
"toggle": "Área de pruebas" "toggle": "Área de pruebas"
}, },
@@ -1317,10 +1321,15 @@
"system_namespace_description": "Automatiza tareas de mantenimiento, desde alertas de fallo hasta limpiezas automatizadas.", "system_namespace_description": "Automatiza tareas de mantenimiento, desde alertas de fallo hasta limpiezas automatizadas.",
"tags": "Etiquetas", "tags": "Etiquetas",
"task": "Tarea", "task": "Tarea",
"task failed": "Tarea FAILED",
"task id": "Task ID", "task id": "Task ID",
"task id already exists": "Task Id ya existe", "task id already exists": "Task Id ya existe",
"task is running": "La tarea está RUNNING",
"task logs": "Task logs", "task logs": "Task logs",
"task run id": "ID de TaskRun", "task run id": "ID de TaskRun",
"task sent a warning": "La task envió un WARNING",
"task was skipped": "La task fue omitida",
"task was successful": "La tarea fue exitosa",
"taskDefaults": "Predeterminados de la task", "taskDefaults": "Predeterminados de la task",
"taskRunners": "Ejecutores de Task", "taskRunners": "Ejecutores de Task",
"task_id_exists": "El Id de Task ya existe", "task_id_exists": "El Id de Task ya existe",
@@ -1356,6 +1365,8 @@
"topology": "Topología", "topology": "Topología",
"topology-graph": { "topology-graph": {
"graph-orientation": "Orientación del gráfico", "graph-orientation": "Orientación del gráfico",
"invalid": "Error de gráfico",
"invalid_description": "Ocurrió un error al cargar el gráfico. Por favor, verifica el código fuente en busca de errores.",
"zoom-fit": "Ajustar", "zoom-fit": "Ajustar",
"zoom-in": "Acercar", "zoom-in": "Acercar",
"zoom-out": "Alejar", "zoom-out": "Alejar",

View File

@@ -1016,10 +1016,14 @@
"pause done": "L'exécution est en PAUSE", "pause done": "L'exécution est en PAUSE",
"pause title": "Mettre en pause l'exécution <code>{id}</code>.<br/>Notez que les tasks actuellement en cours d'exécution seront toujours traitées, et l'exécution devra être reprise manuellement.", "pause title": "Mettre en pause l'exécution <code>{id}</code>.<br/>Notez que les tasks actuellement en cours d'exécution seront toujours traitées, et l'exécution devra être reprise manuellement.",
"playground": { "playground": {
"empty": "Cliquez sur le bouton \"Run task\" pour tester votre workflow avec une exécution simulée.", "confirm_create": "Vous ne pouvez pas exécuter le playground lors de la création d'un flow. Lancer une exécution de playground créera le flow.",
"history": "Dernières 10 exécutions", "history": "Dernières 10 exécutions",
"play_icon_info": "Vous pouvez également cliquer sur l'icône Play dans les vues No-Code ou Topology.",
"run_all_tasks": "Exécuter toutes les tasks", "run_all_tasks": "Exécuter toutes les tasks",
"run_task": "Exécuter la Task", "run_task": "Exécuter la Task",
"run_task_and_downstream": "Exécuter la task et en aval",
"run_task_info": "Survolez n'importe quelle task dans l'éditeur de Flow Code et cliquez sur le bouton \"Run task\" pour tester votre task.",
"run_this_task": "Exécuter cette task",
"title": "Aire de jeu", "title": "Aire de jeu",
"toggle": "Terrain de jeu" "toggle": "Terrain de jeu"
}, },
@@ -1317,10 +1321,15 @@
"system_namespace_description": "Automatisez les tâches de maintenance, des alertes de défaillance aux nettoyages automatisés.", "system_namespace_description": "Automatisez les tâches de maintenance, des alertes de défaillance aux nettoyages automatisés.",
"tags": "Tags", "tags": "Tags",
"task": "Tâche", "task": "Tâche",
"task failed": "Échec de la task",
"task id": "ID de tâche", "task id": "ID de tâche",
"task id already exists": "Identifiant de tâche déjà utilisé", "task id already exists": "Identifiant de tâche déjà utilisé",
"task is running": "La task est en cours d'exécution",
"task logs": "Journaux de la tâche", "task logs": "Journaux de la tâche",
"task run id": "ID d'exécution de tâche", "task run id": "ID d'exécution de tâche",
"task sent a warning": "La tâche a envoyé un WARNING",
"task was skipped": "La tâche a été ignorée",
"task was successful": "La tâche a été un succès",
"taskDefaults": "Valeur de tâches par défaut", "taskDefaults": "Valeur de tâches par défaut",
"taskRunners": "Exécuteurs de Task", "taskRunners": "Exécuteurs de Task",
"task_id_exists": "L'ID de la tâche existe déjà", "task_id_exists": "L'ID de la tâche existe déjà",
@@ -1356,6 +1365,8 @@
"topology": "Topologie", "topology": "Topologie",
"topology-graph": { "topology-graph": {
"graph-orientation": "Orientation du graph", "graph-orientation": "Orientation du graph",
"invalid": "Erreur de graphe",
"invalid_description": "Une erreur s'est produite lors du chargement du graphique. Veuillez vérifier le code source pour les erreurs.",
"zoom-fit": "Voir tout", "zoom-fit": "Voir tout",
"zoom-in": "Zoomer", "zoom-in": "Zoomer",
"zoom-out": "Dézoomer", "zoom-out": "Dézoomer",

View File

@@ -1016,10 +1016,14 @@
"pause done": "निष्पादन PAUSED है", "pause done": "निष्पादन PAUSED है",
"pause title": "प्रक्रिया को PAUSED करें <code>{id}</code>।<br/>ध्यान दें कि वर्तमान में चल रहे tasks अभी भी प्रक्रिया में होंगे, और प्रक्रिया को मैन्युअल रूप से पुनः शुरू करना होगा।", "pause title": "प्रक्रिया को PAUSED करें <code>{id}</code>।<br/>ध्यान दें कि वर्तमान में चल रहे tasks अभी भी प्रक्रिया में होंगे, और प्रक्रिया को मैन्युअल रूप से पुनः शुरू करना होगा।",
"playground": { "playground": {
"empty": "\"Run task\" बटन पर क्लिक करें ताकि आप अपने workflow को एक simulated execution के साथ परीक्षण कर सकें।", "confirm_create": "आप एक flow बनाते समय playground नहीं चला सकते। एक playground रन शुरू करने से flow बन जाएगा।",
"history": "पिछले 10 रन", "history": "पिछले 10 रन",
"play_icon_info": "आप No-Code या Topology दृश्य में Play आइकन पर भी क्लिक कर सकते हैं।",
"run_all_tasks": "सभी Tasks चलाएं", "run_all_tasks": "सभी Tasks चलाएं",
"run_task": "टास्क चलाएं", "run_task": "टास्क चलाएं",
"run_task_and_downstream": "टास्क चलाएं और डाउनस्ट्रीम",
"run_task_info": "Flow Code संपादक में किसी भी task पर होवर करें और अपने task का परीक्षण करने के लिए \"Run task\" बटन पर क्लिक करें।",
"run_this_task": "इस task को चलाएं",
"title": "प्लेग्राउंड", "title": "प्लेग्राउंड",
"toggle": "प्लेग्राउंड" "toggle": "प्लेग्राउंड"
}, },
@@ -1317,10 +1321,15 @@
"system_namespace_description": "रखरखाव के tasks को स्वचालित करें, failure alerts से लेकर automated cleanups तक।", "system_namespace_description": "रखरखाव के tasks को स्वचालित करें, failure alerts से लेकर automated cleanups तक।",
"tags": "टैग्स", "tags": "टैग्स",
"task": "Task", "task": "Task",
"task failed": "टास्क FAILED",
"task id": "Task ID", "task id": "Task ID",
"task id already exists": "Task Id पहले से मौजूद है", "task id already exists": "Task Id पहले से मौजूद है",
"task is running": "टास्क RUNNING है",
"task logs": "Task Logs", "task logs": "Task Logs",
"task run id": "टास्करन ID", "task run id": "टास्करन ID",
"task sent a warning": "टास्क ने एक WARNING भेजा",
"task was skipped": "टास्क को छोड़ दिया गया था",
"task was successful": "टास्क सफल रहा",
"taskDefaults": "taskDefaults", "taskDefaults": "taskDefaults",
"taskRunners": "टास्क Runners", "taskRunners": "टास्क Runners",
"task_id_exists": "टास्क Id पहले से मौजूद है", "task_id_exists": "टास्क Id पहले से मौजूद है",
@@ -1356,6 +1365,8 @@
"topology": "टोपोलॉजी", "topology": "टोपोलॉजी",
"topology-graph": { "topology-graph": {
"graph-orientation": "ग्राफ़ अभिविन्यास", "graph-orientation": "ग्राफ़ अभिविन्यास",
"invalid": "ग्राफ़ त्रुटि",
"invalid_description": "ग्राफ़ लोड करते समय एक त्रुटि हुई। कृपया त्रुटियों के लिए स्रोत कोड की जाँच करें।",
"zoom-fit": "फिट", "zoom-fit": "फिट",
"zoom-in": "ज़ूम इन करें", "zoom-in": "ज़ूम इन करें",
"zoom-out": "ज़ूम आउट करें", "zoom-out": "ज़ूम आउट करें",

View File

@@ -1016,10 +1016,14 @@
"pause done": "L'esecuzione è PAUSED", "pause done": "L'esecuzione è PAUSED",
"pause title": "Metti in pausa l'esecuzione <code>{id}</code>.<br/>Nota che i task attualmente in esecuzione verranno comunque elaborati e l'esecuzione dovrà essere ripresa manualmente.", "pause title": "Metti in pausa l'esecuzione <code>{id}</code>.<br/>Nota che i task attualmente in esecuzione verranno comunque elaborati e l'esecuzione dovrà essere ripresa manualmente.",
"playground": { "playground": {
"empty": "Fai clic sul pulsante \"Run task\" per testare il tuo workflow con un'esecuzione simulata.", "confirm_create": "Non puoi eseguire il playground mentre crei un flow. Avviare un'esecuzione del playground creerà il flow.",
"history": "Ultime 10 esecuzioni", "history": "Ultime 10 esecuzioni",
"play_icon_info": "Puoi anche fare clic sull'icona Play nelle viste No-Code o Topology.",
"run_all_tasks": "Esegui Tutti i Task", "run_all_tasks": "Esegui Tutti i Task",
"run_task": "Esegui Task", "run_task": "Esegui Task",
"run_task_and_downstream": "Esegui task e downstream",
"run_task_info": "Passa il mouse su qualsiasi task nell'editor del Flow Code e fai clic sul pulsante \"Run task\" per testare il tuo task.",
"run_this_task": "Esegui questo task",
"title": "Playground", "title": "Playground",
"toggle": "Playground" "toggle": "Playground"
}, },
@@ -1317,10 +1321,15 @@
"system_namespace_description": "Automatizza le attività di manutenzione, dagli avvisi di errore alle pulizie automatiche.", "system_namespace_description": "Automatizza le attività di manutenzione, dagli avvisi di errore alle pulizie automatiche.",
"tags": "Tag", "tags": "Tag",
"task": "Task", "task": "Task",
"task failed": "Attività FAILED",
"task id": "Task ID", "task id": "Task ID",
"task id already exists": "Task Id già esistente", "task id already exists": "Task Id già esistente",
"task is running": "Il task è in esecuzione",
"task logs": "Task logs", "task logs": "Task logs",
"task run id": "ID di TaskRun", "task run id": "ID di TaskRun",
"task sent a warning": "Task ha inviato un WARNING",
"task was skipped": "Il task è stato saltato",
"task was successful": "Il task è stato completato con SUCCESSO",
"taskDefaults": "Task Defaults", "taskDefaults": "Task Defaults",
"taskRunners": "Runner di Task", "taskRunners": "Runner di Task",
"task_id_exists": "L'ID del task esiste già", "task_id_exists": "L'ID del task esiste già",
@@ -1356,6 +1365,8 @@
"topology": "Topologia", "topology": "Topologia",
"topology-graph": { "topology-graph": {
"graph-orientation": "Orientamento del grafico", "graph-orientation": "Orientamento del grafico",
"invalid": "Errore del grafico",
"invalid_description": "Si è verificato un errore durante il caricamento del grafico. Si prega di controllare il codice sorgente per errori.",
"zoom-fit": "Adatta", "zoom-fit": "Adatta",
"zoom-in": "Zoom avanti", "zoom-in": "Zoom avanti",
"zoom-out": "Zoom indietro", "zoom-out": "Zoom indietro",

View File

@@ -1016,10 +1016,14 @@
"pause done": "実行がPAUSEDされています", "pause done": "実行がPAUSEDされています",
"pause title": "実行を一時停止 <code>{id}</code>。<br/>現在実行中のタスクは引き続き処理されることに注意してください。実行は手動で再開する必要があります。", "pause title": "実行を一時停止 <code>{id}</code>。<br/>現在実行中のタスクは引き続き処理されることに注意してください。実行は手動で再開する必要があります。",
"playground": { "playground": {
"empty": "「Run task」ボタンをクリックして、シミュレーション実行でワークフローをテストします。", "confirm_create": "フローを作成中はプレイグラウンドを実行できません。プレイグラウンドの実行を開始すると、flowが作成されます。",
"history": "直近10回の実行", "history": "直近10回の実行",
"play_icon_info": "ノーコードまたはトポロジービューで再生アイコンをクリックすることもできます。",
"run_all_tasks": "すべてのタスクを実行", "run_all_tasks": "すべてのタスクを実行",
"run_task": "タスクを実行", "run_task": "タスクを実行",
"run_task_and_downstream": "タスクとダウンストリームを実行",
"run_task_info": "Flow Codeエディタ内の任意のtaskにカーソルを合わせ、「Run task」ボタンをクリックしてtaskをテストします。",
"run_this_task": "このtaskを実行",
"title": "プレイグラウンド", "title": "プレイグラウンド",
"toggle": "プレイグラウンド" "toggle": "プレイグラウンド"
}, },
@@ -1317,10 +1321,15 @@
"system_namespace_description": "メンテナンスタスクを自動化し、FAILUREアラートから自動クリーンアップまで対応します。", "system_namespace_description": "メンテナンスタスクを自動化し、FAILUREアラートから自動クリーンアップまで対応します。",
"tags": "タグ", "tags": "タグ",
"task": "Task", "task": "Task",
"task failed": "タスクが失敗しました",
"task id": "Task ID", "task id": "Task ID",
"task id already exists": "Task Idはすでに存在します", "task id already exists": "Task Idはすでに存在します",
"task is running": "タスクがRUNNING中",
"task logs": "Task Logs", "task logs": "Task Logs",
"task run id": "タスクランID", "task run id": "タスクランID",
"task sent a warning": "タスクがWARNINGを送信しました",
"task was skipped": "タスクがスキップされました",
"task was successful": "タスクは成功しました",
"taskDefaults": "taskのデフォルト設定", "taskDefaults": "taskのデフォルト設定",
"taskRunners": "タスクランナー", "taskRunners": "タスクランナー",
"task_id_exists": "タスクIDは既に存在します", "task_id_exists": "タスクIDは既に存在します",
@@ -1356,6 +1365,8 @@
"topology": "トポロジー", "topology": "トポロジー",
"topology-graph": { "topology-graph": {
"graph-orientation": "グラフの向き", "graph-orientation": "グラフの向き",
"invalid": "グラフエラー",
"invalid_description": "グラフの読み込み中にエラーが発生しました。ソースコードにエラーがないか確認してください。",
"zoom-fit": "フィット", "zoom-fit": "フィット",
"zoom-in": "ズームイン", "zoom-in": "ズームイン",
"zoom-out": "ズームアウト", "zoom-out": "ズームアウト",

View File

@@ -1016,10 +1016,14 @@
"pause done": "실행이 PAUSED 상태입니다.", "pause done": "실행이 PAUSED 상태입니다.",
"pause title": "실행 <code>{id}</code>을(를) 일시 중지합니다.<br/>현재 실행 중인 task는 계속 처리되며, 실행은 수동으로 다시 시작해야 합니다.", "pause title": "실행 <code>{id}</code>을(를) 일시 중지합니다.<br/>현재 실행 중인 task는 계속 처리되며, 실행은 수동으로 다시 시작해야 합니다.",
"playground": { "playground": {
"empty": "\"Run task\" 버튼을 클릭하여 시뮬레이션된 실행으로 워크플로우를 테스트하세요.", "confirm_create": "flow를 생성하는 동안에는 playground를 실행할 수 없습니다. playground 실행을 시작하면 flow가 생성됩니다.",
"history": "최근 10회 실행", "history": "최근 10회 실행",
"play_icon_info": "또한 No-Code 또는 Topology 보기에서 Play 아이콘을 클릭할 수 있습니다.",
"run_all_tasks": "모든 Task 실행", "run_all_tasks": "모든 Task 실행",
"run_task": "작업 실행", "run_task": "작업 실행",
"run_task_and_downstream": "Task 및 다운스트림 실행",
"run_task_info": "Flow Code 편집기에서 어떤 task 위에 마우스를 올리고 \"Run task\" 버튼을 클릭하여 task를 테스트하세요.",
"run_this_task": "이 task 실행",
"title": "플레이그라운드", "title": "플레이그라운드",
"toggle": "플레이그라운드" "toggle": "플레이그라운드"
}, },
@@ -1317,10 +1321,15 @@
"system_namespace_description": "유지 관리 작업을 자동화하세요, 실패 경고부터 자동 정리까지.", "system_namespace_description": "유지 관리 작업을 자동화하세요, 실패 경고부터 자동 정리까지.",
"tags": "태그들", "tags": "태그들",
"task": "Task", "task": "Task",
"task failed": "작업 실패",
"task id": "Task ID", "task id": "Task ID",
"task id already exists": "Task Id가 이미 존재합니다", "task id already exists": "Task Id가 이미 존재합니다",
"task is running": "Task가 RUNNING 중입니다.",
"task logs": "Task Logs", "task logs": "Task Logs",
"task run id": "TaskRun ID", "task run id": "TaskRun ID",
"task sent a warning": "Task에서 경고를 보냈습니다.",
"task was skipped": "Task가 건너뛰어졌습니다",
"task was successful": "Task가 성공했습니다",
"taskDefaults": "Task 기본값", "taskDefaults": "Task 기본값",
"taskRunners": "작업 실행기", "taskRunners": "작업 실행기",
"task_id_exists": "Task ID가 이미 존재합니다.", "task_id_exists": "Task ID가 이미 존재합니다.",
@@ -1356,6 +1365,8 @@
"topology": "토폴로지", "topology": "토폴로지",
"topology-graph": { "topology-graph": {
"graph-orientation": "그래프 방향", "graph-orientation": "그래프 방향",
"invalid": "그래프 오류",
"invalid_description": "그래프를 로드하는 중 오류가 발생했습니다. 소스 코드에서 오류를 확인하세요.",
"zoom-fit": "맞춤", "zoom-fit": "맞춤",
"zoom-in": "확대", "zoom-in": "확대",
"zoom-out": "축소", "zoom-out": "축소",

View File

@@ -1016,10 +1016,14 @@
"pause done": "Wykonanie jest PAUSED", "pause done": "Wykonanie jest PAUSED",
"pause title": "Wstrzymaj wykonanie <code>{id}</code>.<br/>Zauważ, że aktualnie uruchomione taski nadal będą przetwarzane, a wykonanie będzie musiało zostać wznowione ręcznie.", "pause title": "Wstrzymaj wykonanie <code>{id}</code>.<br/>Zauważ, że aktualnie uruchomione taski nadal będą przetwarzane, a wykonanie będzie musiało zostać wznowione ręcznie.",
"playground": { "playground": {
"empty": "Kliknij przycisk \"Run task\", aby przetestować swój workflow za pomocą symulowanej execution.", "confirm_create": "Nie można uruchomić playground podczas tworzenia flow. Uruchomienie playground run utworzy flow.",
"history": "Ostatnie 10 uruchomień", "history": "Ostatnie 10 uruchomień",
"play_icon_info": "Możesz również kliknąć ikonę Play w widokach No-Code lub Topology.",
"run_all_tasks": "Uruchom Wszystkie Taski", "run_all_tasks": "Uruchom Wszystkie Taski",
"run_task": "Uruchom Task", "run_task": "Uruchom Task",
"run_task_and_downstream": "Uruchom task i downstream",
"run_task_info": "Najedź kursorem na dowolne zadanie w edytorze Flow Code i kliknij przycisk \"Run task\", aby przetestować swoje zadanie.",
"run_this_task": "Uruchom ten task",
"title": "Plac zabaw", "title": "Plac zabaw",
"toggle": "Plac zabaw" "toggle": "Plac zabaw"
}, },
@@ -1317,10 +1321,15 @@
"system_namespace_description": "Automatyzuj zadania konserwacyjne, od alertów o awariach po automatyczne czyszczenie.", "system_namespace_description": "Automatyzuj zadania konserwacyjne, od alertów o awariach po automatyczne czyszczenie.",
"tags": "Tagi", "tags": "Tagi",
"task": "Task", "task": "Task",
"task failed": "Task failed",
"task id": "Task ID", "task id": "Task ID",
"task id already exists": "Task Id już istnieje", "task id already exists": "Task Id już istnieje",
"task is running": "Task jest w trakcie RUNNING",
"task logs": "Task logs", "task logs": "Task logs",
"task run id": "ID TaskRun", "task run id": "ID TaskRun",
"task sent a warning": "Task wysłał ostrzeżenie",
"task was skipped": "Pominięto task",
"task was successful": "Zadanie zakończyło się sukcesem",
"taskDefaults": "Domyślne ustawienia task", "taskDefaults": "Domyślne ustawienia task",
"taskRunners": "Task Runners", "taskRunners": "Task Runners",
"task_id_exists": "Id zadania już istnieje", "task_id_exists": "Id zadania już istnieje",
@@ -1356,6 +1365,8 @@
"topology": "Topologia", "topology": "Topologia",
"topology-graph": { "topology-graph": {
"graph-orientation": "Orientacja grafu", "graph-orientation": "Orientacja grafu",
"invalid": "Błąd grafu",
"invalid_description": "Wystąpił błąd podczas ładowania grafu. Proszę sprawdzić kod źródłowy pod kątem błędów.",
"zoom-fit": "Dopasuj", "zoom-fit": "Dopasuj",
"zoom-in": "Powiększ", "zoom-in": "Powiększ",
"zoom-out": "Pomniejsz", "zoom-out": "Pomniejsz",

View File

@@ -1016,10 +1016,14 @@
"pause done": "A execução está PAUSED", "pause done": "A execução está PAUSED",
"pause title": "Pausar execução <code>{id}</code>.<br/>Note que as tasks atualmente em execução ainda serão processadas, e a execução terá que ser retomada manualmente.", "pause title": "Pausar execução <code>{id}</code>.<br/>Note que as tasks atualmente em execução ainda serão processadas, e a execução terá que ser retomada manualmente.",
"playground": { "playground": {
"empty": "Clique no botão \"Run task\" para testar seu fluxo de trabalho com uma execução simulada.", "confirm_create": "Você não pode executar o playground enquanto cria um flow. Iniciar uma execução do playground criará o flow.",
"history": "Últimas 10 execuções", "history": "Últimas 10 execuções",
"play_icon_info": "Você também pode clicar no ícone de Play nas visualizações No-Code ou Topology.",
"run_all_tasks": "Executar Todas as Tasks", "run_all_tasks": "Executar Todas as Tasks",
"run_task": "Executar Task", "run_task": "Executar Task",
"run_task_and_downstream": "Executar task & downstream",
"run_task_info": "Passe o cursor sobre qualquer task no editor de Flow Code e clique no botão \"Run task\" para testar sua task.",
"run_this_task": "Execute esta task",
"title": "Playground", "title": "Playground",
"toggle": "Playground" "toggle": "Playground"
}, },
@@ -1317,10 +1321,15 @@
"system_namespace_description": "Automatize tarefas de manutenção, desde alertas de falha até limpezas automatizadas.", "system_namespace_description": "Automatize tarefas de manutenção, desde alertas de falha até limpezas automatizadas.",
"tags": "Tags", "tags": "Tags",
"task": "Task", "task": "Task",
"task failed": "Tarefa falhou",
"task id": "Task ID", "task id": "Task ID",
"task id already exists": "Task Id já existe", "task id already exists": "Task Id já existe",
"task is running": "A task está RUNNING",
"task logs": "Task logs", "task logs": "Task logs",
"task run id": "ID do TaskRun", "task run id": "ID do TaskRun",
"task sent a warning": "A tarefa enviou um WARNING",
"task was skipped": "A tarefa foi ignorada",
"task was successful": "A tarefa foi bem-sucedida",
"taskDefaults": "Padrões da Task", "taskDefaults": "Padrões da Task",
"taskRunners": "Executores de Task", "taskRunners": "Executores de Task",
"task_id_exists": "Id da Task já existe", "task_id_exists": "Id da Task já existe",
@@ -1356,6 +1365,8 @@
"topology": "Topologia", "topology": "Topologia",
"topology-graph": { "topology-graph": {
"graph-orientation": "Orientação do gráfico", "graph-orientation": "Orientação do gráfico",
"invalid": "Erro no gráfico",
"invalid_description": "Ocorreu um erro ao carregar o gráfico. Por favor, verifique o código-fonte para erros.",
"zoom-fit": "Ajustar", "zoom-fit": "Ajustar",
"zoom-in": "Aumentar zoom", "zoom-in": "Aumentar zoom",
"zoom-out": "Diminuir zoom", "zoom-out": "Diminuir zoom",

View File

@@ -1016,10 +1016,14 @@
"pause done": "Выполнение приостановлено", "pause done": "Выполнение приостановлено",
"pause title": "Приостановить выполнение <code>{id}</code>.<br/>Обратите внимание, что текущие RUNNING задачи все равно будут обрабатываться, и выполнение придется возобновить вручную.", "pause title": "Приостановить выполнение <code>{id}</code>.<br/>Обратите внимание, что текущие RUNNING задачи все равно будут обрабатываться, и выполнение придется возобновить вручную.",
"playground": { "playground": {
"empty": "Нажмите кнопку \"Run task\", чтобы протестировать ваш workflow с помощью симулированного выполнения.", "confirm_create": "Вы не можете запустить playground во время создания flow. Запуск playground создаст flow.",
"history": "Последние 10 запусков", "history": "Последние 10 запусков",
"play_icon_info": "Вы также можете нажать на значок Play в представлениях No-Code или Topology.",
"run_all_tasks": "Запустить все Tasks", "run_all_tasks": "Запустить все Tasks",
"run_task": "Запустить Task", "run_task": "Запустить Task",
"run_task_and_downstream": "Запустить task и downstream",
"run_task_info": "Наведите курсор на любую task в редакторе Flow Code и нажмите кнопку \"Run task\", чтобы протестировать вашу task.",
"run_this_task": "Запустить этот task",
"title": "Песочница", "title": "Песочница",
"toggle": "Песочница" "toggle": "Песочница"
}, },
@@ -1317,10 +1321,15 @@
"system_namespace_description": "Автоматизируйте задачи обслуживания, от предупреждений о сбоях до автоматической очистки.", "system_namespace_description": "Автоматизируйте задачи обслуживания, от предупреждений о сбоях до автоматической очистки.",
"tags": "Теги", "tags": "Теги",
"task": "Задача", "task": "Задача",
"task failed": "Задача FAILED",
"task id": "ID задачи", "task id": "ID задачи",
"task id already exists": "ID задачи уже существует", "task id already exists": "ID задачи уже существует",
"task is running": "Задача RUNNING",
"task logs": "Журналы задач", "task logs": "Журналы задач",
"task run id": "ID выполнения задачи", "task run id": "ID выполнения задачи",
"task sent a warning": "Задача отправила предупреждение",
"task was skipped": "Задача была пропущена",
"task was successful": "Задача была успешной",
"taskDefaults": "Задача по умолчанию", "taskDefaults": "Задача по умолчанию",
"taskRunners": "Запускатели Task", "taskRunners": "Запускатели Task",
"task_id_exists": "Id задачи уже существует", "task_id_exists": "Id задачи уже существует",
@@ -1356,6 +1365,8 @@
"topology": "Топология", "topology": "Топология",
"topology-graph": { "topology-graph": {
"graph-orientation": "Ориентация графика", "graph-orientation": "Ориентация графика",
"invalid": "Ошибка графа",
"invalid_description": "Произошла ошибка при загрузке графа. Пожалуйста, проверьте исходный код на наличие ошибок.",
"zoom-fit": "Подогнать", "zoom-fit": "Подогнать",
"zoom-in": "Увеличить", "zoom-in": "Увеличить",
"zoom-out": "Уменьшить", "zoom-out": "Уменьшить",

View File

@@ -1016,10 +1016,14 @@
"pause done": "执行已PAUSED", "pause done": "执行已PAUSED",
"pause title": "暂停执行<code>{id}</code>。<br/>请注意当前正在运行的task仍将被处理并且执行需要手动恢复。", "pause title": "暂停执行<code>{id}</code>。<br/>请注意当前正在运行的task仍将被处理并且执行需要手动恢复。",
"playground": { "playground": {
"empty": "单击“Run task”按钮通过模拟执行测试您的工作流。", "confirm_create": "在创建flow时无法运行playground。启动playground运行将创建flow。",
"history": "最近10次运行", "history": "最近10次运行",
"play_icon_info": "您还可以在无代码或拓扑视图中点击播放图标。",
"run_all_tasks": "运行所有Tasks", "run_all_tasks": "运行所有Tasks",
"run_task": "运行Task", "run_task": "运行Task",
"run_task_and_downstream": "运行 task 和下游",
"run_task_info": "将鼠标悬停在Flow Code编辑器中的任何task上然后点击“Run task”按钮以测试您的task。",
"run_this_task": "运行此task",
"title": "游乐场", "title": "游乐场",
"toggle": "游乐场" "toggle": "游乐场"
}, },
@@ -1317,10 +1321,15 @@
"system_namespace_description": "自动化维护任务,从故障警报到自动清理。", "system_namespace_description": "自动化维护任务,从故障警报到自动清理。",
"tags": "标签", "tags": "标签",
"task": "任务", "task": "任务",
"task failed": "任务失败",
"task id": "任务ID", "task id": "任务ID",
"task id already exists": "任务ID已存在", "task id already exists": "任务ID已存在",
"task is running": "任务正在运行",
"task logs": "任务日志", "task logs": "任务日志",
"task run id": "任务运行 ID", "task run id": "任务运行 ID",
"task sent a warning": "任务发送了警告",
"task was skipped": "任务被跳过",
"task was successful": "任务成功",
"taskDefaults": "任务默认值", "taskDefaults": "任务默认值",
"taskRunners": "任务 Runners", "taskRunners": "任务 Runners",
"task_id_exists": "任务ID已存在", "task_id_exists": "任务ID已存在",
@@ -1356,6 +1365,8 @@
"topology": "拓扑", "topology": "拓扑",
"topology-graph": { "topology-graph": {
"graph-orientation": "图表方向", "graph-orientation": "图表方向",
"invalid": "图形错误",
"invalid_description": "加载图表时发生错误。请检查源代码中的错误。",
"zoom-fit": "适应屏幕", "zoom-fit": "适应屏幕",
"zoom-in": "放大", "zoom-in": "放大",
"zoom-out": "缩小", "zoom-out": "缩小",

View File

@@ -162,6 +162,7 @@ describe("FlowAutoCompletionProvider", () => {
"randomInt(lower=${1:0}, upper=${2:10})", "randomInt(lower=${1:0}, upper=${2:10})",
"randomPort()", "randomPort()",
"tasksWithState(state=${1:'FAILED'})", "tasksWithState(state=${1:'FAILED'})",
"http(uri=${1:'https://example.com'}, method=${2:'GET'})",
]); ]);
}) })

View File

@@ -263,6 +263,9 @@ public class DashboardController {
throw new IllegalArgumentException("`endDate` must be after `startDate`."); throw new IllegalArgumentException("`endDate` must be after `startDate`.");
} }
Pageable pageable = null; Pageable pageable = null;
if (globalFilter != null && globalFilter.getPageSize() != null && globalFilter.getPageNumber() != null) {
pageable = PageableUtils.from(globalFilter.getPageNumber(), globalFilter.getPageSize());
}
return new FetchChartDataQuery(chart, filters, startDate, endDate, tenantId, pageable); return new FetchChartDataQuery(chart, filters, startDate, endDate, tenantId, pageable);
} }

View File

@@ -624,7 +624,9 @@ public class ExecutionController {
} }
} }
public record WebhookResponse(String tenantId, String id, String namespace, String flowId, Integer flowRevision, ExecutionTrigger trigger, Map<String, Object> outputs, List<Label> labels, State state, URI url) { public record WebhookResponse(String tenantId, String id, String namespace, String flowId, Integer flowRevision,
ExecutionTrigger trigger, Map<String, Object> outputs, List<Label> labels,
State state, URI url) {
public static WebhookResponse fromExecution(Execution execution, URI url) { public static WebhookResponse fromExecution(Execution execution, URI url) {
return new WebhookResponse(execution.getTenantId(), execution.getId(), execution.getNamespace(), execution.getFlowId(), execution.getFlowRevision(), execution.getTrigger(), execution.getOutputs(), execution.getLabels(), execution.getState(), url); return new WebhookResponse(execution.getTenantId(), execution.getId(), execution.getNamespace(), execution.getFlowId(), execution.getFlowRevision(), execution.getTrigger(), execution.getOutputs(), execution.getLabels(), execution.getState(), url);
} }
@@ -750,7 +752,7 @@ public class ExecutionController {
// This is not nice, but we cannot use @AllArgsConstructor as it would open a bunch of necessary changes on the Execution class. // This is not nice, but we cannot use @AllArgsConstructor as it would open a bunch of necessary changes on the Execution class.
ExecutionResponse(String tenantId, String id, String namespace, String flowId, Integer flowRevision, List<TaskRun> taskRunList, Map<String, Object> inputs, Map<String, Object> outputs, List<Label> labels, Map<String, Object> variables, State state, String parentId, String originalId, ExecutionTrigger trigger, boolean deleted, ExecutionMetadata metadata, Instant scheduleDate, String traceParent, List<TaskFixture> fixtures, ExecutionKind kind, List<Breakpoint> breakpoints, URI url) { ExecutionResponse(String tenantId, String id, String namespace, String flowId, Integer flowRevision, List<TaskRun> taskRunList, Map<String, Object> inputs, Map<String, Object> outputs, List<Label> labels, Map<String, Object> variables, State state, String parentId, String originalId, ExecutionTrigger trigger, boolean deleted, ExecutionMetadata metadata, Instant scheduleDate, String traceParent, List<TaskFixture> fixtures, ExecutionKind kind, List<Breakpoint> breakpoints, URI url) {
super(tenantId, id, namespace, flowId, flowRevision, taskRunList, inputs, outputs, labels, variables, state, parentId, originalId, trigger, deleted, metadata, scheduleDate, traceParent, fixtures,kind, breakpoints); super(tenantId, id, namespace, flowId, flowRevision, taskRunList, inputs, outputs, labels, variables, state, parentId, originalId, trigger, deleted, metadata, scheduleDate, traceParent, fixtures, kind, breakpoints);
this.url = url; this.url = url;
} }
@@ -870,7 +872,8 @@ public class ExecutionController {
} }
InputStream fileHandler = switch (path.getScheme()) { InputStream fileHandler = switch (path.getScheme()) {
case StorageContext.KESTRA_SCHEME -> storageInterface.get(execution.get().getTenantId(), execution.get().getNamespace(), path); case StorageContext.KESTRA_SCHEME ->
storageInterface.get(execution.get().getTenantId(), execution.get().getNamespace(), path);
case LocalPath.FILE_SCHEME -> localPathFactory.createLocalPath().get(path); case LocalPath.FILE_SCHEME -> localPathFactory.createLocalPath().get(path);
case Namespace.NAMESPACE_FILE_SCHEME -> { case Namespace.NAMESPACE_FILE_SCHEME -> {
URI uri = nsFileToInternalStorageURI(path, execution.get()); URI uri = nsFileToInternalStorageURI(path, execution.get());
@@ -906,7 +909,8 @@ public class ExecutionController {
} }
long size = switch (path.getScheme()) { long size = switch (path.getScheme()) {
case StorageContext.KESTRA_SCHEME -> storageInterface.getAttributes(execution.get().getTenantId(), execution.get().getNamespace(), path).getSize(); case StorageContext.KESTRA_SCHEME ->
storageInterface.getAttributes(execution.get().getTenantId(), execution.get().getNamespace(), path).getSize();
case LocalPath.FILE_SCHEME -> localPathFactory.createLocalPath().getAttributes(path).size(); case LocalPath.FILE_SCHEME -> localPathFactory.createLocalPath().getAttributes(path).size();
case Namespace.NAMESPACE_FILE_SCHEME -> { case Namespace.NAMESPACE_FILE_SCHEME -> {
URI uri = nsFileToInternalStorageURI(path, execution.get()); URI uri = nsFileToInternalStorageURI(path, execution.get());
@@ -1805,12 +1809,25 @@ public class ExecutionController {
// Register for updates // Register for updates
streamingService.registerSubscriber(executionId, subscriberId, emitter, flow); streamingService.registerSubscriber(executionId, subscriberId, emitter, flow);
} catch (TimeoutException e) {
emitter.error(new HttpStatusException(HttpStatus.NOT_FOUND, // Fetch again the execution to avoid race when execution is ended before we are subscribed
"Unable to find execution " + executionId)); execution = executionRepository.findById(tenantService.resolveTenant(), executionId).orElse(null);
if (streamingService.isStopFollow(flow, execution)) {
emitter.next(Event.of(execution).id("end"));
emitter.complete();
}
if (execution.getState().isBreakpoint()) {
emitter.next(Event.of(execution).id("progress"));
}
} catch (IllegalStateException e) { } catch (IllegalStateException e) {
log.error(e.getMessage(), e);
emitter.error(new HttpStatusException(HttpStatus.NOT_FOUND, emitter.error(new HttpStatusException(HttpStatus.NOT_FOUND,
"Unable to find flow for execution " + executionId)); "Unable to find flow for execution " + executionId));
} catch (Exception e) {
log.error(e.getMessage(), e);
emitter.error(new HttpStatusException(HttpStatus.NOT_FOUND,
"Unable to find execution " + executionId));
} }
}, FluxSink.OverflowStrategy.BUFFER) }, FluxSink.OverflowStrategy.BUFFER)
.timeout(Duration.ofHours(1)) // avoid idle SSE sockets by setting a between-item timeout .timeout(Duration.ofHours(1)) // avoid idle SSE sockets by setting a between-item timeout
@@ -1843,7 +1860,8 @@ public class ExecutionController {
} }
InputStream fileStream = switch (path.getScheme()) { InputStream fileStream = switch (path.getScheme()) {
case StorageContext.KESTRA_SCHEME -> storageInterface.get(execution.get().getTenantId(), execution.get().getNamespace(), path); case StorageContext.KESTRA_SCHEME ->
storageInterface.get(execution.get().getTenantId(), execution.get().getNamespace(), path);
case LocalPath.FILE_SCHEME -> localPathFactory.createLocalPath().get(path); case LocalPath.FILE_SCHEME -> localPathFactory.createLocalPath().get(path);
case Namespace.NAMESPACE_FILE_SCHEME -> { case Namespace.NAMESPACE_FILE_SCHEME -> {
URI uri = nsFileToInternalStorageURI(path, execution.get()); URI uri = nsFileToInternalStorageURI(path, execution.get());
@@ -1959,8 +1977,8 @@ public class ExecutionController {
} }
executions.forEach(execution -> setLabelsOnTerminatedExecution( executions.forEach(execution -> setLabelsOnTerminatedExecution(
execution, execution,
Label.deduplicate(ListUtils.concat(execution.getLabels(), setLabelsByIds.executionLabels()))) Label.deduplicate(ListUtils.concat(execution.getLabels(), setLabelsByIds.executionLabels())))
); );
return HttpResponse.ok(BulkResponse.builder().count(executions.size()).build()); return HttpResponse.ok(BulkResponse.builder().count(executions.size()).build());
} }

View File

@@ -96,9 +96,6 @@ public class FlowController {
@Inject @Inject
private TenantService tenantService; private TenantService tenantService;
@Inject
private JsonSchemaGenerator jsonSchemaGenerator;
@ExecuteOn(TaskExecutors.IO) @ExecuteOn(TaskExecutors.IO)
@Get(uri = "{namespace}/{id}/graph") @Get(uri = "{namespace}/{id}/graph")

View File

@@ -88,6 +88,7 @@ public class ExecutionDependenciesStreamingService {
// end the flux if there are no more dependencies to follow // end the flux if there are no more dependencies to follow
if (consumer.dependencies().isEmpty()) { if (consumer.dependencies().isEmpty()) {
sink.next(Event.of(ExecutionStatusEvent.of(Execution.builder().id(executionId).build())).id("end-all"));
sink.complete(); sink.complete();
} }
} catch (Exception e) { } catch (Exception e) {
@@ -149,4 +150,4 @@ public class ExecutionDependenciesStreamingService {
return execution.getLabels().stream().anyMatch(label -> label.key().equals(CORRELATION_ID) && label.value().equals(correlationId)) && return execution.getLabels().stream().anyMatch(label -> label.key().equals(CORRELATION_ID) && label.value().equals(correlationId)) &&
nodes.stream().anyMatch(node -> node.getTenantId().equals(execution.getTenantId()) && node.getNamespace().equals(execution.getNamespace()) && node.getId().equals(execution.getFlowId())); nodes.stream().anyMatch(node -> node.getTenantId().equals(execution.getTenantId()) && node.getNamespace().equals(execution.getNamespace()) && node.getId().equals(execution.getFlowId()));
} }
} }