Compare commits

...

54 Commits

Author SHA1 Message Date
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
84 changed files with 1116 additions and 583 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

@@ -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

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

View File

@@ -8,6 +8,7 @@ import io.kestra.core.events.CrudEventType;
import io.kestra.core.exceptions.DeserializationException;
import io.kestra.core.exceptions.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

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

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

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

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -44,6 +44,7 @@ import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.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

@@ -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

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

View File

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

View File

@@ -1,6 +1,6 @@
version=0.24.0-SNAPSHOT
version=0.24.0-rc2-SNAPSHOT
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

@@ -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");

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

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

View File

@@ -92,6 +92,20 @@ export function defaultConfig(override, theme) {
);
}
export function extractState(value) {
if (!value || typeof value !== "string") return value;
if (value.includes(",")) {
const stateNames = State.arrayAllStates().map(state => state.name);
const matchedState = value.split(",")
.map(part => part.trim())
.find(part => stateNames.includes(part.toUpperCase()));
return matchedState || value;
}
return value;
}
export function chartClick(moment, router, route, event, parsedData, elements, type = "label") {
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

@@ -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

@@ -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})` : ""
@@ -1122,6 +1122,9 @@
color: #ffb703;
}
}
.code-text {
color: var(--ks-content-primary);
}
</style>
<style lang="scss">

View File

@@ -45,7 +45,7 @@
</el-tooltip>
</el-form-item>
<el-form-item>
<el-button-group class="min-w-auto">
<el-button-group class="ks-b-group">
<restart :execution="executionsStore.execution" class="ms-0" @follow="forwardEvent('follow', $event)" />
<el-button @click="downloadContent()">
<kicon :tooltip="$t('download logs')">
@@ -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

@@ -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

@@ -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()">

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();
@@ -288,4 +281,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

@@ -153,12 +153,16 @@
<input
:data-testid="`input-form-${input.id}`"
:id="input.id+'-file'"
class="el-input__inner custom-file-input"
class="el-input__inner"
type="file"
@change="onFileChange(input, $event)"
autocomplete="off"
:style="{display: isFile(inputsValues[input.id]) ? 'none': ''}"
>
<span class="file-placeholder" v-html="getFilePlaceholder(inputsValues[input.id])" />
<label
v-if="isFile(inputsValues[input.id])"
:for="input.id+'-file'"
>Kestra Internal Storage File</label>
</div>
</div>
<div
@@ -414,11 +418,11 @@
}
},
onChange(input) {
// give 2 seconds for the user to finish their edit
// give a second for the user to finish their edit
// and for the server to return with validated content
setTimeout(() => {
this.inputsValidated.add(input.id);
}, 2000);
}, 300);
this.$emit("update:modelValue", this.inputsValues);
},
onSubmit() {
@@ -577,15 +581,9 @@
this.updateArrayValue(input);
},
getFilePlaceholder(value) {
if (typeof value === "string" && value.startsWith("nsfile://")) {
return this.$t("defaultsToNamespaceFile", {name: value.substring(10)});
}
if (value && typeof value.name === "string") {
return value.name;
}
return this.$t("no_file_choosen");
},
isFile(data) {
return typeof data === "string" && (data.startsWith("kestra:///") || data.startsWith("file://") || data.startsWith("nsfile://"));
}
},
watch: {
flow () {
@@ -762,19 +760,4 @@
overflow-x: hidden;
}
}
.custom-file-input {
color: transparent;
width: 120px;
}
.custom-file-input::-webkit-file-upload-text {
visibility: hidden;
}
.file-placeholder {
margin-left: 8px;
color: var(--ks-content-secondary);
font-size: 0.9em;
}
</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

@@ -102,6 +102,7 @@
: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

@@ -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

@@ -119,37 +119,31 @@
(ns) => namespaces.includes(ns.code) || this.isFilter,
);
},
load() {
this.$store
.dispatch("namespace/loadNamespacesForDatatype", {
async load() {
try {
await 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)
})
let namespaces = [...this.datatypeNamespaces];
if (this.all) {
const allNamespaces = await this.$store.dispatch("namespace/autocomplete", {
q: this.value || "",
ids: [],
apiUrl: undefined
});
namespaces = [...new Set([...namespaces, ...allNamespaces])];
}
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

@@ -93,7 +93,7 @@
: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">

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

@@ -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

@@ -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": "Уменьшить",

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": "您还可以在无代码或拓扑视图中点击播放图标。",
"run_all_tasks": "运行所有Tasks",
"run_task": "运行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 failed": "任务失败",
"task id": "任务ID",
"task id already exists": "任务ID已存在",
"task is running": "任务正在运行",
"task logs": "任务日志",
"task run id": "任务运行 ID",
"task sent a warning": "任务发送了警告",
"task was skipped": "任务被跳过",
"task was successful": "任务成功",
"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

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

View File

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

View File

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

View File

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