Compare commits

...

72 Commits

Author SHA1 Message Date
YannC.
f58bc4caba chore: update version to 0.23.11 2025-08-12 10:58:41 +02:00
Loïc Mathieu
e99ae9513f fix(executions): correctly fail the request when trying to resume an execution with the wrong inputs
Fixes #9959
2025-08-12 09:40:44 +02:00
Piyush Bhaskar
c8b51fcacf fix(core): reduce size of code block text and padding (#10689) 2025-08-12 11:47:41 +05:30
brian.mulier
813b2f6439 fix(dashboard): avoid duplicate dashboard calls + properly refresh dashboards on refresh button + don't discard component entirely on refresh 2025-08-11 22:29:15 +02:00
brian.mulier
c6b5bca25b fix(dashboard): properly use time filters in queries
closes kestra-io/kestra-ee#4389
2025-08-11 22:29:15 +02:00
brian.mulier
de35d2cdb9 tests(core): add a test to taskrunners to ensure it's working multiple times on the same working directory
part of kestra-io/plugin-ee-kubernetes#45
2025-08-11 15:06:21 +02:00
Loïc Mathieu
a6ffbd59d0 fix(executions): properly fail the task if it contains unsupported unicode sequence
This occurs in Postgres using the `\u0000` unicode sequence. Postgres refuse to store any JSONB with this sequence as it has no textual representation.
We now properly detect that and fail the task.

Fixes #10326
2025-08-11 11:54:16 +02:00
Piyush Bhaskar
568740a214 fix(flows): copy trigger url propely. (#10645) 2025-08-08 13:13:02 +05:30
Loïc Mathieu
aa0d2c545f fix(executions): allow caching tasks that use the 'workingDir' variable
Fixes #10253
2025-08-08 09:08:00 +02:00
brian.mulier
cda77d5146 fix(core): ensure props with defaults are not marked as required in generated doc 2025-08-07 15:10:16 +02:00
brian.mulier
d4fd1f61ba fix(core): wrong @NotNull import leading to key not being marked as required
closes #9287
2025-08-07 15:10:16 +02:00
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
107 changed files with 1342 additions and 637 deletions

View File

@@ -20,6 +20,15 @@ on:
required: false
type: string
default: "LATEST"
force-download-artifact:
description: 'Force download artifact'
required: false
type: string
default: "true"
options:
- "true"
- "false"
env:
PLUGIN_VERSION: ${{ github.event.inputs.plugin-version != null && github.event.inputs.plugin-version || 'LATEST' }}
jobs:
@@ -38,9 +47,18 @@ jobs:
id: plugins
with:
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:
name: Publish Docker
needs: [ plugins ]
needs: [ plugins, build-artifacts ]
runs-on: ubuntu-latest
strategy:
matrix:
@@ -69,18 +87,31 @@ jobs:
fi
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
echo "plugins=${{ matrix.image.plugins }}" >> $GITHUB_OUTPUT
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
with:
tag: ${{steps.vars.outputs.tag}}
fileName: 'kestra-*'
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
run: |
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_PASSWORD: ${{ secrets.SONATYPE_GPG_PASSWORD }}
SONATYPE_GPG_FILE: ${{ secrets.SONATYPE_GPG_FILE }}
GH_PERSONAL_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
SLACK_RELEASES_WEBHOOK_URL: ${{ secrets.SLACK_RELEASES_WEBHOOK_URL }}
end:
runs-on: ubuntu-latest
needs:

View File

@@ -1,23 +1,7 @@
name: Build Artifacts
on:
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."
workflow_call: {}
jobs:
build:
@@ -82,55 +66,6 @@ jobs:
run: |
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
- name: Artifacts - Upload JAR
uses: actions/upload-artifact@v4
@@ -143,10 +78,3 @@ jobs:
with:
name: exe
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
on:
workflow_dispatch:
workflow_call:
secrets:
GH_PERSONAL_TOKEN:
description: "The Github personal token."
required: true
push:
tags:
- '*'
SLACK_RELEASES_WEBHOOK_URL:
description: "The Slack webhook URL."
required: true
jobs:
publish:

View File

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

View File

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

View File

@@ -122,12 +122,13 @@ public class JsonSchemaGenerator {
if (jsonNode instanceof ObjectNode clazzSchema && clazzSchema.get("required") instanceof ArrayNode requiredPropsNode && clazzSchema.get("properties") instanceof ObjectNode properties) {
List<String> requiredFieldValues = StreamSupport.stream(requiredPropsNode.spliterator(), false)
.map(JsonNode::asText)
.toList();
.collect(Collectors.toList());
properties.fields().forEachRemaining(e -> {
int indexInRequiredArray = requiredFieldValues.indexOf(e.getKey());
if (indexInRequiredArray != -1 && e.getValue() instanceof ObjectNode valueNode && valueNode.has("default")) {
requiredPropsNode.remove(indexInRequiredArray);
requiredFieldValues.remove(indexInRequiredArray);
}
});

View File

@@ -27,7 +27,7 @@ public interface QueueInterface<T> extends Closeable, Pauseable {
void delete(String consumerGroup, T message) throws QueueException;
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) {

View File

@@ -0,0 +1,12 @@
package io.kestra.core.queues;
import java.io.Serial;
public class UnsupportedMessageException extends QueueException {
@Serial
private static final long serialVersionUID = 1L;
public UnsupportedMessageException(String message, Throwable cause) {
super(message, cause);
}
}

View File

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

View File

@@ -764,6 +764,7 @@ public class Worker implements Service, Runnable, AutoCloseable {
workerTask = workerTask.withTaskRun(workerTask.getTaskRun().withState(state));
WorkerTaskResult workerTaskResult = new WorkerTaskResult(workerTask.getTaskRun(), dynamicTaskRuns);
this.workerTaskResultQueue.emit(workerTaskResult);
// upload the cache file, hash may not be present if we didn't succeed in computing it
@@ -796,6 +797,10 @@ public class Worker implements Service, Runnable, AutoCloseable {
// If it's a message too big, we remove the outputs
failed = failed.withOutputs(Variables.empty());
}
if (e instanceof UnsupportedMessageException) {
// we expect the offending char is in the output so we remove it
failed = failed.withOutputs(Variables.empty());
}
WorkerTaskResult workerTaskResult = new WorkerTaskResult(failed);
RunContextLogger contextLogger = runContextLoggerFactory.create(workerTask);
contextLogger.logger().error("Unable to emit the worker task result to the queue: {}", e.getMessage(), e);
@@ -818,7 +823,11 @@ public class Worker implements Service, Runnable, AutoCloseable {
private Optional<String> hashTask(RunContext runContext, Task task) {
try {
var map = JacksonMapper.toMap(task);
var rMap = runContext.render(map);
// If there are task provided variables, rendering the task may fail.
// The best we can do is to add a fake 'workingDir' as it's an often added variables,
// and it should not be part of the task hash.
Map<String, Object> variables = Map.of("workingDir", "workingDir");
var rMap = runContext.render(map, variables);
var json = JacksonMapper.ofJson().writeValueAsBytes(rMap);
MessageDigest digest = MessageDigest.getInstance("SHA-256");
digest.update(json);

View File

@@ -8,6 +8,7 @@ import io.kestra.core.events.CrudEventType;
import io.kestra.core.exceptions.DeserializationException;
import io.kestra.core.exceptions.InternalException;
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.ConditionContext;
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
List<Trigger> triggers = triggerState.findAllForAllTenants();
Map<String, Trigger> triggers = triggerState.findAllForAllTenants().stream().collect(Collectors.toMap(HasUID::uid, Function.identity()));
flows
.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)))
.distinct()
.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()) {
RunContext runContext = runContextFactory.of(flowAndTrigger.flow(), flowAndTrigger.trigger());
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) {
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()
.filter(trigger -> !flows.stream().map(FlowId::uidWithoutRevision).toList().contains(FlowId.uid(trigger)))
.filter(trigger -> !flowIds.contains(FlowId.uid(trigger)))
.forEach(trigger -> {
try {
this.triggerState.delete(trigger);
@@ -491,12 +496,8 @@ public abstract class AbstractScheduler implements Scheduler, Service {
.map(abstractTrigger -> {
RunContext runContext = runContextFactory.of(flow, abstractTrigger);
ConditionContext conditionContext = conditionService.conditionContext(runContext, flow, null);
Trigger triggerContext = null;
Trigger lastTrigger = triggerContextsToEvaluate
.stream()
.filter(triggerContextToFind -> triggerContextToFind.uid().equals(Trigger.uid(flow, abstractTrigger)))
.findFirst()
.orElse(null);
Trigger triggerContext;
Trigger lastTrigger = triggerById.get(Trigger.uid(flow, abstractTrigger));
// If a trigger is not found in triggers to evaluate, then we ignore it
if (lastTrigger == 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.FlowWithSource;
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.triggers.AbstractTrigger;
import io.kestra.core.models.validations.ModelValidator;
@@ -51,7 +52,6 @@ import java.util.stream.StreamSupport;
@Singleton
@Slf4j
public class FlowService {
@Inject
Optional<FlowRepositoryInterface> flowRepository;
@@ -236,6 +236,7 @@ public class FlowService {
}
List<String> warnings = new ArrayList<>(checkValidSubflows(flow, tenantId));
List<io.kestra.plugin.core.trigger.Flow> flowTriggers = ListUtils.emptyOnNull(flow.getTriggers()).stream()
.filter(io.kestra.plugin.core.trigger.Flow.class::isInstance)
.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;
}

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.");
}
List<Task> allTasks = value.allTasksWithChilds();
// tasks unique id
List<String> taskIds = value.allTasksWithChilds()
.stream()
List<String> taskIds = allTasks.stream()
.map(Task::getId)
.toList();
@@ -72,8 +73,8 @@ public class FlowValidator implements ConstraintValidator<FlowValidation, Flow>
violations.add("Duplicate trigger id with name [" + String.join(", ", duplicateIds) + "]");
}
value.allTasksWithChilds()
.stream().filter(task -> task instanceof ExecutableTask<?> executableTask
allTasks.stream()
.filter(task -> task instanceof ExecutableTask<?> executableTask
&& value.getId().equals(executableTask.subflowId().flowId())
&& value.getNamespace().equals(executableTask.subflowId().namespace()))
.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*\\}\\}"))
.collect(Collectors.toList());
List<String> invalidTasks = value.allTasks()
List<String> invalidTasks = allTasks.stream()
.filter(task -> checkObjectFieldsWithPatterns(task, inputsWithMinusPatterns))
.map(task -> task.getId())
.collect(Collectors.toList());
@@ -112,12 +113,12 @@ public class FlowValidator implements ConstraintValidator<FlowValidation, Flow>
" [" + String.join(", ", invalidTasks) + "]");
}
List<Pattern> outputsWithMinusPattern = value.allTasks()
List<Pattern> outputsWithMinusPattern = allTasks.stream()
.filter(output -> Optional.ofNullable(output.getId()).orElse("").contains("-"))
.map(output -> Pattern.compile("\\{\\{\\s*outputs\\." + output.getId() + "\\.[^}]+\\s*\\}\\}"))
.collect(Collectors.toList());
invalidTasks = value.allTasks()
invalidTasks = allTasks.stream()
.filter(task -> checkObjectFieldsWithPatterns(task, outputsWithMinusPattern))
.map(task -> task.getId())
.collect(Collectors.toList());

View File

@@ -19,7 +19,6 @@ import lombok.experimental.SuperBuilder;
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
@EqualsAndHashCode
//@TriggersDataFilterValidation
@Schema(
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."

View File

@@ -208,48 +208,50 @@ public class Subflow extends Task implements ExecutableTask<Subflow.Output>, Chi
return Optional.empty();
}
boolean isOutputsAllowed = runContext
.<Boolean>pluginConfiguration(PLUGIN_FLOW_OUTPUTS_ENABLED)
.orElse(true);
final Output.OutputBuilder builder = Output.builder()
.executionId(execution.getId())
.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);
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);
if (this.wait) { // we only compute outputs if we wait for the subflow
boolean isOutputsAllowed = runContext
.<Boolean>pluginConfiguration(PLUGIN_FLOW_OUTPUTS_ENABLED)
.orElse(true);
return Optional.of(SubflowExecutionResult.builder()
.executionId(execution.getId())
.state(State.Type.FAILED)
.parentTaskRun(taskRun)
.build());
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);
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

@@ -9,11 +9,11 @@ import io.kestra.core.runners.DefaultRunContext;
import io.kestra.core.runners.RunContext;
import io.kestra.core.services.FlowService;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import org.codehaus.commons.nullanalysis.NotNull;
import java.util.NoSuchElementException;

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

@@ -112,7 +112,7 @@ class JsonSchemaGeneratorTest {
var requiredWithDefault = definitions.get("io.kestra.core.docs.JsonSchemaGeneratorTest-RequiredWithDefault");
assertThat(requiredWithDefault, is(notNullValue()));
assertThat((List<String>) requiredWithDefault.get("required"), not(contains("requiredWithDefault")));
assertThat((List<String>) requiredWithDefault.get("required"), not(containsInAnyOrder("requiredWithDefault", "anotherRequiredWithDefault")));
var properties = (Map<String, Map<String, Object>>) flow.get("properties");
var listeners = properties.get("listeners");
@@ -253,7 +253,7 @@ class JsonSchemaGeneratorTest {
void requiredAreRemovedIfThereIsADefault() {
Map<String, Object> generate = jsonSchemaGenerator.properties(Task.class, RequiredWithDefault.class);
assertThat(generate, is(not(nullValue())));
assertThat((List<String>) generate.get("required"), not(containsInAnyOrder("requiredWithDefault")));
assertThat((List<String>) generate.get("required"), not(containsInAnyOrder("requiredWithDefault", "anotherRequiredWithDefault")));
assertThat((List<String>) generate.get("required"), containsInAnyOrder("requiredWithNoDefault"));
}
@@ -466,6 +466,11 @@ class JsonSchemaGeneratorTest {
@Builder.Default
private Property<TaskWithEnum.TestClass> requiredWithDefault = Property.ofValue(TaskWithEnum.TestClass.builder().testProperty("test").build());
@PluginProperty
@NotNull
@Builder.Default
private Property<TaskWithEnum.TestClass> anotherRequiredWithDefault = Property.ofValue(TaskWithEnum.TestClass.builder().testProperty("test2").build());
@PluginProperty
@NotNull
private Property<TaskWithEnum.TestClass> requiredWithNoDefault;

View File

@@ -44,6 +44,7 @@ import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
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);
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

@@ -5,6 +5,7 @@ import io.kestra.core.junit.annotations.LoadFlows;
import io.kestra.core.models.annotations.Plugin;
import io.kestra.core.models.executions.Execution;
import io.kestra.core.models.flows.State;
import io.kestra.core.models.property.Property;
import io.kestra.core.models.tasks.RunnableTask;
import io.kestra.core.models.tasks.Task;
import io.kestra.core.queues.QueueException;
@@ -18,6 +19,7 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.util.Map;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
@@ -77,8 +79,12 @@ public class TaskCacheTest {
@Plugin
public static class CounterTask extends Task implements RunnableTask<CounterTask.Output> {
private String workingDir;
@Override
public Output run(RunContext runContext) throws Exception {
Map<String, Object> variables = Map.of("workingDir", runContext.workingDir().path().toString());
runContext.render(this.workingDir, variables);
return Output.builder()
.counter(COUNTER.incrementAndGet())
.build();

View File

@@ -372,4 +372,44 @@ class FlowServiceTest {
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.models.Label;
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.QueueFactoryInterface;
import io.kestra.core.queues.QueueInterface;
import io.kestra.core.repositories.ExecutionRepositoryInterface;
import io.kestra.core.runners.RunnerUtils;
import jakarta.inject.Inject;
import jakarta.inject.Named;
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.atomic.AtomicReference;
import static io.kestra.core.tenant.TenantService.MAIN_TENANT;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertTrue;
@KestraTest(startRunner = true)
class SubflowRunnerTest {
@@ -24,6 +32,10 @@ class SubflowRunnerTest {
@Inject
private ExecutionRepositoryInterface executionRepository;
@Inject
@Named(QueueFactoryInterface.EXECUTION_NAMED)
protected QueueInterface<Execution> executionQueue;
@Test
@LoadFlows({"flows/valids/subflow-inherited-labels-child.yaml", "flows/valids/subflow-inherited-labels-parent.yaml"})
void inheritedLabelsAreOverridden() throws QueueException, TimeoutException {
@@ -50,4 +62,29 @@ class SubflowRunnerTest {
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

@@ -4,6 +4,7 @@ namespace: io.kestra.tests
tasks:
- id: cache
type: io.kestra.core.runners.TaskCacheTest$CounterTask
workingDir: "{{workingDir}}"
taskCache:
enabled: true
ttl: PT1S

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.1
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.priority=low
org.gradle.priority=low

View File

@@ -4,6 +4,7 @@ import io.kestra.core.models.executions.TaskRun;
import io.kestra.core.models.executions.Variables;
import io.kestra.core.models.flows.State;
import io.kestra.core.queues.QueueException;
import io.kestra.core.queues.UnsupportedMessageException;
import io.kestra.core.runners.WorkerTaskResult;
import io.kestra.core.utils.IdUtils;
import io.kestra.jdbc.runner.JdbcQueueTest;
@@ -31,7 +32,8 @@ class PostgresQueueTest extends JdbcQueueTest {
.build();
var exception = assertThrows(QueueException.class, () -> workerTaskResultQueue.emit(workerTaskResult));
assertThat(exception.getMessage()).isEqualTo("Unable to emit a message to the queue");
assertThat(exception).isInstanceOf(UnsupportedMessageException.class);
assertThat(exception.getMessage()).contains("ERROR: unsupported Unicode escape sequence");
assertThat(exception.getCause()).isInstanceOf(DataException.class);
}
}

View File

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

View File

@@ -7,16 +7,13 @@ import com.google.common.collect.Iterables;
import io.kestra.core.exceptions.DeserializationException;
import io.kestra.core.metrics.MetricRegistry;
import io.kestra.core.models.executions.Execution;
import io.kestra.core.queues.QueueException;
import io.kestra.core.queues.QueueInterface;
import io.kestra.core.queues.QueueService;
import io.kestra.core.queues.*;
import io.kestra.core.utils.Either;
import io.kestra.core.utils.ExecutorsUtils;
import io.kestra.core.utils.IdUtils;
import io.kestra.jdbc.JdbcTableConfigs;
import io.kestra.jdbc.JdbcMapper;
import io.kestra.jdbc.JooqDSLContextWrapper;
import io.kestra.core.queues.MessageTooBigException;
import io.kestra.jdbc.repository.AbstractJdbcRepository;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.Timer;
@@ -151,6 +148,11 @@ public abstract class JdbcQueue<T> implements QueueInterface<T> {
.execute();
});
} catch (DataException e) { // The exception is from the data itself, not the database/network/driver so instead of fail fast, we throw a recoverable QueueException
// Postgres refuses to store JSONB with the '\0000' codepoint as it has no textual representation.
// We try to detect that and fail with a specific exception so the Worker can recover from it.
if (e.getMessage() != null && e.getMessage().contains("ERROR: unsupported Unicode escape sequence")) {
throw new UnsupportedMessageException(e.getMessage(), e);
}
throw new QueueException("Unable to emit a message to the queue", e);
}

View File

@@ -38,6 +38,10 @@ public abstract class AbstractTaskRunnerTest {
@Test
protected void run() throws Exception {
var runContext = runContext(this.runContextFactory);
simpleRun(runContext);
}
private void simpleRun(RunContext runContext) throws Exception {
var commands = initScriptCommands(runContext);
Mockito.when(commands.getCommands()).thenReturn(
Property.ofValue(ScriptService.scriptCommands(List.of("/bin/sh", "-c"), Collections.emptyList(), List.of("echo 'Hello World'")))
@@ -166,6 +170,13 @@ public abstract class AbstractTaskRunnerTest {
assertThat(taskException.getLogConsumer().getOutputs().get("logOutput")).isEqualTo("Hello World");
}
@Test
protected void canWorkMultipleTimeInSameWdir() throws Exception {
var runContext = runContext(this.runContextFactory);
simpleRun(runContext);
simpleRun(runContext);
}
protected RunContext runContext(RunContextFactory runContextFactory) {
return this.runContext(runContextFactory, null);
}
@@ -236,4 +247,4 @@ public abstract class AbstractTaskRunnerTest {
protected boolean needsToSpecifyWorkingDirectory() {
return false;
}
}
}

14
ui/package-lock.json generated
View File

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

View File

@@ -24,7 +24,7 @@
},
"dependencies": {
"@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/controls": "^1.1.2",
"@vue-flow/core": "^1.45.0",
@@ -149,7 +149,7 @@
"@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7"
},
"el-table-infinite-scroll": {
"vue": "$vue"
"vue": "^3.5.18"
},
"storybook": "$storybook"
},

View File

@@ -49,6 +49,7 @@
ref="tabContent"
:is="activeTab.component"
@go-to-detail="blueprintId => selectedBlueprintId = blueprintId"
:namespace
:embed="activeTab.props && activeTab.props.embed !== undefined ? activeTab.props.embed : true"
/>
</section>
@@ -163,16 +164,11 @@
},
getTabClasses(tab) {
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 {
"container": !isEnterpriseTab && !isOverviewTab,
"mt-4": !isEnterpriseTab && !isOverviewTab,
"px-0": isEnterpriseTab && isOverviewTab,
"gantt-container": isGanttTab
"container": !isEnterpriseTab,
"mt-4": !isEnterpriseTab,
"px-0": isEnterpriseTab,
};
},
},

View File

@@ -1,6 +1,6 @@
<template>
<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 />
</div>
@@ -85,6 +85,7 @@
<style scoped lang="scss">
@import "../../styles/code.scss";
@import "@kestra-io/ui-libs/src/scss/_color-palette";
.element {
cursor: pointer;
@@ -107,7 +108,8 @@
}
.playground-run-task{
background-color: blue;
color: $base-white;
background-color: $base-blue-400;
height: 16px;
width: 16px;
font-size: 4px;

View File

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

View File

@@ -14,11 +14,11 @@
/>
</section>
<Sections :key :dashboard :charts :show-default="dashboard.id === 'default'" :padding="padding" />
<Sections ref="dashboardComponent" :dashboard :charts :show-default="dashboard.id === 'default'" :padding="padding" />
</template>
<script setup lang="ts">
import {computed, onBeforeMount, ref} from "vue";
import {computed, onBeforeMount, ref, useTemplateRef} from "vue";
import type {Dashboard, Chart} from "./composables/useDashboards";
import {ALLOWED_CREATION_ROUTES, getDashboard, processFlowYaml} from "./composables/useDashboards";
@@ -43,8 +43,6 @@
import YAML_FLOW from "./assets/default_flow_definition.yaml?raw";
import YAML_NAMESPACE from "./assets/default_namespace_definition.yaml?raw";
import UTILS from "../../utils/utils.js";
import {useRoute, useRouter} from "vue-router";
const route = useRoute();
const router = useRouter();
@@ -65,21 +63,18 @@
const dashboard = ref<Dashboard>({id: "", charts: []});
const charts = ref<Chart[]>([]);
// We use a key to force re-rendering of the Sections component
let key = ref(UTILS.uid());
const loadCharts = async (allCharts: Chart[] = []) => {
charts.value = [];
for (const chart of allCharts) {
charts.value.push({...chart, content: stringify(chart)});
}
refreshCharts()
};
const dashboardComponent = useTemplateRef("dashboardComponent");
const refreshCharts = () => {
key.value = UTILS.uid();
dashboardComponent.value!.refreshCharts();
};
const load = async (id = "default", defaultYAML = YAML_MAIN) => {

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") {
const query = {};
@@ -107,7 +121,7 @@ export function chartClick(moment, router, route, event, parsedData, elements, t
state = parsedData.labels[element.index];
}
if (state) {
query.state = state;
query.state = extractState(state);
query.scope = "USER";
query.size = 100;
query.page = 1;
@@ -137,7 +151,7 @@ export function chartClick(moment, router, route, event, parsedData, elements, t
}
if (event.state) {
query.state = event.state;
query.state = extractState(event.state);
}
if (route.query.namespace) {

View File

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

View File

@@ -11,12 +11,12 @@
</template>
<script lang="ts" setup>
import {PropType, computed} from "vue";
import {PropType, computed, watch} from "vue";
import moment from "moment";
import {Bar} from "vue-chartjs";
import NoData from "../../layout/NoData.vue";
import type {Chart} from "../composables/useDashboards";
import {Chart, getDashboard} from "../composables/useDashboards";
import {useChartGenerator} from "../composables/useDashboards";
@@ -159,7 +159,19 @@
return {labels, datasets};
});
const {data: generated} = useChartGenerator(props);
const {data: generated, generate} = useChartGenerator(props);
function refresh() {
return generate(getDashboard(route, "id")!);
}
defineExpose({
refresh
});
watch(() => route.params.filters, () => {
refresh();
}, {deep: true});
</script>
<style lang="scss" scoped>
@@ -182,4 +194,4 @@
min-height: var(--chart-height);
max-height: var(--chart-height);
}
</style>
</style>

View File

@@ -10,12 +10,13 @@
</template>
<script setup lang="ts">
import {PropType} from "vue";
import {PropType, watch} from "vue";
import type {Chart} from "../composables/useDashboards";
import {Chart, getDashboard} from "../composables/useDashboards";
import {getChartTitle, getPropertyValue, useChartGenerator} from "../composables/useDashboards";
import NoData from "../../layout/NoData.vue";
import {useRoute} from "vue-router";
const props = defineProps({
chart: {type: Object as PropType<Chart>, required: true},
@@ -23,7 +24,21 @@
showDefault: {type: Boolean, default: false},
});
const {percentageShown, EMPTY_TEXT, data} = useChartGenerator(props);
const route = useRoute();
const {percentageShown, EMPTY_TEXT, data, generate} = useChartGenerator(props);
function refresh() {
return generate(getDashboard(route, "id")!);
}
defineExpose({
refresh
});
watch(() => route.params.filters, () => {
refresh();
}, {deep: true});
</script>
<style scoped lang="scss">

View File

@@ -7,7 +7,7 @@
</template>
<script setup lang="ts">
import {PropType, onMounted, watch, ref} from "vue";
import {PropType, watch, ref} from "vue";
import type {RouteLocation} from "vue-router";
@@ -34,9 +34,17 @@
else data.value = props.chart.content ?? props.chart.source?.content;
};
const dashboardID = (route: RouteLocation) => getDashboard(route, "id") || "default"
const dashboardID = (route: RouteLocation) => getDashboard(route, "id")!;
watch(route, async (changed) => await getData(dashboardID(changed)));
function refresh() {
return getData(dashboardID(route));
}
onMounted(async () => await getData(dashboardID(route)));
defineExpose({
refresh
});
watch(() => route.params.filters, () => {
refresh();
}, {deep: true, immediate: true});
</script>

View File

@@ -22,9 +22,9 @@
</template>
<script lang="ts" setup>
import {computed,PropType} from "vue";
import {computed, PropType, watch} from "vue";
import type {Chart} from "../composables/useDashboards";
import {Chart, getDashboard} from "../composables/useDashboards";
import {useChartGenerator} from "../composables/useDashboards";
@@ -183,7 +183,19 @@
};
});
const {data: generated} = useChartGenerator(props);
const {data: generated, generate} = useChartGenerator(props);
function refresh() {
return generate(getDashboard(route, "id")!);
}
defineExpose({
refresh
});
watch(() => route.params.filters, () => {
refresh();
}, {deep: true});
</script>
<style lang="scss" scoped>
@@ -192,4 +204,4 @@
.chart {
max-height: $height;
}
</style>
</style>

View File

@@ -56,6 +56,7 @@
<div class="flex-grow-1">
<component
ref="chartsComponents"
:is="TYPES[chart.type as keyof typeof TYPES]"
:chart
:filters
@@ -89,6 +90,18 @@
import Download from "vue-material-design-icons/Download.vue";
import Pencil from "vue-material-design-icons/Pencil.vue";
const chartsComponents = ref<{refresh(): void}[]>();
function refreshCharts() {
chartsComponents.value!.forEach((component) => {
component.refresh();
});
}
defineExpose({
refreshCharts
});
const props = defineProps<{
dashboard: Dashboard;
charts?: Chart[];

View File

@@ -34,7 +34,7 @@
</template>
<script lang="ts" setup>
import {PropType, onMounted, watch, ref, computed} from "vue";
import {PropType, watch, ref, computed} from "vue";
import type {RouteLocation} from "vue-router";
@@ -116,16 +116,24 @@
const dashboardID = (route: RouteLocation) => getDashboard(route, "id") as string;
const handlePageChange = async (options: { page: number; size: number }) => {
const handlePageChange = (options: { page: number; size: number }) => {
if (pageNumber.value === options.page && pageSize.value === options.size) return;
pageNumber.value = options.page;
pageSize.value = options.size;
getData(dashboardID(route));
return getData(dashboardID(route));
};
watch(route, async (changed) => getData(dashboardID(changed)));
function refresh() {
return getData(dashboardID(route));
}
onMounted(async () => getData(dashboardID(route)));
defineExpose({
refresh
});
watch(() => route.params.filters, () => {
refresh();
}, {deep: true, immediate: true});
</script>

View File

@@ -12,13 +12,13 @@
</template>
<script lang="ts" setup>
import {PropType, computed} from "vue";
import {PropType, computed, watch} from "vue";
import NoData from "../../layout/NoData.vue";
import {Bar} from "vue-chartjs";
import type {Chart} from "../composables/useDashboards";
import {Chart, getDashboard} from "../composables/useDashboards";
import {useChartGenerator} from "../composables/useDashboards";
@@ -264,7 +264,19 @@
: yDatasetData,
};
});
const {data: generated} = useChartGenerator(props);
const {data: generated, generate} = useChartGenerator(props);
function refresh() {
return generate(getDashboard(route, "id")!);
}
defineExpose({
refresh
});
watch(() => route.params.filters, () => {
refresh();
}, {deep: true});
</script>
<style lang="scss" scoped>
@@ -278,4 +290,4 @@
min-height: var(--chart-height);
max-height: var(--chart-height);
}
</style>
</style>

View File

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

View File

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

View File

@@ -58,7 +58,7 @@
</template>
<template v-if="showStatChart()" #top>
<Sections :dashboard="{id: 'default'}" :charts show-default />
<Sections ref="dashboardComponent" :dashboard="{id: 'default'}" :charts show-default />
</template>
<template #table>
@@ -260,7 +260,7 @@
class-name="shrink"
>
<template #default="scope">
<code>{{ scope.row.flowRevision }}</code>
<code class="code-text">{{ scope.row.flowRevision }}</code>
</template>
</el-table-column>
@@ -293,7 +293,7 @@
</el-tooltip>
</template>
<template #default="scope">
<code>
<code class="code-text">
{{ scope.row.taskRunList?.slice(-1)[0].taskId }}
{{
scope.row.taskRunList?.slice(-1)[0].attempts?.length > 1 ? `(${scope.row.taskRunList?.slice(-1)[0].attempts.length})` : ""
@@ -771,6 +771,7 @@
},
refresh() {
this.recomputeInterval = !this.recomputeInterval;
this.$refs.dashboardComponent.refreshCharts();
this.load();
},
selectionMapper(execution) {
@@ -1122,6 +1123,9 @@
color: #ffb703;
}
}
.code-text {
color: var(--ks-content-primary);
}
</style>
<style lang="scss">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -28,6 +28,7 @@
import {TaskIcon} from "@kestra-io/ui-libs";
import {usePluginsStore} from "../../stores/plugins";
import {mapStores} from "pinia";
import Utils from "../../utils/utils";
export default {
props: {
@@ -57,16 +58,19 @@
return split[split.length - 1].substr(0, 1).toUpperCase();
},
copyLink(trigger) {
async copyLink(trigger) {
if (trigger?.type === "io.kestra.plugin.core.trigger.Webhook" && this.flow) {
const url = new URL(window.location.href).origin + `/api/v1/${this.$route.params.tenant ? this.$route.params.tenant +"/" : ""}executions/webhook/${this.flow.namespace}/${this.flow.id}/${trigger.key}`;
navigator.clipboard.writeText(url).then(() => {
try {
await Utils.copy(url);
this.$message({
message: this.$t("webhook link copied"),
type: "success"
});
});
} catch (error) {
console.error(error);
}
}
}
},

View File

@@ -1,6 +1,6 @@
<template>
<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") }}
</el-button>
<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() {
await this.executionsStore.loadFlowForExecution({
flowId: this.flowId,
namespace: this.namespace
namespace: this.namespace,
store: true
});
},
reset() {

View File

@@ -94,11 +94,12 @@
},
inheritAttrs: false,
mixins: [Task],
emits: ["update:modelValue"],
emits: ["update:modelValue", "update:selectedSchema"],
data() {
return {
isOpen: false,
selectedSchema: undefined,
delayedSelectedSchema: undefined,
finishedMounting: false,
};
},
@@ -150,10 +151,41 @@
}
this.onAnyOfInput(this.modelValue || {type: val});
},
selectedSchema(val) {
this.$emit("update:selectedSchema", val);
this.$nextTick(() => {
this.delayedSelectedSchema = val;
});
},
},
methods: {
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;
// Set up default values
if (
@@ -172,20 +204,7 @@
}
this.onInput(defaultValues)
}
// 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);
}
this.delayedSelectedSchema = value;
},
onAnyOfInput(value) {
if(this.constantType?.length && typeof value === "object") {
@@ -233,7 +252,7 @@
}) : [];
},
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);
},
schemaByType() {
@@ -243,7 +262,7 @@
}, {});
},
currentSchemaType() {
return this.selectedSchema ? getTaskComponent(this.currentSchema) : undefined;
return this.delayedSelectedSchema ? getTaskComponent(this.currentSchema) : undefined;
},
isSelectingPlugins() {
return this.schemas.length > 4;

View File

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

View File

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

View File

@@ -92,7 +92,6 @@
import {TabFocus} from "monaco-editor/esm/vs/editor/browser/config/tabFocus.js";
import MonacoEditor from "./MonacoEditor.vue";
import type * as monaco from "monaco-editor/esm/vs/editor/editor.api";
import {nextTick} from "process";
const {t} = useI18n()
@@ -568,11 +567,11 @@
const showWidgetContent = ref(false)
function addContentWidget(widget: {
async function addContentWidget(widget: {
id: string;
position: monaco.IPosition;
height: number
marginLeft: number
right: string
}) {
if(!isCodeEditor(editor)) return
if(!monacoEditor.value) return
@@ -591,16 +590,32 @@
},
getDomNode: () => {
const content = widgetNode.querySelector(".editor-content-widget-content") as HTMLDivElement;
widgetNode.style.marginLeft = widget.marginLeft / 2.2 + "rem";
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) {
@@ -637,9 +652,10 @@
display: flex;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
padding: 0 4rem;
.el-button-group {
display: inline-flex;
}
}
:not(.namespace-defaults, .el-drawer__body) > .ks-editor {

View File

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

View File

@@ -1,5 +1,5 @@
<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>
<script setup lang="ts">
@@ -14,5 +14,8 @@
<style lang="scss" scoped>
.toggle{
margin-right: 1rem;
&.is-active ::v-deep(.el-switch__label){
color: white;
}
}
</style>

View File

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

View File

@@ -14,19 +14,32 @@
@expand-subflow="onExpandSubflow"
@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>
</template>
<script lang="ts" setup>
import {computed, ref} from "vue";
import {useI18n} from "vue-i18n";
import {useStore} from "vuex";
import {Utils} from "@kestra-io/ui-libs";
import LowCodeEditor from "./LowCodeEditor.vue";
const store = useStore();
const {t} = useI18n();
const flowYaml = computed(() => store.state.flow.flowYaml);
const flowGraph = computed(() => store.state.flow.flowGraph);
const invalidGraph = computed(() => store.state.flow.invalidGraph);
const flowId = computed(() => store.state.flow.id);
const namespace = computed(() => store.state.flow.namespace);
const expandedSubflows = computed<string[]>(() => store.state.flow.expandedSubflows);
@@ -88,4 +101,8 @@
:deep(.vue-flow__panel.bottom) {
bottom: 2rem !important;
}
.invalid-graph {
margin: 1rem;
width: auto;
}
</style>

View File

@@ -429,7 +429,7 @@
codeEditor.removeContentWidget(datePickerWidget);
}
watch(suggestWidget, (newVal) => {
watch(suggestWidget, async (newVal) => {
const asCodeEditor = editorResolved.value?.getEditorType() === EditorType.ICodeEditor ? editorResolved.value as editor.ICodeEditor : undefined;
if (newVal !== undefined) {
@@ -481,7 +481,7 @@
};
}
asCodeEditor.addContentWidget(datePickerWidget);
await asCodeEditor.addContentWidget(datePickerWidget);
datePicker.value!.handleOpen();
setTimeout(() => {
datePicker.value!.focus();
@@ -662,11 +662,11 @@
showClasses: false,
showWords: false
},
...(isInFlowEditor && {
...(isInFlowEditor ? {
padding: {
top: 28
top: 16
}
}),
} : {}),
...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
v-model="kv.namespace"
:readonly="kv.update"
data-type="flow"
:include-system-namespace="true"
all
/>
</el-form-item>

View File

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

View File

@@ -15,7 +15,7 @@
</template>
<template v-if="showStatChart()" #top>
<Sections :charts :dashboard="{id: 'default', charts: []}" show-default />
<Sections ref="dashboard" :charts :dashboard="{id: 'default', charts: []}" show-default />
</template>
<template #table v-if="logsStore.logs !== undefined && logsStore.logs.length > 0">
@@ -188,6 +188,7 @@
},
refresh() {
this.lastRefreshDate = new Date();
this.$refs.dashboard.refreshCharts();
this.load();
},
loadQuery(base) {

View File

@@ -123,6 +123,7 @@
import {apiUrl} from "override/utils/route";
import Utils from "../../utils/utils";
import LogUtils from "../../utils/logs.js";
import throttle from "lodash/throttle";
export default {
name: "TaskRunDetails",
@@ -208,7 +209,9 @@
selectedLogLevel: undefined,
childrenLogIndicesByLevelByChildUid: {},
logsScrollerRefs: {},
subflowTaskRunDetailsRefs: {}
subflowTaskRunDetailsRefs: {},
throttledExecutionUpdate: undefined,
targetExecution: undefined
};
},
watch: {
@@ -236,14 +239,6 @@
},
immediate: true
},
"followedExecution.id": {
handler: function (executionId, oldExecutionId) {
if (executionId && executionId !== oldExecutionId) {
this.followExecution(executionId);
}
},
immediate: true
},
followedExecution: {
handler: async function (newExecution, oldExecution) {
if (!newExecution) {
@@ -265,15 +260,15 @@
{
namespace: newExecution.namespace,
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
setTimeout(() => {
this.closeExecutionSSE()
this.closeLogsSSE()
}, 2000);
@@ -301,13 +296,21 @@
}
},
mounted() {
this.throttledExecutionUpdate = throttle((executionEvent) => {
this.targetExecution = JSON.parse(executionEvent.data);
}, 500);
if (this.targetExecutionId) {
this.followExecution(this.targetExecutionId);
}
this.autoExpandBasedOnSettings();
},
computed: {
...mapState("auth", ["user"]),
...mapStores(useCoreStore, useExecutionsStore),
followedExecution() {
return this.executionsStore.execution;
return this.targetExecutionId === undefined ? this.executionsStore.execution : this.targetExecution;
},
Download() {
return Download
@@ -346,7 +349,7 @@
return _groupBy(indexedLogs, indexedLog => this.attemptUid(indexedLog.taskRunId, indexedLog.attemptNumber));
},
autoExpandTaskrunStates() {
autoExpandTaskRunStates() {
switch (localStorage.getItem("logDisplay") || logDisplayTypes.DEFAULT) {
case logDisplayTypes.ERROR:
return [State.FAILED, State.RUNNING, State.PAUSED]
@@ -411,9 +414,6 @@
});
this.logFileSizeByPath[path] = Utils.humanFileSize(axiosResponse.data.size);
},
closeExecutionSSE() {
this.executionsStore.closeSSE();
},
closeLogsSSE() {
if (this.logsSSE) {
this.logsSSE.close();
@@ -428,7 +428,7 @@
}
},
autoExpandBasedOnSettings() {
if (this.autoExpandTaskrunStates.length === 0) {
if (this.autoExpandTaskRunStates.length === 0) {
return;
}
@@ -441,7 +441,7 @@
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]));
}
});
@@ -458,10 +458,30 @@
this.logsWithIndexByAttemptUid[this.attemptUid(taskRun.id, this.selectedAttemptNumberByTaskRunId[taskRun.id])])) &&
this.showLogs
},
closeTargetExecutionSSE() {
if (this.executionSSE) {
this.executionSSE.close();
this.executionSSE = undefined;
}
},
followExecution(executionId) {
this.closeExecutionSSE();
this.closeTargetExecutionSSE();
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) {
this.executionsStore
@@ -550,7 +570,7 @@
return `${taskRunId}-${attemptNumber}`
},
scrollToBottomFailedTask() {
if (this.autoExpandTaskrunStates.includes(this.followedExecution.state.current)) {
if (this.autoExpandTaskRunStates.includes(this.followedExecution?.state?.current)) {
this.currentTaskRuns.forEach((taskRun) => {
if (taskRun.state.current === State.FAILED || taskRun.state.current === State.RUNNING) {
const attemptNumber = taskRun.attempts ? taskRun.attempts.length - 1 : (this.forcedAttemptNumber ?? 0)
@@ -634,7 +654,6 @@
}
},
beforeUnmount() {
this.closeExecutionSSE();
this.closeLogsSSE()
},
};
@@ -707,4 +726,4 @@
}
}
}
</style>
</style>

View File

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

View File

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

View File

@@ -91,9 +91,8 @@
<namespace-select
v-model="secret.namespace"
:readonly="secret.update"
data-type="flow"
:include-system-namespace="true"
:all="true"
all
/>
</el-form-item>
<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}) {
if(!taskCode) {
if(!taskCode || playgroundStore.dropdownOpened) {
return
}
@@ -79,16 +79,20 @@ export default function useFlowEditorRunTaskButton(isCurrentTabFlow: Ref<boolean
id: `task-hovered-${taskCode.taskId}`,
position: {
lineNumber: taskCode.start,
column: taskCode.longestLineLength + 1
column: 0
},
height: (taskCode.end - taskCode.start) + 1,
marginLeft: (taskCode.longestLineLength - taskCode.firstLineLength),
height: Math.max(taskCode.end - taskCode.start + 1, 1),
right: "1rem",
});
}
const highlightedTaskId = ref<string | undefined>(undefined);
watch(hoveredTaskProperties, (res) => {
if (playgroundStore.dropdownOpened) {
return;
}
if(!res || !playgroundStore.enabled || !isCurrentTabFlow.value) {
highlightedLines.value = undefined;
editorRefElement.value?.clearHighlights();
@@ -127,4 +131,4 @@ export default function useFlowEditorRunTaskButton(isCurrentTabFlow: Ref<boolean
playgroundStore,
highlightedLines,
}
}
}

View File

@@ -64,6 +64,7 @@ export class FlowAutoCompletion extends YamlAutoCompletion {
"randomInt(lower=${1:0}, upper=${2:10})",
"randomPort()",
"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;
}, 500);
const followExecution = (options: { id: string }, translate: (itn: string) => string) => {
closeSSE();
const followExecution = (options: { id: string, rawSSE?: boolean }, translate: (itn: string) => string) => {
if (!options.rawSSE) {
execution.value = undefined;
closeSSE();
}
const serverSentEventSource = new EventSource(`${apiUrl(store)}/executions/${options.id}/follow`, {withCredentials: true});
if (options.rawSSE) {
return Promise.resolve(serverSentEventSource);
}
sse.value = serverSentEventSource;
serverSentEventSource.onmessage = (executionEvent) => {
const isEnd = executionEvent && executionEvent.lastEventId === "end";
if (isEnd) {
closeSSE();
}
// we are receiving a first "fake" event to force initializing the connection: ignoring it
if (executionEvent.lastEventId !== "start") {
throttledExecutionUpdate(executionEvent);
}
if (isEnd) {
closeSSE();
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}` : "";
return store.$http.get(`${apiUrl(store)}/executions/flows/${options.namespace}/${options.flowId}${revision}`)
.then(response => {
flow.value = response.data;
if (options.store) {
flow.value = response.data;
}
return response.data;
});
}
@@ -738,4 +744,4 @@ export const useExecutionsStore = defineStore("executions", () => {
appendFollowedLogs,
getFlowExecutions,
};
});
});

View File

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

View File

@@ -24,6 +24,11 @@ export default {
addKvModalVisible: false
},
actions: {
async autocomplete({dispatch}, options) {
return (await dispatch("search", {
q: options.q
})).results.map(({id}) => id);
},
search({commit}, options) {
const shouldCommit = options.commit !== false;
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 {Execution, useExecutionsStore} from "./executions";
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 {
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[]>([])
function addExecution(execution: ExecutionWithGraph, graph: VueFlowUtils.FlowGraph) {
execution.graph = graph
@@ -32,24 +57,27 @@ export const usePlaygroundStore = defineStore("playground", () => {
function clearExecutions() {
executions.value = [];
executionsStore.execution = undefined;
}
const store = useStore();
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
// to the previous execution's revision,
// we can skip them and start the execution at the current task using replayExecution()
if (taskId && executions.value.length && 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({
executionId: executions.value[0].id,
taskRunId: taskIdToTaskRunIdMap[taskId],
breakpoints: nextTasksIds,
taskRunId: taskIdToTaskRunIdMap.get(taskId),
revision: store.state.flow.flow.revision,
breakpoints,
});
}
@@ -64,7 +92,7 @@ export const usePlaygroundStore = defineStore("playground", () => {
namespace: store.state.flow.flow?.namespace,
formData: defaultInputValues,
kind: "PLAYGROUND",
breakpoints: nextTasksIds,
breakpoints,
})
}
@@ -85,14 +113,86 @@ export const usePlaygroundStore = defineStore("playground", () => {
return {nextTasksIds, graph};
}
async function runUntilTask(taskId?: string) {
await store.dispatch("flow/saveAll")
const latestExecution = computed(() => executions.value[0]);
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,
// 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;
addExecution(execution, graph);
@@ -103,7 +203,7 @@ export const usePlaygroundStore = defineStore("playground", () => {
if(execution.taskRunList){
for(const taskRun of execution.taskRunList) {
// map taskId to taskRunId for later use in replayExecution()
taskIdToTaskRunIdMap[taskRun.taskId] = taskRun.id;
taskIdToTaskRunIdMap.set(taskRun.taskId, taskRun.id);
}
}
if (index !== -1) {
@@ -120,11 +220,17 @@ export const usePlaygroundStore = defineStore("playground", () => {
}
})
const dropdownOpened = ref<boolean>(false);
return {
enabled,
dropdownOpened,
readyToStart,
executions,
latestExecution: computed(() => executions.value[0]),
latestExecution,
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)) {
this.editorPlugin = undefined;
this.currentlyLoading = undefined;

View File

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

View File

@@ -29,7 +29,18 @@
:deep(.code-block) {
background-color: var(--ks-background-card);
border: 1px solid var(--ks-border-primary)
border: 1px solid var(--ks-border-primary);
padding: 0.75rem;
.line {
font-size: 0.75rem;
}
.language, .copy {
position: absolute;
top: 0.75rem;
right: 0.75rem;
}
}
:deep(.language) {
@@ -54,7 +65,7 @@
color: var(--ks-content-link);
}
[id$="-body"] span {
[id$="-body"]:not(#examples-body) span {
font-size: 1rem !important;
}

View File

@@ -28,11 +28,14 @@
}
&.el-button--playground {
#{--el-button-disabled-text-color}: $base-blue-50;
#{--el-button-text-color}: $base-white;
#{--el-button-hover-text-color}: $base-white;
#{--el-button-bg-color}: $base-blue-500;
#{--el-button-hover-bg-color}: $base-blue-400;
#{--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 {

View File

@@ -1016,10 +1016,14 @@
"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.",
"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",
"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_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",
"toggle": "Spielplatz"
},
@@ -1317,10 +1321,15 @@
"system_namespace_description": "Automatisieren Sie Wartungsaufgaben, von Fehlerwarnungen bis hin zu automatisierten Bereinigungen.",
"tags": "Tags",
"task": "Task",
"task failed": "Task fehlgeschlagen",
"task id": "Task-ID",
"task id already exists": "Task-Id existiert bereits",
"task is running": "Task läuft",
"task logs": "Task Logs",
"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",
"taskRunners": "Task Runners",
"task_id_exists": "Task-ID existiert bereits",
@@ -1356,6 +1365,8 @@
"topology": "Topologie",
"topology-graph": {
"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-in": "Vergrößern",
"zoom-out": "Verkleinern",

View File

@@ -275,7 +275,9 @@
"zoom-in": "Zoom in",
"zoom-out": "Zoom out",
"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 condition": "Show task condition",
@@ -1455,11 +1457,15 @@
},
"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",
"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",
"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",
"to toggle": "to toggle",
@@ -1467,6 +1473,11 @@
"reject": "Reject",
"last modified": "Last modified",
"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>",
"no_file_choosen": "No file chosen"
}

View File

@@ -1016,10 +1016,14 @@
"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.",
"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",
"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_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",
"toggle": "Área de pruebas"
},
@@ -1317,10 +1321,15 @@
"system_namespace_description": "Automatiza tareas de mantenimiento, desde alertas de fallo hasta limpiezas automatizadas.",
"tags": "Etiquetas",
"task": "Tarea",
"task failed": "Tarea FAILED",
"task id": "Task ID",
"task id already exists": "Task Id ya existe",
"task is running": "La tarea está RUNNING",
"task logs": "Task logs",
"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",
"taskRunners": "Ejecutores de Task",
"task_id_exists": "El Id de Task ya existe",
@@ -1356,6 +1365,8 @@
"topology": "Topología",
"topology-graph": {
"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-in": "Acercar",
"zoom-out": "Alejar",

View File

@@ -1016,10 +1016,14 @@
"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.",
"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",
"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_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",
"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.",
"tags": "Tags",
"task": "Tâche",
"task failed": "Échec de la task",
"task id": "ID de tâche",
"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 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",
"taskRunners": "Exécuteurs de Task",
"task_id_exists": "L'ID de la tâche existe déjà",
@@ -1356,6 +1365,8 @@
"topology": "Topologie",
"topology-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-in": "Zoomer",
"zoom-out": "Dézoomer",

View File

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

View File

@@ -1016,10 +1016,14 @@
"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.",
"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",
"play_icon_info": "Puoi anche fare clic sull'icona Play nelle viste No-Code o Topology.",
"run_all_tasks": "Esegui Tutti i 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",
"toggle": "Playground"
},
@@ -1317,10 +1321,15 @@
"system_namespace_description": "Automatizza le attività di manutenzione, dagli avvisi di errore alle pulizie automatiche.",
"tags": "Tag",
"task": "Task",
"task failed": "Attività FAILED",
"task id": "Task ID",
"task id already exists": "Task Id già esistente",
"task is running": "Il task è in esecuzione",
"task logs": "Task logs",
"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",
"taskRunners": "Runner di Task",
"task_id_exists": "L'ID del task esiste già",
@@ -1356,6 +1365,8 @@
"topology": "Topologia",
"topology-graph": {
"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-in": "Zoom avanti",
"zoom-out": "Zoom indietro",

View File

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

View File

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

View File

@@ -1016,10 +1016,14 @@
"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.",
"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ń",
"play_icon_info": "Możesz również kliknąć ikonę Play w widokach No-Code lub Topology.",
"run_all_tasks": "Uruchom Wszystkie Taski",
"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",
"toggle": "Plac zabaw"
},
@@ -1317,10 +1321,15 @@
"system_namespace_description": "Automatyzuj zadania konserwacyjne, od alertów o awariach po automatyczne czyszczenie.",
"tags": "Tagi",
"task": "Task",
"task failed": "Task failed",
"task id": "Task ID",
"task id already exists": "Task Id już istnieje",
"task is running": "Task jest w trakcie RUNNING",
"task logs": "Task logs",
"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",
"taskRunners": "Task Runners",
"task_id_exists": "Id zadania już istnieje",
@@ -1356,6 +1365,8 @@
"topology": "Topologia",
"topology-graph": {
"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-in": "Powiększ",
"zoom-out": "Pomniejsz",

View File

@@ -1016,10 +1016,14 @@
"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.",
"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",
"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_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",
"toggle": "Playground"
},
@@ -1317,10 +1321,15 @@
"system_namespace_description": "Automatize tarefas de manutenção, desde alertas de falha até limpezas automatizadas.",
"tags": "Tags",
"task": "Task",
"task failed": "Tarefa falhou",
"task id": "Task ID",
"task id already exists": "Task Id já existe",
"task is running": "A task está RUNNING",
"task logs": "Task logs",
"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",
"taskRunners": "Executores de Task",
"task_id_exists": "Id da Task já existe",
@@ -1356,6 +1365,8 @@
"topology": "Topologia",
"topology-graph": {
"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-in": "Aumentar zoom",
"zoom-out": "Diminuir zoom",

View File

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

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