mirror of
https://github.com/kestra-io/kestra.git
synced 2025-12-25 11:12:12 -05:00
Compare commits
72 Commits
docs/purge
...
v0.24.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f58bc4caba | ||
|
|
e99ae9513f | ||
|
|
c8b51fcacf | ||
|
|
813b2f6439 | ||
|
|
c6b5bca25b | ||
|
|
de35d2cdb9 | ||
|
|
a6ffbd59d0 | ||
|
|
568740a214 | ||
|
|
aa0d2c545f | ||
|
|
cda77d5146 | ||
|
|
d4fd1f61ba | ||
|
|
9859ea5eb6 | ||
|
|
aca374a28f | ||
|
|
c413ba95e1 | ||
|
|
9c6b92619e | ||
|
|
8173e8df51 | ||
|
|
5c95505911 | ||
|
|
33f0b533bb | ||
|
|
23e35a7f97 | ||
|
|
0357321c58 | ||
|
|
5c08403398 | ||
|
|
a63cb71218 | ||
|
|
317885b91c | ||
|
|
87637302e4 | ||
|
|
056faaaf9f | ||
|
|
54c74a1328 | ||
|
|
fae0c88c5e | ||
|
|
db5d83d1cb | ||
|
|
066b947762 | ||
|
|
b6597475b1 | ||
|
|
f2610baf15 | ||
|
|
b619bf76d8 | ||
|
|
117f453a77 | ||
|
|
053d6276ff | ||
|
|
3870eca70b | ||
|
|
afd7c216f9 | ||
|
|
59a17e88e7 | ||
|
|
99f8dca1c2 | ||
|
|
1068c9fe51 | ||
|
|
ea6d30df7c | ||
|
|
04ba7363c2 | ||
|
|
281a987944 | ||
|
|
c9ce54b0be | ||
|
|
ccd9baef3c | ||
|
|
97869b9c75 | ||
|
|
1c681c1492 | ||
|
|
de2a446f93 | ||
|
|
d778947017 | ||
|
|
3f97845fdd | ||
|
|
631cd169a1 | ||
|
|
1648fa076c | ||
|
|
474806882e | ||
|
|
65467bd118 | ||
|
|
387bbb80ac | ||
|
|
19d4c64f19 | ||
|
|
809c0a228c | ||
|
|
6a045900fb | ||
|
|
4ada5fe8f3 | ||
|
|
998087ca30 | ||
|
|
146338e48f | ||
|
|
de177b925e | ||
|
|
04bfb19095 | ||
|
|
c913c48785 | ||
|
|
0d5b593d42 | ||
|
|
83f92535c5 | ||
|
|
fd6a0a6c11 | ||
|
|
104c4c97b4 | ||
|
|
21cd21269f | ||
|
|
679befa2fe | ||
|
|
8a0ecdeb8a | ||
|
|
ee8762e138 | ||
|
|
d16324f265 |
39
.github/workflows/docker.yml
vendored
39
.github/workflows/docker.yml
vendored
@@ -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
|
||||
|
||||
3
.github/workflows/main.yml
vendored
3
.github/workflows/main.yml
vendored
@@ -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:
|
||||
|
||||
74
.github/workflows/workflow-build-artifacts.yml
vendored
74
.github/workflows/workflow-build-artifacts.yml
vendored
@@ -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
|
||||
|
||||
10
.github/workflows/workflow-github-release.yml
vendored
10
.github/workflows/workflow-github-release.yml
vendored
@@ -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:
|
||||
|
||||
@@ -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
|
||||
# ********************************************************************************************************************
|
||||
|
||||
11
.github/workflows/workflow-release.yml
vendored
11
.github/workflows/workflow-release.yml
vendored
@@ -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 }}
|
||||
5
.plugins
5
.plugins
@@ -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
|
||||
|
||||
@@ -122,12 +122,13 @@ public class JsonSchemaGenerator {
|
||||
if (jsonNode instanceof ObjectNode clazzSchema && clazzSchema.get("required") instanceof ArrayNode requiredPropsNode && clazzSchema.get("properties") instanceof ObjectNode properties) {
|
||||
List<String> requiredFieldValues = StreamSupport.stream(requiredPropsNode.spliterator(), false)
|
||||
.map(JsonNode::asText)
|
||||
.toList();
|
||||
.collect(Collectors.toList());
|
||||
|
||||
properties.fields().forEachRemaining(e -> {
|
||||
int indexInRequiredArray = requiredFieldValues.indexOf(e.getKey());
|
||||
if (indexInRequiredArray != -1 && e.getValue() instanceof ObjectNode valueNode && valueNode.has("default")) {
|
||||
requiredPropsNode.remove(indexInRequiredArray);
|
||||
requiredFieldValues.remove(indexInRequiredArray);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package io.kestra.core.queues;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
public class UnsupportedMessageException extends QueueException {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public UnsupportedMessageException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -161,7 +161,7 @@ public interface ExecutionRepositoryInterface extends SaveRepositoryInterface<Ex
|
||||
}
|
||||
|
||||
List<Execution> lastExecutions(
|
||||
@Nullable String tenantId,
|
||||
String tenantId,
|
||||
@Nullable List<FlowFilter> flows
|
||||
);
|
||||
}
|
||||
|
||||
@@ -764,6 +764,7 @@ public class Worker implements Service, Runnable, AutoCloseable {
|
||||
workerTask = workerTask.withTaskRun(workerTask.getTaskRun().withState(state));
|
||||
|
||||
WorkerTaskResult workerTaskResult = new WorkerTaskResult(workerTask.getTaskRun(), dynamicTaskRuns);
|
||||
|
||||
this.workerTaskResultQueue.emit(workerTaskResult);
|
||||
|
||||
// upload the cache file, hash may not be present if we didn't succeed in computing it
|
||||
@@ -796,6 +797,10 @@ public class Worker implements Service, Runnable, AutoCloseable {
|
||||
// If it's a message too big, we remove the outputs
|
||||
failed = failed.withOutputs(Variables.empty());
|
||||
}
|
||||
if (e instanceof UnsupportedMessageException) {
|
||||
// we expect the offending char is in the output so we remove it
|
||||
failed = failed.withOutputs(Variables.empty());
|
||||
}
|
||||
WorkerTaskResult workerTaskResult = new WorkerTaskResult(failed);
|
||||
RunContextLogger contextLogger = runContextLoggerFactory.create(workerTask);
|
||||
contextLogger.logger().error("Unable to emit the worker task result to the queue: {}", e.getMessage(), e);
|
||||
@@ -818,7 +823,11 @@ public class Worker implements Service, Runnable, AutoCloseable {
|
||||
private Optional<String> hashTask(RunContext runContext, Task task) {
|
||||
try {
|
||||
var map = JacksonMapper.toMap(task);
|
||||
var rMap = runContext.render(map);
|
||||
// If there are task provided variables, rendering the task may fail.
|
||||
// The best we can do is to add a fake 'workingDir' as it's an often added variables,
|
||||
// and it should not be part of the task hash.
|
||||
Map<String, Object> variables = Map.of("workingDir", "workingDir");
|
||||
var rMap = runContext.render(map, variables);
|
||||
var json = JacksonMapper.ofJson().writeValueAsBytes(rMap);
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
digest.update(json);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,11 +9,11 @@ import io.kestra.core.runners.DefaultRunContext;
|
||||
import io.kestra.core.runners.RunContext;
|
||||
import io.kestra.core.services.FlowService;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import org.codehaus.commons.nullanalysis.NotNull;
|
||||
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
|
||||
@@ -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 |
@@ -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 |
@@ -112,7 +112,7 @@ class JsonSchemaGeneratorTest {
|
||||
|
||||
var requiredWithDefault = definitions.get("io.kestra.core.docs.JsonSchemaGeneratorTest-RequiredWithDefault");
|
||||
assertThat(requiredWithDefault, is(notNullValue()));
|
||||
assertThat((List<String>) requiredWithDefault.get("required"), not(contains("requiredWithDefault")));
|
||||
assertThat((List<String>) requiredWithDefault.get("required"), not(containsInAnyOrder("requiredWithDefault", "anotherRequiredWithDefault")));
|
||||
|
||||
var properties = (Map<String, Map<String, Object>>) flow.get("properties");
|
||||
var listeners = properties.get("listeners");
|
||||
@@ -253,7 +253,7 @@ class JsonSchemaGeneratorTest {
|
||||
void requiredAreRemovedIfThereIsADefault() {
|
||||
Map<String, Object> generate = jsonSchemaGenerator.properties(Task.class, RequiredWithDefault.class);
|
||||
assertThat(generate, is(not(nullValue())));
|
||||
assertThat((List<String>) generate.get("required"), not(containsInAnyOrder("requiredWithDefault")));
|
||||
assertThat((List<String>) generate.get("required"), not(containsInAnyOrder("requiredWithDefault", "anotherRequiredWithDefault")));
|
||||
assertThat((List<String>) generate.get("required"), containsInAnyOrder("requiredWithNoDefault"));
|
||||
}
|
||||
|
||||
@@ -466,6 +466,11 @@ class JsonSchemaGeneratorTest {
|
||||
@Builder.Default
|
||||
private Property<TaskWithEnum.TestClass> requiredWithDefault = Property.ofValue(TaskWithEnum.TestClass.builder().testProperty("test").build());
|
||||
|
||||
@PluginProperty
|
||||
@NotNull
|
||||
@Builder.Default
|
||||
private Property<TaskWithEnum.TestClass> anotherRequiredWithDefault = Property.ofValue(TaskWithEnum.TestClass.builder().testProperty("test2").build());
|
||||
|
||||
@PluginProperty
|
||||
@NotNull
|
||||
private Property<TaskWithEnum.TestClass> requiredWithNoDefault;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import io.kestra.core.junit.annotations.LoadFlows;
|
||||
import io.kestra.core.models.annotations.Plugin;
|
||||
import io.kestra.core.models.executions.Execution;
|
||||
import io.kestra.core.models.flows.State;
|
||||
import io.kestra.core.models.property.Property;
|
||||
import io.kestra.core.models.tasks.RunnableTask;
|
||||
import io.kestra.core.models.tasks.Task;
|
||||
import io.kestra.core.queues.QueueException;
|
||||
@@ -18,6 +19,7 @@ import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
@@ -77,8 +79,12 @@ public class TaskCacheTest {
|
||||
@Plugin
|
||||
public static class CounterTask extends Task implements RunnableTask<CounterTask.Output> {
|
||||
|
||||
private String workingDir;
|
||||
|
||||
@Override
|
||||
public Output run(RunContext runContext) throws Exception {
|
||||
Map<String, Object> variables = Map.of("workingDir", runContext.workingDir().path().toString());
|
||||
runContext.render(this.workingDir, variables);
|
||||
return Output.builder()
|
||||
.counter(COUNTER.incrementAndGet())
|
||||
.build();
|
||||
|
||||
@@ -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."
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace: io.kestra.tests
|
||||
tasks:
|
||||
- id: cache
|
||||
type: io.kestra.core.runners.TaskCacheTest$CounterTask
|
||||
workingDir: "{{workingDir}}"
|
||||
taskCache:
|
||||
enabled: true
|
||||
ttl: PT1S
|
||||
@@ -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 }}"
|
||||
@@ -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
|
||||
@@ -1,6 +1,6 @@
|
||||
version=0.24.0-SNAPSHOT
|
||||
version=0.24.1
|
||||
|
||||
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||
org.gradle.parallel=true
|
||||
org.gradle.caching=true
|
||||
org.gradle.priority=low
|
||||
org.gradle.priority=low
|
||||
|
||||
@@ -4,6 +4,7 @@ import io.kestra.core.models.executions.TaskRun;
|
||||
import io.kestra.core.models.executions.Variables;
|
||||
import io.kestra.core.models.flows.State;
|
||||
import io.kestra.core.queues.QueueException;
|
||||
import io.kestra.core.queues.UnsupportedMessageException;
|
||||
import io.kestra.core.runners.WorkerTaskResult;
|
||||
import io.kestra.core.utils.IdUtils;
|
||||
import io.kestra.jdbc.runner.JdbcQueueTest;
|
||||
@@ -31,7 +32,8 @@ class PostgresQueueTest extends JdbcQueueTest {
|
||||
.build();
|
||||
|
||||
var exception = assertThrows(QueueException.class, () -> workerTaskResultQueue.emit(workerTaskResult));
|
||||
assertThat(exception.getMessage()).isEqualTo("Unable to emit a message to the queue");
|
||||
assertThat(exception).isInstanceOf(UnsupportedMessageException.class);
|
||||
assertThat(exception.getMessage()).contains("ERROR: unsupported Unicode escape sequence");
|
||||
assertThat(exception.getCause()).isInstanceOf(DataException.class);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -7,16 +7,13 @@ import com.google.common.collect.Iterables;
|
||||
import io.kestra.core.exceptions.DeserializationException;
|
||||
import io.kestra.core.metrics.MetricRegistry;
|
||||
import io.kestra.core.models.executions.Execution;
|
||||
import io.kestra.core.queues.QueueException;
|
||||
import io.kestra.core.queues.QueueInterface;
|
||||
import io.kestra.core.queues.QueueService;
|
||||
import io.kestra.core.queues.*;
|
||||
import io.kestra.core.utils.Either;
|
||||
import io.kestra.core.utils.ExecutorsUtils;
|
||||
import io.kestra.core.utils.IdUtils;
|
||||
import io.kestra.jdbc.JdbcTableConfigs;
|
||||
import io.kestra.jdbc.JdbcMapper;
|
||||
import io.kestra.jdbc.JooqDSLContextWrapper;
|
||||
import io.kestra.core.queues.MessageTooBigException;
|
||||
import io.kestra.jdbc.repository.AbstractJdbcRepository;
|
||||
import io.micrometer.core.instrument.Counter;
|
||||
import io.micrometer.core.instrument.Timer;
|
||||
@@ -151,6 +148,11 @@ public abstract class JdbcQueue<T> implements QueueInterface<T> {
|
||||
.execute();
|
||||
});
|
||||
} catch (DataException e) { // The exception is from the data itself, not the database/network/driver so instead of fail fast, we throw a recoverable QueueException
|
||||
// Postgres refuses to store JSONB with the '\0000' codepoint as it has no textual representation.
|
||||
// We try to detect that and fail with a specific exception so the Worker can recover from it.
|
||||
if (e.getMessage() != null && e.getMessage().contains("ERROR: unsupported Unicode escape sequence")) {
|
||||
throw new UnsupportedMessageException(e.getMessage(), e);
|
||||
}
|
||||
throw new QueueException("Unable to emit a message to the queue", e);
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,10 @@ public abstract class AbstractTaskRunnerTest {
|
||||
@Test
|
||||
protected void run() throws Exception {
|
||||
var runContext = runContext(this.runContextFactory);
|
||||
simpleRun(runContext);
|
||||
}
|
||||
|
||||
private void simpleRun(RunContext runContext) throws Exception {
|
||||
var commands = initScriptCommands(runContext);
|
||||
Mockito.when(commands.getCommands()).thenReturn(
|
||||
Property.ofValue(ScriptService.scriptCommands(List.of("/bin/sh", "-c"), Collections.emptyList(), List.of("echo 'Hello World'")))
|
||||
@@ -166,6 +170,13 @@ public abstract class AbstractTaskRunnerTest {
|
||||
assertThat(taskException.getLogConsumer().getOutputs().get("logOutput")).isEqualTo("Hello World");
|
||||
}
|
||||
|
||||
@Test
|
||||
protected void canWorkMultipleTimeInSameWdir() throws Exception {
|
||||
var runContext = runContext(this.runContextFactory);
|
||||
simpleRun(runContext);
|
||||
simpleRun(runContext);
|
||||
}
|
||||
|
||||
protected RunContext runContext(RunContextFactory runContextFactory) {
|
||||
return this.runContext(runContextFactory, null);
|
||||
}
|
||||
@@ -236,4 +247,4 @@ public abstract class AbstractTaskRunnerTest {
|
||||
protected boolean needsToSpecifyWorkingDirectory() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
14
ui/package-lock.json
generated
14
ui/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -14,11 +14,11 @@
|
||||
/>
|
||||
</section>
|
||||
|
||||
<Sections :key :dashboard :charts :show-default="dashboard.id === 'default'" :padding="padding" />
|
||||
<Sections ref="dashboardComponent" :dashboard :charts :show-default="dashboard.id === 'default'" :padding="padding" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, onBeforeMount, ref} from "vue";
|
||||
import {computed, onBeforeMount, ref, useTemplateRef} from "vue";
|
||||
|
||||
import type {Dashboard, Chart} from "./composables/useDashboards";
|
||||
import {ALLOWED_CREATION_ROUTES, getDashboard, processFlowYaml} from "./composables/useDashboards";
|
||||
@@ -43,8 +43,6 @@
|
||||
import YAML_FLOW from "./assets/default_flow_definition.yaml?raw";
|
||||
import YAML_NAMESPACE from "./assets/default_namespace_definition.yaml?raw";
|
||||
|
||||
import UTILS from "../../utils/utils.js";
|
||||
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -65,21 +63,18 @@
|
||||
const dashboard = ref<Dashboard>({id: "", charts: []});
|
||||
const charts = ref<Chart[]>([]);
|
||||
|
||||
// We use a key to force re-rendering of the Sections component
|
||||
let key = ref(UTILS.uid());
|
||||
|
||||
const loadCharts = async (allCharts: Chart[] = []) => {
|
||||
charts.value = [];
|
||||
|
||||
for (const chart of allCharts) {
|
||||
charts.value.push({...chart, content: stringify(chart)});
|
||||
}
|
||||
|
||||
refreshCharts()
|
||||
};
|
||||
|
||||
const dashboardComponent = useTemplateRef("dashboardComponent");
|
||||
|
||||
const refreshCharts = () => {
|
||||
key.value = UTILS.uid();
|
||||
dashboardComponent.value!.refreshCharts();
|
||||
};
|
||||
|
||||
const load = async (id = "default", defaultYAML = YAML_MAIN) => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -11,12 +11,12 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {PropType, computed} from "vue";
|
||||
import {PropType, computed, watch} from "vue";
|
||||
import moment from "moment";
|
||||
import {Bar} from "vue-chartjs";
|
||||
|
||||
import NoData from "../../layout/NoData.vue";
|
||||
import type {Chart} from "../composables/useDashboards";
|
||||
import {Chart, getDashboard} from "../composables/useDashboards";
|
||||
import {useChartGenerator} from "../composables/useDashboards";
|
||||
|
||||
|
||||
@@ -159,7 +159,19 @@
|
||||
return {labels, datasets};
|
||||
});
|
||||
|
||||
const {data: generated} = useChartGenerator(props);
|
||||
const {data: generated, generate} = useChartGenerator(props);
|
||||
|
||||
function refresh() {
|
||||
return generate(getDashboard(route, "id")!);
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
refresh
|
||||
});
|
||||
|
||||
watch(() => route.params.filters, () => {
|
||||
refresh();
|
||||
}, {deep: true});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -182,4 +194,4 @@
|
||||
min-height: var(--chart-height);
|
||||
max-height: var(--chart-height);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -10,12 +10,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {PropType} from "vue";
|
||||
import {PropType, watch} from "vue";
|
||||
|
||||
import type {Chart} from "../composables/useDashboards";
|
||||
import {Chart, getDashboard} from "../composables/useDashboards";
|
||||
import {getChartTitle, getPropertyValue, useChartGenerator} from "../composables/useDashboards";
|
||||
|
||||
import NoData from "../../layout/NoData.vue";
|
||||
import {useRoute} from "vue-router";
|
||||
|
||||
const props = defineProps({
|
||||
chart: {type: Object as PropType<Chart>, required: true},
|
||||
@@ -23,7 +24,21 @@
|
||||
showDefault: {type: Boolean, default: false},
|
||||
});
|
||||
|
||||
const {percentageShown, EMPTY_TEXT, data} = useChartGenerator(props);
|
||||
const route = useRoute();
|
||||
|
||||
const {percentageShown, EMPTY_TEXT, data, generate} = useChartGenerator(props);
|
||||
|
||||
function refresh() {
|
||||
return generate(getDashboard(route, "id")!);
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
refresh
|
||||
});
|
||||
|
||||
watch(() => route.params.filters, () => {
|
||||
refresh();
|
||||
}, {deep: true});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {PropType, onMounted, watch, ref} from "vue";
|
||||
import {PropType, watch, ref} from "vue";
|
||||
|
||||
import type {RouteLocation} from "vue-router";
|
||||
|
||||
@@ -34,9 +34,17 @@
|
||||
else data.value = props.chart.content ?? props.chart.source?.content;
|
||||
};
|
||||
|
||||
const dashboardID = (route: RouteLocation) => getDashboard(route, "id") || "default"
|
||||
const dashboardID = (route: RouteLocation) => getDashboard(route, "id")!;
|
||||
|
||||
watch(route, async (changed) => await getData(dashboardID(changed)));
|
||||
function refresh() {
|
||||
return getData(dashboardID(route));
|
||||
}
|
||||
|
||||
onMounted(async () => await getData(dashboardID(route)));
|
||||
defineExpose({
|
||||
refresh
|
||||
});
|
||||
|
||||
watch(() => route.params.filters, () => {
|
||||
refresh();
|
||||
}, {deep: true, immediate: true});
|
||||
</script>
|
||||
|
||||
@@ -22,9 +22,9 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {computed,PropType} from "vue";
|
||||
import {computed, PropType, watch} from "vue";
|
||||
|
||||
import type {Chart} from "../composables/useDashboards";
|
||||
import {Chart, getDashboard} from "../composables/useDashboards";
|
||||
import {useChartGenerator} from "../composables/useDashboards";
|
||||
|
||||
|
||||
@@ -183,7 +183,19 @@
|
||||
};
|
||||
});
|
||||
|
||||
const {data: generated} = useChartGenerator(props);
|
||||
const {data: generated, generate} = useChartGenerator(props);
|
||||
|
||||
function refresh() {
|
||||
return generate(getDashboard(route, "id")!);
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
refresh
|
||||
});
|
||||
|
||||
watch(() => route.params.filters, () => {
|
||||
refresh();
|
||||
}, {deep: true});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -192,4 +204,4 @@
|
||||
.chart {
|
||||
max-height: $height;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
|
||||
<div class="flex-grow-1">
|
||||
<component
|
||||
ref="chartsComponents"
|
||||
:is="TYPES[chart.type as keyof typeof TYPES]"
|
||||
:chart
|
||||
:filters
|
||||
@@ -89,6 +90,18 @@
|
||||
import Download from "vue-material-design-icons/Download.vue";
|
||||
import Pencil from "vue-material-design-icons/Pencil.vue";
|
||||
|
||||
const chartsComponents = ref<{refresh(): void}[]>();
|
||||
|
||||
function refreshCharts() {
|
||||
chartsComponents.value!.forEach((component) => {
|
||||
component.refresh();
|
||||
});
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
refreshCharts
|
||||
});
|
||||
|
||||
const props = defineProps<{
|
||||
dashboard: Dashboard;
|
||||
charts?: Chart[];
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {PropType, onMounted, watch, ref, computed} from "vue";
|
||||
import {PropType, watch, ref, computed} from "vue";
|
||||
|
||||
import type {RouteLocation} from "vue-router";
|
||||
|
||||
@@ -116,16 +116,24 @@
|
||||
|
||||
const dashboardID = (route: RouteLocation) => getDashboard(route, "id") as string;
|
||||
|
||||
const handlePageChange = async (options: { page: number; size: number }) => {
|
||||
const handlePageChange = (options: { page: number; size: number }) => {
|
||||
if (pageNumber.value === options.page && pageSize.value === options.size) return;
|
||||
|
||||
pageNumber.value = options.page;
|
||||
pageSize.value = options.size;
|
||||
|
||||
getData(dashboardID(route));
|
||||
return getData(dashboardID(route));
|
||||
};
|
||||
|
||||
watch(route, async (changed) => getData(dashboardID(changed)));
|
||||
function refresh() {
|
||||
return getData(dashboardID(route));
|
||||
}
|
||||
|
||||
onMounted(async () => getData(dashboardID(route)));
|
||||
defineExpose({
|
||||
refresh
|
||||
});
|
||||
|
||||
watch(() => route.params.filters, () => {
|
||||
refresh();
|
||||
}, {deep: true, immediate: true});
|
||||
</script>
|
||||
|
||||
@@ -12,13 +12,13 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {PropType, computed} from "vue";
|
||||
import {PropType, computed, watch} from "vue";
|
||||
|
||||
import NoData from "../../layout/NoData.vue";
|
||||
|
||||
import {Bar} from "vue-chartjs";
|
||||
|
||||
import type {Chart} from "../composables/useDashboards";
|
||||
import {Chart, getDashboard} from "../composables/useDashboards";
|
||||
import {useChartGenerator} from "../composables/useDashboards";
|
||||
|
||||
|
||||
@@ -264,7 +264,19 @@
|
||||
: yDatasetData,
|
||||
};
|
||||
});
|
||||
const {data: generated} = useChartGenerator(props);
|
||||
const {data: generated, generate} = useChartGenerator(props);
|
||||
|
||||
function refresh() {
|
||||
return generate(getDashboard(route, "id")!);
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
refresh
|
||||
});
|
||||
|
||||
watch(() => route.params.filters, () => {
|
||||
refresh();
|
||||
}, {deep: true});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -278,4 +290,4 @@
|
||||
min-height: var(--chart-height);
|
||||
max-height: var(--chart-height);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
</template>
|
||||
|
||||
<template v-if="showStatChart()" #top>
|
||||
<Sections :dashboard="{id: 'default'}" :charts show-default />
|
||||
<Sections ref="dashboardComponent" :dashboard="{id: 'default'}" :charts show-default />
|
||||
</template>
|
||||
|
||||
<template #table>
|
||||
@@ -260,7 +260,7 @@
|
||||
class-name="shrink"
|
||||
>
|
||||
<template #default="scope">
|
||||
<code>{{ scope.row.flowRevision }}</code>
|
||||
<code class="code-text">{{ scope.row.flowRevision }}</code>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
@@ -293,7 +293,7 @@
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<template #default="scope">
|
||||
<code>
|
||||
<code class="code-text">
|
||||
{{ scope.row.taskRunList?.slice(-1)[0].taskId }}
|
||||
{{
|
||||
scope.row.taskRunList?.slice(-1)[0].attempts?.length > 1 ? `(${scope.row.taskRunList?.slice(-1)[0].attempts.length})` : ""
|
||||
@@ -771,6 +771,7 @@
|
||||
},
|
||||
refresh() {
|
||||
this.recomputeInterval = !this.recomputeInterval;
|
||||
this.$refs.dashboardComponent.refreshCharts();
|
||||
this.load();
|
||||
},
|
||||
selectionMapper(execution) {
|
||||
@@ -1122,6 +1123,9 @@
|
||||
color: #ffb703;
|
||||
}
|
||||
}
|
||||
.code-text {
|
||||
color: var(--ks-content-primary);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -45,8 +45,8 @@
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button-group class="min-w-auto">
|
||||
<restart :execution="executionsStore.execution" class="ms-0" @follow="forwardEvent('follow', $event)" />
|
||||
<el-button-group class="ks-b-group">
|
||||
<restart v-if="executionsStore.execution" :execution="executionsStore.execution" class="ms-0" @follow="forwardEvent('follow', $event)" />
|
||||
<el-button @click="downloadContent()">
|
||||
<kicon :tooltip="$t('download logs')">
|
||||
<download />
|
||||
@@ -60,7 +60,7 @@
|
||||
</el-button-group>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button-group class="min-w-auto">
|
||||
<el-button-group class="ks-b-group">
|
||||
<el-button @click="loadLogs()">
|
||||
<kicon :tooltip="$t('refresh')">
|
||||
<refresh />
|
||||
@@ -361,4 +361,9 @@
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.ks-b-group {
|
||||
min-width: auto!important;
|
||||
max-width: max-content !important;
|
||||
}
|
||||
</style>
|
||||
@@ -102,7 +102,8 @@
|
||||
loadDefinition() {
|
||||
this.executionsStore.loadFlowForExecution({
|
||||
flowId: this.execution.flowId,
|
||||
namespace: this.execution.namespace
|
||||
namespace: this.execution.namespace,
|
||||
store: true
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -428,7 +428,8 @@
|
||||
),
|
||||
loading: false,
|
||||
lastExecutionByFlowReady: false,
|
||||
latestExecutions: []
|
||||
latestExecutions: [],
|
||||
dblClickRouteName: "flows/update"
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
import {TaskIcon} from "@kestra-io/ui-libs";
|
||||
import {usePluginsStore} from "../../stores/plugins";
|
||||
import {mapStores} from "pinia";
|
||||
import Utils from "../../utils/utils";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
@@ -57,16 +58,19 @@
|
||||
|
||||
return split[split.length - 1].substr(0, 1).toUpperCase();
|
||||
},
|
||||
copyLink(trigger) {
|
||||
async copyLink(trigger) {
|
||||
if (trigger?.type === "io.kestra.plugin.core.trigger.Webhook" && this.flow) {
|
||||
const url = new URL(window.location.href).origin + `/api/v1/${this.$route.params.tenant ? this.$route.params.tenant +"/" : ""}executions/webhook/${this.flow.namespace}/${this.flow.id}/${trigger.key}`;
|
||||
|
||||
navigator.clipboard.writeText(url).then(() => {
|
||||
try {
|
||||
await Utils.copy(url);
|
||||
this.$message({
|
||||
message: this.$t("webhook link copied"),
|
||||
type: "success"
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="trigger-flow-wrapper">
|
||||
<el-button v-if="playgroundStore.enabled" id="run-all-button" :icon="icon.Play" class="el-button--playground" :disabled="isDisabled()" @click="playgroundStore.runUntilTask()">
|
||||
<el-button v-if="playgroundStore.enabled" id="run-all-button" :icon="icon.Play" class="el-button--playground" :disabled="isDisabled() || !playgroundStore.readyToStart" @click="playgroundStore.runUntilTask()">
|
||||
{{ $t("playground.run_all_tasks") }}
|
||||
</el-button>
|
||||
<el-button v-else id="execute-button" :class="{'onboarding-glow': coreStore.guidedProperties.tourStarted}" :icon="icon.Flash" :type="type" :disabled="isDisabled()" @click="onClick()">
|
||||
@@ -153,7 +153,8 @@
|
||||
async loadDefinition() {
|
||||
await this.executionsStore.loadFlowForExecution({
|
||||
flowId: this.flowId,
|
||||
namespace: this.namespace
|
||||
namespace: this.namespace,
|
||||
store: true
|
||||
});
|
||||
},
|
||||
reset() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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") => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -30,12 +30,7 @@
|
||||
<ContentSave v-if="!isCurrentTabFlow" @click="saveFileContent" />
|
||||
</template>
|
||||
<template v-if="playgroundStore.enabled" #widget-content>
|
||||
<el-button
|
||||
class="el-button--playground"
|
||||
@click="playgroundStore.runUntilTask(highlightedLines?.taskId)"
|
||||
>
|
||||
{{ t('playground.run_task') }}
|
||||
</el-button>
|
||||
<PlaygroundRunTaskButton :task-id="highlightedLines?.taskId" />
|
||||
</template>
|
||||
</editor>
|
||||
<transition name="el-zoom-in-center">
|
||||
@@ -58,15 +53,12 @@
|
||||
<script lang="ts" setup>
|
||||
import {computed, onActivated, onMounted, ref, provide, onBeforeUnmount} from "vue";
|
||||
import {useStore} from "vuex";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import Editor from "./Editor.vue";
|
||||
|
||||
import ContentSave from "vue-material-design-icons/ContentSave.vue";
|
||||
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
|
||||
const {t} = useI18n();
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
@@ -79,6 +71,7 @@
|
||||
import AcceptDecline from "./AcceptDecline.vue";
|
||||
import * as YAML_UTILS from "@kestra-io/ui-libs/flow-yaml-utils";
|
||||
import useFlowEditorRunTaskButton from "../../composables/playground/useFlowEditorRunTaskButton";
|
||||
import PlaygroundRunTaskButton from "./PlaygroundRunTaskButton.vue";
|
||||
|
||||
const store = useStore();
|
||||
const miscStore = useMiscStore();
|
||||
@@ -147,6 +140,7 @@
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("keydown", handleGlobalSave);
|
||||
window.removeEventListener("keydown", toggleAiShortcut);
|
||||
pluginsStore.editorPlugin = undefined;
|
||||
});
|
||||
|
||||
const editorRefElement = ref<InstanceType<typeof Editor>>();
|
||||
@@ -288,4 +282,4 @@
|
||||
.actions {
|
||||
bottom: 10%;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
42
ui/src/components/inputs/PlaygroundRunTaskButton.vue
Normal file
42
ui/src/components/inputs/PlaygroundRunTaskButton.vue
Normal 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>
|
||||
@@ -100,8 +100,8 @@
|
||||
<namespace-select
|
||||
v-model="kv.namespace"
|
||||
:readonly="kv.update"
|
||||
data-type="flow"
|
||||
:include-system-namespace="true"
|
||||
all
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
|
||||
@@ -105,6 +105,7 @@
|
||||
"level",
|
||||
"index",
|
||||
"attemptNumber",
|
||||
"executionKind"
|
||||
];
|
||||
excludes.push.apply(excludes, this.excludeMetas);
|
||||
for (const key in this.log) {
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
</template>
|
||||
|
||||
<template v-if="showStatChart()" #top>
|
||||
<Sections :charts :dashboard="{id: 'default', charts: []}" show-default />
|
||||
<Sections ref="dashboard" :charts :dashboard="{id: 'default', charts: []}" show-default />
|
||||
</template>
|
||||
|
||||
<template #table v-if="logsStore.logs !== undefined && logsStore.logs.length > 0">
|
||||
@@ -188,6 +188,7 @@
|
||||
},
|
||||
refresh() {
|
||||
this.lastRefreshDate = new Date();
|
||||
this.$refs.dashboard.refreshCharts();
|
||||
this.load();
|
||||
},
|
||||
loadQuery(base) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
<el-select
|
||||
class="fit-text"
|
||||
:model-value="value"
|
||||
@update:model-value="onInput"
|
||||
@update:model-value="$emit('update:modelValue', $event)"
|
||||
:disabled="readonly"
|
||||
clearable
|
||||
:placeholder="$t('Select namespace')"
|
||||
:persistent="false"
|
||||
remote
|
||||
:remote-method="onInput"
|
||||
filterable
|
||||
:allow-create="allowCreate"
|
||||
default-first-option
|
||||
@@ -25,14 +27,12 @@
|
||||
import {mapStores} from "pinia";
|
||||
import {useMiscStore} from "../../../stores/misc";
|
||||
import _uniqBy from "lodash/uniqBy";
|
||||
import permission from "../../../models/permission";
|
||||
import action from "../../../models/action";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
dataType: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: undefined,
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
@@ -60,19 +60,7 @@
|
||||
}
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
created() {
|
||||
if (
|
||||
this.user &&
|
||||
this.user.hasAnyActionOnAnyNamespace(
|
||||
permission.NAMESPACE,
|
||||
action.READ,
|
||||
)
|
||||
) {
|
||||
this.load();
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState("namespace", ["datatypeNamespaces"]),
|
||||
...mapState("auth", ["user"]),
|
||||
...mapStores(useMiscStore),
|
||||
},
|
||||
@@ -85,8 +73,7 @@
|
||||
methods: {
|
||||
onInput(value) {
|
||||
this.$emit("update:modelValue", value);
|
||||
this.localNamespaceInput = value;
|
||||
this.load();
|
||||
this.load(value);
|
||||
},
|
||||
groupNamespaces(namespaces) {
|
||||
let res = [];
|
||||
@@ -119,37 +106,29 @@
|
||||
(ns) => namespaces.includes(ns.code) || this.isFilter,
|
||||
);
|
||||
},
|
||||
load() {
|
||||
this.$store
|
||||
.dispatch("namespace/loadNamespacesForDatatype", {
|
||||
dataType: this.dataType
|
||||
})
|
||||
.then(() => {
|
||||
this.groupedNamespaces = this.groupNamespaces(
|
||||
this.datatypeNamespaces
|
||||
).filter(
|
||||
(namespace) =>
|
||||
this.includeSystemNamespace ||
|
||||
namespace.code !==
|
||||
(this.miscStore.configs?.systemNamespace || "system")
|
||||
);
|
||||
});
|
||||
if (this.all) {
|
||||
// Then include datatype namespaces + all from namespaces tables
|
||||
this.$store.dispatch("namespace/autocomplete" + (this.value ? "?q=" + this.value : "")).then(namespaces => {
|
||||
const concatNamespaces = this.groupedNamespaces.concat(this.groupNamespaces(
|
||||
namespaces
|
||||
).filter(
|
||||
(namespace) =>
|
||||
this.includeSystemNamespace ||
|
||||
namespace.code !==
|
||||
(this.miscStore.configs?.systemNamespace || "system")
|
||||
));
|
||||
// Remove duplicates after merge
|
||||
this.groupedNamespaces = _uniqBy(concatNamespaces, "code").filter(
|
||||
(ns) => namespaces.includes(ns.code) || this.isFilter,
|
||||
).sort((a,b) => a.code > b.code)
|
||||
})
|
||||
async load(value) {
|
||||
try {
|
||||
let namespaces;
|
||||
if (this.all) {
|
||||
namespaces = await this.$store.dispatch("namespace/autocomplete", {
|
||||
q: value || "",
|
||||
ids: [],
|
||||
apiUrl: undefined
|
||||
});
|
||||
} else {
|
||||
namespaces = await this.$store.dispatch("namespace/loadNamespacesForDatatype", {
|
||||
dataType: this.dataType
|
||||
});
|
||||
}
|
||||
|
||||
this.groupedNamespaces = this.groupNamespaces(namespaces)
|
||||
.filter(namespace =>
|
||||
this.includeSystemNamespace ||
|
||||
namespace.code !== (this.miscStore.configs?.systemNamespace || "system")
|
||||
)
|
||||
.sort((a, b) => a.code.localeCompare(b.code));
|
||||
} catch (error) {
|
||||
console.error("Error loading namespaces:", error);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -91,9 +91,8 @@
|
||||
<namespace-select
|
||||
v-model="secret.namespace"
|
||||
:readonly="secret.update"
|
||||
data-type="flow"
|
||||
:include-system-namespace="true"
|
||||
:all="true"
|
||||
all
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('secret.key')" prop="key">
|
||||
@@ -469,4 +468,4 @@
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'})",
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -24,6 +24,11 @@ export default {
|
||||
addKvModalVisible: false
|
||||
},
|
||||
actions: {
|
||||
async autocomplete({dispatch}, options) {
|
||||
return (await dispatch("search", {
|
||||
q: options.q
|
||||
})).results.map(({id}) => id);
|
||||
},
|
||||
search({commit}, options) {
|
||||
const shouldCommit = options.commit !== false;
|
||||
delete options.commit;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -29,7 +29,18 @@
|
||||
|
||||
:deep(.code-block) {
|
||||
background-color: var(--ks-background-card);
|
||||
border: 1px solid var(--ks-border-primary)
|
||||
border: 1px solid var(--ks-border-primary);
|
||||
padding: 0.75rem;
|
||||
|
||||
.line {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.language, .copy {
|
||||
position: absolute;
|
||||
top: 0.75rem;
|
||||
right: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.language) {
|
||||
@@ -54,7 +65,7 @@
|
||||
color: var(--ks-content-link);
|
||||
}
|
||||
|
||||
[id$="-body"] span {
|
||||
[id$="-body"]:not(#examples-body) span {
|
||||
font-size: 1rem !important;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "ज़ूम आउट करें",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "ズームアウト",
|
||||
|
||||
@@ -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": "축소",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1016,10 +1016,14 @@
|
||||
"pause done": "Выполнение приостановлено",
|
||||
"pause title": "Приостановить выполнение <code>{id}</code>.<br/>Обратите внимание, что текущие RUNNING задачи все равно будут обрабатываться, и выполнение придется возобновить вручную.",
|
||||
"playground": {
|
||||
"empty": "Нажмите кнопку \"Run task\", чтобы протестировать ваш workflow с помощью симулированного выполнения.",
|
||||
"confirm_create": "Вы не можете запустить playground во время создания flow. Запуск playground создаст flow.",
|
||||
"history": "Последние 10 запусков",
|
||||
"play_icon_info": "Вы также можете нажать на значок Play в представлениях No-Code или Topology.",
|
||||
"run_all_tasks": "Запустить все Tasks",
|
||||
"run_task": "Запустить Task",
|
||||
"run_task_and_downstream": "Запустить task и downstream",
|
||||
"run_task_info": "Наведите курсор на любую task в редакторе Flow Code и нажмите кнопку \"Run task\", чтобы протестировать вашу task.",
|
||||
"run_this_task": "Запустить этот task",
|
||||
"title": "Песочница",
|
||||
"toggle": "Песочница"
|
||||
},
|
||||
@@ -1317,10 +1321,15 @@
|
||||
"system_namespace_description": "Автоматизируйте задачи обслуживания, от предупреждений о сбоях до автоматической очистки.",
|
||||
"tags": "Теги",
|
||||
"task": "Задача",
|
||||
"task failed": "Задача FAILED",
|
||||
"task id": "ID задачи",
|
||||
"task id already exists": "ID задачи уже существует",
|
||||
"task is running": "Задача RUNNING",
|
||||
"task logs": "Журналы задач",
|
||||
"task run id": "ID выполнения задачи",
|
||||
"task sent a warning": "Задача отправила предупреждение",
|
||||
"task was skipped": "Задача была пропущена",
|
||||
"task was successful": "Задача была успешной",
|
||||
"taskDefaults": "Задача по умолчанию",
|
||||
"taskRunners": "Запускатели Task",
|
||||
"task_id_exists": "Id задачи уже существует",
|
||||
@@ -1356,6 +1365,8 @@
|
||||
"topology": "Топология",
|
||||
"topology-graph": {
|
||||
"graph-orientation": "Ориентация графика",
|
||||
"invalid": "Ошибка графа",
|
||||
"invalid_description": "Произошла ошибка при загрузке графа. Пожалуйста, проверьте исходный код на наличие ошибок.",
|
||||
"zoom-fit": "Подогнать",
|
||||
"zoom-in": "Увеличить",
|
||||
"zoom-out": "Уменьшить",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user