mirror of
https://github.com/kestra-io/kestra.git
synced 2025-12-25 11:12:12 -05:00
Compare commits
86 Commits
feat/execu
...
v0.24.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
208b244f0f | ||
|
|
b93976091d | ||
|
|
eec52d76f0 | ||
|
|
b96fd87572 | ||
|
|
1aa5bfab43 | ||
|
|
c4572e86a5 | ||
|
|
f2f97bb70c | ||
|
|
804c740d3c | ||
|
|
75cd4f44e0 | ||
|
|
f167a2a2bb | ||
|
|
08d9416e3a | ||
|
|
2a879c617c | ||
|
|
3227ca7c11 | ||
|
|
428a52ce02 | ||
|
|
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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1040,6 +1040,16 @@ public class Execution implements DeletedInterface, TenantInterface {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all children of this {@link TaskRun}.
|
||||
*/
|
||||
public List<TaskRun> findChildren(TaskRun parentTaskRun) {
|
||||
return taskRunList.stream()
|
||||
.filter(taskRun -> parentTaskRun.getId().equals(taskRun.getParentTaskRunId()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
|
||||
public List<String> findParentsValues(TaskRun taskRun, boolean withCurrent) {
|
||||
return (withCurrent ?
|
||||
Stream.concat(findParents(taskRun).stream(), Stream.of(taskRun)) :
|
||||
|
||||
@@ -116,7 +116,7 @@ public class State {
|
||||
}
|
||||
|
||||
public Instant maxDate() {
|
||||
if (this.histories.size() == 0) {
|
||||
if (this.histories.isEmpty()) {
|
||||
return Instant.now();
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ public class State {
|
||||
}
|
||||
|
||||
public Instant minDate() {
|
||||
if (this.histories.size() == 0) {
|
||||
if (this.histories.isEmpty()) {
|
||||
return Instant.now();
|
||||
}
|
||||
|
||||
@@ -173,6 +173,11 @@ public class State {
|
||||
return this.current.isBreakpoint();
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public boolean isQueued() {
|
||||
return this.current.isQueued();
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public boolean isRetrying() {
|
||||
return this.current.isRetrying();
|
||||
@@ -206,6 +211,14 @@ public class State {
|
||||
return this.histories.get(this.histories.size() - 2).state.isPaused();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the execution has failed, then was restarted.
|
||||
* This is to disambiguate between a RESTARTED after PAUSED and RESTARTED after FAILED state.
|
||||
*/
|
||||
public boolean failedThenRestarted() {
|
||||
return this.current == Type.RESTARTED && this.histories.get(this.histories.size() - 2).state.isFailed();
|
||||
}
|
||||
|
||||
@Introspected
|
||||
public enum Type {
|
||||
CREATED,
|
||||
@@ -264,6 +277,10 @@ public class State {
|
||||
return this == Type.KILLED;
|
||||
}
|
||||
|
||||
public boolean isQueued(){
|
||||
return this == Type.QUEUED;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return states that are terminal to an execution
|
||||
*/
|
||||
|
||||
@@ -68,6 +68,19 @@ public class Property<T> {
|
||||
String getExpression() {
|
||||
return expression;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link Property} with no cached rendered value,
|
||||
* so that the next render will evaluate its original Pebble expression.
|
||||
* <p>
|
||||
* The returned property will still cache its rendered result.
|
||||
* To re-evaluate on a subsequent render, call {@code skipCache()} again.
|
||||
*
|
||||
* @return a new {@link Property} without a pre-rendered value
|
||||
*/
|
||||
public Property<T> skipCache() {
|
||||
return Property.ofExpression(expression);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a new Property object with a value already set.<br>
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ public class Executor {
|
||||
|
||||
public Boolean canBeProcessed() {
|
||||
return !(this.getException() != null || this.getFlow() == null || this.getFlow() instanceof FlowWithException || this.getFlow().getTasks() == null ||
|
||||
this.getExecution().isDeleted() || this.getExecution().getState().isPaused() || this.getExecution().getState().isBreakpoint());
|
||||
this.getExecution().isDeleted() || this.getExecution().getState().isPaused() || this.getExecution().getState().isBreakpoint() || this.getExecution().getState().isQueued());
|
||||
}
|
||||
|
||||
public Executor withFlow(FlowWithSource flow) {
|
||||
|
||||
@@ -237,9 +237,9 @@ public class ExecutorService {
|
||||
try {
|
||||
state = flowableParent.resolveState(runContext, execution, parentTaskRun);
|
||||
} catch (Exception e) {
|
||||
// This will lead to the next task being still executed but at least Kestra will not crash.
|
||||
// This will lead to the next task being still executed, but at least Kestra will not crash.
|
||||
// This is the best we can do, Flowable task should not fail, so it's a kind of panic mode.
|
||||
runContext.logger().error("Unable to resolve state from the Flowable task: " + e.getMessage(), e);
|
||||
runContext.logger().error("Unable to resolve state from the Flowable task: {}", e.getMessage(), e);
|
||||
state = Optional.of(State.Type.FAILED);
|
||||
}
|
||||
Optional<WorkerTaskResult> endedTask = childWorkerTaskTypeToWorkerTask(
|
||||
@@ -589,6 +589,23 @@ public class ExecutorService {
|
||||
list = list.stream().filter(workerTaskResult -> !workerTaskResult.getTaskRun().getId().equals(taskRun.getParentTaskRunId()))
|
||||
.collect(Collectors.toCollection(ArrayList::new));
|
||||
}
|
||||
|
||||
// If the task is a flowable and its terminated, check that all children are terminated.
|
||||
// This may not be the case for parallel flowable tasks like Parallel, Dag, ForEach...
|
||||
// After a fail task, some child flowable may not be correctly terminated.
|
||||
if (task instanceof FlowableTask<?> && taskRun.getState().isTerminated()) {
|
||||
List<TaskRun> updated = executor.getExecution().findChildren(taskRun).stream()
|
||||
.filter(child -> !child.getState().isTerminated())
|
||||
.map(throwFunction(child -> child.withState(taskRun.getState().getCurrent())))
|
||||
.toList();
|
||||
if (!updated.isEmpty()) {
|
||||
Execution execution = executor.getExecution();
|
||||
for (TaskRun child : updated) {
|
||||
execution = execution.withTaskRun(child);
|
||||
}
|
||||
executor = executor.withExecution(execution, "handledTerminatedFlowableTasks");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
metricRegistry
|
||||
|
||||
@@ -4,15 +4,11 @@ import io.kestra.core.exceptions.IllegalVariableEvaluationException;
|
||||
import io.kestra.core.models.property.Property;
|
||||
import io.kestra.core.models.tasks.Task;
|
||||
import io.kestra.core.models.triggers.AbstractTrigger;
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
import jakarta.validation.Validator;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import static io.kestra.core.utils.Rethrow.throwFunction;
|
||||
|
||||
@@ -27,12 +23,19 @@ public class RunContextProperty<T> {
|
||||
private final RunContext runContext;
|
||||
private final Task task;
|
||||
private final AbstractTrigger trigger;
|
||||
|
||||
private final boolean skipCache;
|
||||
|
||||
RunContextProperty(Property<T> property, RunContext runContext) {
|
||||
this(property, runContext, false);
|
||||
}
|
||||
|
||||
RunContextProperty(Property<T> property, RunContext runContext, boolean skipCache) {
|
||||
this.property = property;
|
||||
this.runContext = runContext;
|
||||
this.task = ((DefaultRunContext) runContext).getTask();
|
||||
this.trigger = ((DefaultRunContext) runContext).getTrigger();
|
||||
this.skipCache = skipCache;
|
||||
}
|
||||
|
||||
private void validate() {
|
||||
@@ -45,6 +48,19 @@ public class RunContextProperty<T> {
|
||||
log.trace("Unable to do validation: no task or trigger found");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link RunContextProperty} that will always be rendered by evaluating
|
||||
* its original Pebble expression, without using any previously cached value.
|
||||
* <p>
|
||||
* This ensures that each time the property is rendered, the underlying
|
||||
* expression is re-evaluated to produce a fresh result.
|
||||
*
|
||||
* @return a new {@link Property} that bypasses the cache
|
||||
*/
|
||||
public RunContextProperty<T> skipCache() {
|
||||
return new RunContextProperty<>(this.property, this.runContext, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a property then convert it to its target type and validate it.<br>
|
||||
@@ -55,13 +71,13 @@ public class RunContextProperty<T> {
|
||||
* Warning, due to the caching mechanism, this method is not thread-safe.
|
||||
*/
|
||||
public Optional<T> as(Class<T> clazz) throws IllegalVariableEvaluationException {
|
||||
var as = Optional.ofNullable(this.property)
|
||||
var as = Optional.ofNullable(getProperty())
|
||||
.map(throwFunction(prop -> Property.as(prop, this.runContext, clazz)));
|
||||
|
||||
validate();
|
||||
return as;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Render a property with additional variables, then convert it to its target type and validate it.<br>
|
||||
*
|
||||
@@ -71,7 +87,7 @@ public class RunContextProperty<T> {
|
||||
* Warning, due to the caching mechanism, this method is not thread-safe.
|
||||
*/
|
||||
public Optional<T> as(Class<T> clazz, Map<String, Object> variables) throws IllegalVariableEvaluationException {
|
||||
var as = Optional.ofNullable(this.property)
|
||||
var as = Optional.ofNullable(getProperty())
|
||||
.map(throwFunction(prop -> Property.as(prop, this.runContext, clazz, variables)));
|
||||
|
||||
validate();
|
||||
@@ -89,7 +105,7 @@ public class RunContextProperty<T> {
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <I> T asList(Class<I> itemClazz) throws IllegalVariableEvaluationException {
|
||||
var as = Optional.ofNullable(this.property)
|
||||
var as = Optional.ofNullable(getProperty())
|
||||
.map(throwFunction(prop -> Property.asList(prop, this.runContext, itemClazz)))
|
||||
.orElse((T) Collections.emptyList());
|
||||
|
||||
@@ -108,7 +124,7 @@ public class RunContextProperty<T> {
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <I> T asList(Class<I> itemClazz, Map<String, Object> variables) throws IllegalVariableEvaluationException {
|
||||
var as = Optional.ofNullable(this.property)
|
||||
var as = Optional.ofNullable(getProperty())
|
||||
.map(throwFunction(prop -> Property.asList(prop, this.runContext, itemClazz, variables)))
|
||||
.orElse((T) Collections.emptyList());
|
||||
|
||||
@@ -127,7 +143,7 @@ public class RunContextProperty<T> {
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <K,V> T asMap(Class<K> keyClass, Class<V> valueClass) throws IllegalVariableEvaluationException {
|
||||
var as = Optional.ofNullable(this.property)
|
||||
var as = Optional.ofNullable(getProperty())
|
||||
.map(throwFunction(prop -> Property.asMap(prop, this.runContext, keyClass, valueClass)))
|
||||
.orElse((T) Collections.emptyMap());
|
||||
|
||||
@@ -146,11 +162,15 @@ public class RunContextProperty<T> {
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <K,V> T asMap(Class<K> keyClass, Class<V> valueClass, Map<String, Object> variables) throws IllegalVariableEvaluationException {
|
||||
var as = Optional.ofNullable(this.property)
|
||||
var as = Optional.ofNullable(getProperty())
|
||||
.map(throwFunction(prop -> Property.asMap(prop, this.runContext, keyClass, valueClass, variables)))
|
||||
.orElse((T) Collections.emptyMap());
|
||||
|
||||
validate();
|
||||
return as;
|
||||
}
|
||||
|
||||
private Property<T> getProperty() {
|
||||
return skipCache ? this.property.skipCache() : this.property;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package io.kestra.core.services;
|
||||
|
||||
import io.kestra.core.models.executions.Execution;
|
||||
import io.kestra.core.models.executions.ExecutionKind;
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.models.flows.FlowWithException;
|
||||
import io.kestra.core.models.flows.FlowWithSource;
|
||||
@@ -10,7 +11,6 @@ import io.kestra.core.models.triggers.multipleflows.MultipleConditionStorageInte
|
||||
import io.kestra.core.models.triggers.multipleflows.MultipleConditionWindow;
|
||||
import io.kestra.core.runners.RunContextFactory;
|
||||
import io.kestra.core.utils.ListUtils;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
@@ -24,14 +24,15 @@ import java.util.stream.Stream;
|
||||
|
||||
@Singleton
|
||||
public class FlowTriggerService {
|
||||
@Inject
|
||||
private ConditionService conditionService;
|
||||
private final ConditionService conditionService;
|
||||
private final RunContextFactory runContextFactory;
|
||||
private final FlowService flowService;
|
||||
|
||||
@Inject
|
||||
private RunContextFactory runContextFactory;
|
||||
|
||||
@Inject
|
||||
private FlowService flowService;
|
||||
public FlowTriggerService(ConditionService conditionService, RunContextFactory runContextFactory, FlowService flowService) {
|
||||
this.conditionService = conditionService;
|
||||
this.runContextFactory = runContextFactory;
|
||||
this.flowService = flowService;
|
||||
}
|
||||
|
||||
// used in EE only
|
||||
public Stream<FlowWithFlowTrigger> withFlowTriggersOnly(Stream<FlowWithSource> allFlows) {
|
||||
@@ -53,6 +54,8 @@ public class FlowTriggerService {
|
||||
List<FlowWithFlowTrigger> validTriggersBeforeMultipleConditionEval = allFlows.stream()
|
||||
// prevent recursive flow triggers
|
||||
.filter(flow -> flowService.removeUnwanted(flow, execution))
|
||||
// filter out Test Executions
|
||||
.filter(flow -> execution.getKind() == null)
|
||||
// ensure flow & triggers are enabled
|
||||
.filter(flow -> !flow.isDisabled() && !(flow instanceof FlowWithException))
|
||||
.filter(flow -> flow.getTriggers() != null && !flow.getTriggers().isEmpty())
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -90,7 +90,7 @@ public class ExecutionOutputs extends Condition implements ScheduleCondition {
|
||||
private static final String OUTPUTS_VAR = "outputs";
|
||||
|
||||
@NotNull
|
||||
private Property<String> expression;
|
||||
private Property<Boolean> expression;
|
||||
|
||||
/** {@inheritDoc} **/
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -105,9 +105,8 @@ public class ExecutionOutputs extends Condition implements ScheduleCondition {
|
||||
conditionContext.getVariables(),
|
||||
Map.of(TRIGGER_VAR, Map.of(OUTPUTS_VAR, conditionContext.getExecution().getOutputs()))
|
||||
);
|
||||
|
||||
String render = conditionContext.getRunContext().render(expression).as(String.class, variables).orElseThrow();
|
||||
return !(render.isBlank() || render.trim().equals("false"));
|
||||
|
||||
return conditionContext.getRunContext().render(expression).skipCache().as(Boolean.class, variables).orElseThrow();
|
||||
}
|
||||
|
||||
private boolean hasNoOutputs(final Execution execution) {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -387,6 +387,13 @@ public abstract class AbstractRunnerTest {
|
||||
forEachItemCaseTest.forEachItemInIf();
|
||||
}
|
||||
|
||||
@Test
|
||||
@LoadFlows({"flows/valids/for-each-item-subflow-after-execution.yaml",
|
||||
"flows/valids/for-each-item-after-execution.yaml"})
|
||||
protected void forEachItemWithAfterExecution() throws Exception {
|
||||
forEachItemCaseTest.forEachItemWithAfterExecution();
|
||||
}
|
||||
|
||||
@Test
|
||||
@LoadFlows({"flows/valids/flow-concurrency-cancel.yml"})
|
||||
void concurrencyCancel() throws Exception {
|
||||
@@ -423,6 +430,18 @@ public abstract class AbstractRunnerTest {
|
||||
flowConcurrencyCaseTest.flowConcurrencyWithForEachItem();
|
||||
}
|
||||
|
||||
@Test
|
||||
@LoadFlows({"flows/valids/flow-concurrency-queue-fail.yml"})
|
||||
void concurrencyQueueRestarted() throws Exception {
|
||||
flowConcurrencyCaseTest.flowConcurrencyQueueRestarted();
|
||||
}
|
||||
|
||||
@Test
|
||||
@LoadFlows({"flows/valids/flow-concurrency-queue-after-execution.yml"})
|
||||
void concurrencyQueueAfterExecution() throws Exception {
|
||||
flowConcurrencyCaseTest.flowConcurrencyQueueAfterExecution();
|
||||
}
|
||||
|
||||
@Test
|
||||
@ExecuteFlow("flows/valids/executable-fail.yml")
|
||||
void badExecutable(Execution execution) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import io.kestra.core.queues.QueueException;
|
||||
import io.kestra.core.queues.QueueFactoryInterface;
|
||||
import io.kestra.core.queues.QueueInterface;
|
||||
import io.kestra.core.repositories.FlowRepositoryInterface;
|
||||
import io.kestra.core.services.ExecutionService;
|
||||
import io.kestra.core.storages.StorageInterface;
|
||||
import io.kestra.core.utils.TestsUtils;
|
||||
import jakarta.inject.Inject;
|
||||
@@ -53,6 +54,9 @@ public class FlowConcurrencyCaseTest {
|
||||
@Named(QueueFactoryInterface.EXECUTION_NAMED)
|
||||
protected QueueInterface<Execution> executionQueue;
|
||||
|
||||
@Inject
|
||||
private ExecutionService executionService;
|
||||
|
||||
public void flowConcurrencyCancel() throws TimeoutException, QueueException, InterruptedException {
|
||||
Execution execution1 = runnerUtils.runOneUntilRunning(MAIN_TENANT, "io.kestra.tests", "flow-concurrency-cancel", null, null, Duration.ofSeconds(30));
|
||||
Execution execution2 = runnerUtils.runOne(MAIN_TENANT, "io.kestra.tests", "flow-concurrency-cancel");
|
||||
@@ -278,6 +282,115 @@ public class FlowConcurrencyCaseTest {
|
||||
assertThat(terminated.getState().getCurrent()).isEqualTo(Type.SUCCESS);
|
||||
}
|
||||
|
||||
public void flowConcurrencyQueueRestarted() throws Exception {
|
||||
Execution execution1 = runnerUtils.runOneUntilRunning(MAIN_TENANT, "io.kestra.tests", "flow-concurrency-queue-fail", null, null, Duration.ofSeconds(30));
|
||||
Flow flow = flowRepository
|
||||
.findById(MAIN_TENANT, "io.kestra.tests", "flow-concurrency-queue-fail", Optional.empty())
|
||||
.orElseThrow();
|
||||
Execution execution2 = Execution.newExecution(flow, null, null, Optional.empty());
|
||||
executionQueue.emit(execution2);
|
||||
|
||||
assertThat(execution1.getState().isRunning()).isTrue();
|
||||
assertThat(execution2.getState().getCurrent()).isEqualTo(State.Type.CREATED);
|
||||
|
||||
var executionResult1 = new AtomicReference<Execution>();
|
||||
var executionResult2 = new AtomicReference<Execution>();
|
||||
|
||||
CountDownLatch latch1 = new CountDownLatch(2);
|
||||
AtomicReference<Execution> failedExecution = new AtomicReference<>();
|
||||
CountDownLatch latch2 = new CountDownLatch(1);
|
||||
CountDownLatch latch3 = new CountDownLatch(1);
|
||||
|
||||
Flux<Execution> receive = TestsUtils.receive(executionQueue, e -> {
|
||||
if (e.getLeft().getId().equals(execution1.getId())) {
|
||||
executionResult1.set(e.getLeft());
|
||||
if (e.getLeft().getState().getCurrent() == Type.FAILED) {
|
||||
failedExecution.set(e.getLeft());
|
||||
latch1.countDown();
|
||||
}
|
||||
}
|
||||
|
||||
if (e.getLeft().getId().equals(execution2.getId())) {
|
||||
executionResult2.set(e.getLeft());
|
||||
if (e.getLeft().getState().getCurrent() == State.Type.RUNNING) {
|
||||
latch2.countDown();
|
||||
}
|
||||
if (e.getLeft().getState().getCurrent() == Type.FAILED) {
|
||||
latch3.countDown();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
assertTrue(latch2.await(1, TimeUnit.MINUTES));
|
||||
assertThat(failedExecution.get()).isNotNull();
|
||||
// here the first fail and the second is now running.
|
||||
// we restart the first one, it should be queued then fail again.
|
||||
Execution restarted = executionService.restart(failedExecution.get(), null);
|
||||
executionQueue.emit(restarted);
|
||||
|
||||
assertTrue(latch3.await(1, TimeUnit.MINUTES));
|
||||
assertTrue(latch1.await(1, TimeUnit.MINUTES));
|
||||
receive.blockLast();
|
||||
|
||||
assertThat(executionResult1.get().getState().getCurrent()).isEqualTo(Type.FAILED);
|
||||
// it should have been queued after restarted
|
||||
assertThat(executionResult1.get().getState().getHistories().stream().anyMatch(history -> history.getState() == Type.RESTARTED)).isTrue();
|
||||
assertThat(executionResult1.get().getState().getHistories().stream().anyMatch(history -> history.getState() == Type.QUEUED)).isTrue();
|
||||
assertThat(executionResult2.get().getState().getCurrent()).isEqualTo(Type.FAILED);
|
||||
assertThat(executionResult2.get().getState().getHistories().getFirst().getState()).isEqualTo(State.Type.CREATED);
|
||||
assertThat(executionResult2.get().getState().getHistories().get(1).getState()).isEqualTo(State.Type.QUEUED);
|
||||
assertThat(executionResult2.get().getState().getHistories().get(2).getState()).isEqualTo(State.Type.RUNNING);
|
||||
}
|
||||
|
||||
public void flowConcurrencyQueueAfterExecution() throws TimeoutException, QueueException, InterruptedException {
|
||||
Execution execution1 = runnerUtils.runOneUntilRunning(MAIN_TENANT, "io.kestra.tests", "flow-concurrency-queue-after-execution", null, null, Duration.ofSeconds(30));
|
||||
Flow flow = flowRepository
|
||||
.findById(MAIN_TENANT, "io.kestra.tests", "flow-concurrency-queue-after-execution", Optional.empty())
|
||||
.orElseThrow();
|
||||
Execution execution2 = Execution.newExecution(flow, null, null, Optional.empty());
|
||||
executionQueue.emit(execution2);
|
||||
|
||||
assertThat(execution1.getState().isRunning()).isTrue();
|
||||
assertThat(execution2.getState().getCurrent()).isEqualTo(State.Type.CREATED);
|
||||
|
||||
var executionResult1 = new AtomicReference<Execution>();
|
||||
var executionResult2 = new AtomicReference<Execution>();
|
||||
|
||||
CountDownLatch latch1 = new CountDownLatch(1);
|
||||
CountDownLatch latch2 = new CountDownLatch(1);
|
||||
CountDownLatch latch3 = new CountDownLatch(1);
|
||||
|
||||
Flux<Execution> receive = TestsUtils.receive(executionQueue, e -> {
|
||||
if (e.getLeft().getId().equals(execution1.getId())) {
|
||||
executionResult1.set(e.getLeft());
|
||||
if (e.getLeft().getState().getCurrent() == State.Type.SUCCESS) {
|
||||
latch1.countDown();
|
||||
}
|
||||
}
|
||||
|
||||
if (e.getLeft().getId().equals(execution2.getId())) {
|
||||
executionResult2.set(e.getLeft());
|
||||
if (e.getLeft().getState().getCurrent() == State.Type.RUNNING) {
|
||||
latch2.countDown();
|
||||
}
|
||||
if (e.getLeft().getState().getCurrent() == State.Type.SUCCESS) {
|
||||
latch3.countDown();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
assertTrue(latch1.await(1, TimeUnit.MINUTES));
|
||||
assertTrue(latch2.await(1, TimeUnit.MINUTES));
|
||||
assertTrue(latch3.await(1, TimeUnit.MINUTES));
|
||||
receive.blockLast();
|
||||
|
||||
assertThat(executionResult1.get().getState().getCurrent()).isEqualTo(State.Type.SUCCESS);
|
||||
assertThat(executionResult2.get().getState().getCurrent()).isEqualTo(State.Type.SUCCESS);
|
||||
assertThat(executionResult2.get().getState().getHistories().getFirst().getState()).isEqualTo(State.Type.CREATED);
|
||||
assertThat(executionResult2.get().getState().getHistories().get(1).getState()).isEqualTo(State.Type.QUEUED);
|
||||
assertThat(executionResult2.get().getState().getHistories().get(2).getState()).isEqualTo(State.Type.RUNNING);
|
||||
}
|
||||
|
||||
private URI storageUpload() throws URISyntaxException, IOException {
|
||||
File tempFile = File.createTempFile("file", ".txt");
|
||||
|
||||
|
||||
@@ -83,4 +83,24 @@ class RunContextPropertyTest {
|
||||
runContextProperty = new RunContextProperty<>(Property.<Map<String, String>>builder().expression("{ \"key\": \"{{ key }}\"}").build(), runContext);
|
||||
assertThat(runContextProperty.asMap(String.class, String.class, Map.of("key", "value"))).containsEntry("key", "value");
|
||||
}
|
||||
|
||||
@Test
|
||||
void asShouldReturnCachedRenderedProperty() throws IllegalVariableEvaluationException {
|
||||
var runContext = runContextFactory.of();
|
||||
|
||||
var runContextProperty = new RunContextProperty<>(Property.<String>builder().expression("{{ variable }}").build(), runContext);
|
||||
|
||||
assertThat(runContextProperty.as(String.class, Map.of("variable", "value1"))).isEqualTo(Optional.of("value1"));
|
||||
assertThat(runContextProperty.as(String.class, Map.of("variable", "value2"))).isEqualTo(Optional.of("value1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void asShouldNotReturnCachedRenderedPropertyWithSkipCache() throws IllegalVariableEvaluationException {
|
||||
var runContext = runContextFactory.of();
|
||||
|
||||
var runContextProperty = new RunContextProperty<>(Property.<String>builder().expression("{{ variable }}").build(), runContext);
|
||||
|
||||
assertThat(runContextProperty.as(String.class, Map.of("variable", "value1"))).isEqualTo(Optional.of("value1"));
|
||||
assertThat(runContextProperty.skipCache().as(String.class, Map.of("variable", "value2"))).isEqualTo(Optional.of("value2"));
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package io.kestra.core.services;
|
||||
|
||||
import io.kestra.core.context.TestRunContextFactory;
|
||||
import io.kestra.core.junit.annotations.KestraTest;
|
||||
import io.kestra.core.models.Label;
|
||||
import io.kestra.core.models.executions.Execution;
|
||||
import io.kestra.core.models.executions.ExecutionKind;
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.models.flows.State;
|
||||
import io.kestra.core.models.triggers.multipleflows.MultipleConditionStorageInterface;
|
||||
import io.kestra.core.utils.IdUtils;
|
||||
import io.kestra.plugin.core.log.Log;
|
||||
import jakarta.inject.Inject;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static io.kestra.core.repositories.AbstractFlowRepositoryTest.TEST_NAMESPACE;
|
||||
import static io.kestra.core.tenant.TenantService.MAIN_TENANT;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@KestraTest
|
||||
class FlowTriggerServiceTest {
|
||||
public static final List<Label> EMPTY_LABELS = List.of();
|
||||
public static final Optional<MultipleConditionStorageInterface> EMPTY_MULTIPLE_CONDITION_STORAGE = Optional.empty();
|
||||
|
||||
@Inject
|
||||
private TestRunContextFactory runContextFactory;
|
||||
@Inject
|
||||
private ConditionService conditionService;
|
||||
@Inject
|
||||
private FlowService flowService;
|
||||
private FlowTriggerService flowTriggerService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
flowTriggerService = new FlowTriggerService(conditionService, runContextFactory, flowService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void computeExecutionsFromFlowTriggers_ok() {
|
||||
var simpleFlow = aSimpleFlow();
|
||||
var flowWithFlowTrigger = Flow.builder()
|
||||
.id("flow-with-flow-trigger")
|
||||
.namespace(TEST_NAMESPACE)
|
||||
.tenantId(MAIN_TENANT)
|
||||
.tasks(List.of(simpleLogTask()))
|
||||
.triggers(List.of(
|
||||
flowTriggerWithNoConditions()
|
||||
))
|
||||
.build();
|
||||
|
||||
var simpleFlowExecution = Execution.newExecution(simpleFlow, EMPTY_LABELS).withState(State.Type.SUCCESS);
|
||||
|
||||
var resultingExecutionsToRun = flowTriggerService.computeExecutionsFromFlowTriggers(
|
||||
simpleFlowExecution,
|
||||
List.of(simpleFlow, flowWithFlowTrigger),
|
||||
EMPTY_MULTIPLE_CONDITION_STORAGE
|
||||
);
|
||||
|
||||
assertThat(resultingExecutionsToRun).size().isEqualTo(1);
|
||||
assertThat(resultingExecutionsToRun.get(0).getFlowId()).isEqualTo(flowWithFlowTrigger.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void computeExecutionsFromFlowTriggers_filteringOutCreatedExecutions() {
|
||||
var simpleFlow = aSimpleFlow();
|
||||
var flowWithFlowTrigger = Flow.builder()
|
||||
.id("flow-with-flow-trigger")
|
||||
.namespace(TEST_NAMESPACE)
|
||||
.tenantId(MAIN_TENANT)
|
||||
.tasks(List.of(simpleLogTask()))
|
||||
.triggers(List.of(
|
||||
flowTriggerWithNoConditions()
|
||||
))
|
||||
.build();
|
||||
|
||||
var simpleFlowExecution = Execution.newExecution(simpleFlow, EMPTY_LABELS).withState(State.Type.CREATED);
|
||||
|
||||
var resultingExecutionsToRun = flowTriggerService.computeExecutionsFromFlowTriggers(
|
||||
simpleFlowExecution,
|
||||
List.of(simpleFlow, flowWithFlowTrigger),
|
||||
EMPTY_MULTIPLE_CONDITION_STORAGE
|
||||
);
|
||||
|
||||
assertThat(resultingExecutionsToRun).size().isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void computeExecutionsFromFlowTriggers_filteringOutTestExecutions() {
|
||||
var simpleFlow = aSimpleFlow();
|
||||
var flowWithFlowTrigger = Flow.builder()
|
||||
.id("flow-with-flow-trigger")
|
||||
.namespace(TEST_NAMESPACE)
|
||||
.tenantId(MAIN_TENANT)
|
||||
.tasks(List.of(simpleLogTask()))
|
||||
.triggers(List.of(
|
||||
flowTriggerWithNoConditions()
|
||||
))
|
||||
.build();
|
||||
|
||||
var simpleFlowExecutionComingFromATest = Execution.newExecution(simpleFlow, EMPTY_LABELS)
|
||||
.withState(State.Type.SUCCESS)
|
||||
.toBuilder()
|
||||
.kind(ExecutionKind.TEST)
|
||||
.build();
|
||||
|
||||
var resultingExecutionsToRun = flowTriggerService.computeExecutionsFromFlowTriggers(
|
||||
simpleFlowExecutionComingFromATest,
|
||||
List.of(simpleFlow, flowWithFlowTrigger),
|
||||
EMPTY_MULTIPLE_CONDITION_STORAGE
|
||||
);
|
||||
|
||||
assertThat(resultingExecutionsToRun).size().isEqualTo(0);
|
||||
}
|
||||
|
||||
private static Flow aSimpleFlow() {
|
||||
return Flow.builder()
|
||||
.id("simple-flow")
|
||||
.namespace(TEST_NAMESPACE)
|
||||
.tenantId(MAIN_TENANT)
|
||||
.tasks(List.of(simpleLogTask()))
|
||||
.build();
|
||||
}
|
||||
|
||||
private static io.kestra.plugin.core.trigger.Flow flowTriggerWithNoConditions() {
|
||||
return io.kestra.plugin.core.trigger.Flow.builder()
|
||||
.id("flowTrigger")
|
||||
.type(io.kestra.plugin.core.trigger.Flow.class.getName())
|
||||
.build();
|
||||
}
|
||||
|
||||
private static Log simpleLogTask() {
|
||||
return Log.builder()
|
||||
.id(IdUtils.create())
|
||||
.type(Log.class.getName())
|
||||
.message("Hello World")
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -372,6 +372,51 @@ public class ForEachItemCaseTest {
|
||||
assertThat(correlationId.get().value()).isEqualTo(execution.getId());
|
||||
}
|
||||
|
||||
public void forEachItemWithAfterExecution() throws TimeoutException, InterruptedException, URISyntaxException, IOException, QueueException {
|
||||
CountDownLatch countDownLatch = new CountDownLatch(26);
|
||||
AtomicReference<Execution> triggered = new AtomicReference<>();
|
||||
|
||||
Flux<Execution> receive = TestsUtils.receive(executionQueue, either -> {
|
||||
Execution execution = either.getLeft();
|
||||
if (execution.getFlowId().equals("for-each-item-subflow-after-execution") && execution.getState().getCurrent().isTerminated()) {
|
||||
triggered.set(execution);
|
||||
countDownLatch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
URI file = storageUpload();
|
||||
Map<String, Object> inputs = Map.of("file", file.toString(), "batch", 4);
|
||||
Execution execution = runnerUtils.runOne(MAIN_TENANT, TEST_NAMESPACE, "for-each-item-after-execution", null,
|
||||
(flow, execution1) -> flowIO.readExecutionInputs(flow, execution1, inputs),
|
||||
Duration.ofSeconds(30));
|
||||
|
||||
// we should have triggered 26 subflows
|
||||
assertThat(countDownLatch.await(1, TimeUnit.MINUTES)).isTrue();
|
||||
receive.blockLast();
|
||||
|
||||
// assert on the main flow execution
|
||||
assertThat(execution.getTaskRunList()).hasSize(5);
|
||||
assertThat(execution.getTaskRunList().get(2).getAttempts()).hasSize(1);
|
||||
assertThat(execution.getTaskRunList().get(2).getAttempts().getFirst().getState().getCurrent()).isEqualTo(State.Type.SUCCESS);
|
||||
assertThat(execution.getState().getCurrent()).isEqualTo(State.Type.SUCCESS);
|
||||
Map<String, Object> outputs = execution.getTaskRunList().get(2).getOutputs();
|
||||
assertThat(outputs.get("numberOfBatches")).isEqualTo(26);
|
||||
assertThat(outputs.get("iterations")).isNotNull();
|
||||
Map<String, Integer> iterations = (Map<String, Integer>) outputs.get("iterations");
|
||||
assertThat(iterations.get("CREATED")).isZero();
|
||||
assertThat(iterations.get("RUNNING")).isZero();
|
||||
assertThat(iterations.get("SUCCESS")).isEqualTo(26);
|
||||
|
||||
// assert on the last subflow execution
|
||||
assertThat(triggered.get().getState().getCurrent()).isEqualTo(State.Type.SUCCESS);
|
||||
assertThat(triggered.get().getFlowId()).isEqualTo("for-each-item-subflow-after-execution");
|
||||
assertThat((String) triggered.get().getInputs().get("items")).matches("kestra:///io/kestra/tests/for-each-item-after-execution/executions/.*/tasks/each-split/.*\\.txt");
|
||||
assertThat(triggered.get().getTaskRunList()).hasSize(2);
|
||||
Optional<Label> correlationId = triggered.get().getLabels().stream().filter(label -> label.key().equals(Label.CORRELATION_ID)).findAny();
|
||||
assertThat(correlationId.isPresent()).isTrue();
|
||||
assertThat(correlationId.get().value()).isEqualTo(execution.getId());
|
||||
}
|
||||
|
||||
private URI storageUpload() throws URISyntaxException, IOException {
|
||||
File tempFile = File.createTempFile("file", ".txt");
|
||||
|
||||
|
||||
@@ -58,4 +58,15 @@ class ParallelTest {
|
||||
assertThat(execution.findTaskRunsByTaskId("a2").getFirst().getState().getStartDate().isAfter(execution.findTaskRunsByTaskId("a1").getFirst().getState().getEndDate().orElseThrow())).isTrue();
|
||||
assertThat(execution.findTaskRunsByTaskId("e2").getFirst().getState().getStartDate().isAfter(execution.findTaskRunsByTaskId("e1").getFirst().getState().getEndDate().orElseThrow())).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@ExecuteFlow("flows/valids/parallel-fail-with-flowable.yaml")
|
||||
void parallelFailWithFlowable(Execution execution) {
|
||||
assertThat(execution.getState().getCurrent()).isEqualTo(State.Type.FAILED);
|
||||
assertThat(execution.getTaskRunList()).hasSize(5);
|
||||
// all tasks must be terminated except the Sleep that will ends later as everything is concurrent
|
||||
execution.getTaskRunList().stream()
|
||||
.filter(taskRun -> !"sleep".equals(taskRun.getTaskId()))
|
||||
.forEach(run -> assertThat(run.getState().isTerminated()).isTrue());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,17 @@
|
||||
id: flow-concurrency-queue-after-execution
|
||||
namespace: io.kestra.tests
|
||||
|
||||
concurrency:
|
||||
behavior: QUEUE
|
||||
limit: 1
|
||||
|
||||
tasks:
|
||||
- id: sleep
|
||||
type: io.kestra.plugin.core.flow.Sleep
|
||||
duration: PT2S
|
||||
|
||||
afterExecution:
|
||||
- id: afterExecution
|
||||
type: io.kestra.plugin.core.output.OutputValues
|
||||
values:
|
||||
some: value
|
||||
@@ -0,0 +1,13 @@
|
||||
id: flow-concurrency-queue-fail
|
||||
namespace: io.kestra.tests
|
||||
|
||||
concurrency:
|
||||
behavior: QUEUE
|
||||
limit: 1
|
||||
|
||||
tasks:
|
||||
- id: sleep
|
||||
type: io.kestra.plugin.core.flow.Sleep
|
||||
duration: PT2S
|
||||
- id: fail
|
||||
type: io.kestra.plugin.core.execution.Fail
|
||||
@@ -0,0 +1,26 @@
|
||||
id: for-each-item-after-execution
|
||||
namespace: io.kestra.tests
|
||||
|
||||
inputs:
|
||||
- id: file
|
||||
type: FILE
|
||||
- id: batch
|
||||
type: INT
|
||||
|
||||
tasks:
|
||||
- id: each
|
||||
type: io.kestra.plugin.core.flow.ForEachItem
|
||||
items: "{{ inputs.file }}"
|
||||
batch:
|
||||
rows: "{{inputs.batch}}"
|
||||
namespace: io.kestra.tests
|
||||
flowId: for-each-item-subflow-after-execution
|
||||
wait: true
|
||||
transmitFailed: true
|
||||
inputs:
|
||||
items: "{{ taskrun.items }}"
|
||||
|
||||
afterExecution:
|
||||
- id: afterExecution
|
||||
type: io.kestra.plugin.core.log.Log
|
||||
message: Hello from afterExecution!
|
||||
@@ -0,0 +1,16 @@
|
||||
id: for-each-item-subflow-after-execution
|
||||
namespace: io.kestra.tests
|
||||
|
||||
inputs:
|
||||
- id: items
|
||||
type: STRING
|
||||
|
||||
tasks:
|
||||
- id: per-item
|
||||
type: io.kestra.plugin.core.log.Log
|
||||
message: "{{ inputs.items }}"
|
||||
|
||||
afterExecution:
|
||||
- id: afterExecution
|
||||
type: io.kestra.plugin.core.log.Log
|
||||
message: Hello from afterExecution!
|
||||
@@ -0,0 +1,28 @@
|
||||
id: parallel-fail-with-flowable
|
||||
namespace: io.kestra.tests
|
||||
|
||||
inputs:
|
||||
- id: user
|
||||
type: STRING
|
||||
defaults: Rick Astley
|
||||
|
||||
|
||||
tasks:
|
||||
- id: parallel
|
||||
type: io.kestra.plugin.core.flow.Parallel
|
||||
tasks:
|
||||
- id: if-1
|
||||
type: io.kestra.plugin.core.flow.If
|
||||
condition: "{{ inputs.user == 'Rick Astley'}}"
|
||||
then:
|
||||
- id: sleep
|
||||
type: io.kestra.plugin.core.flow.Sleep
|
||||
duration: PT1S
|
||||
|
||||
- id: if-2
|
||||
type: io.kestra.plugin.core.flow.If
|
||||
condition: "{{ inputs.user == 'Rick Astley'}}"
|
||||
then:
|
||||
- id: fail_missing_variable
|
||||
type: io.kestra.plugin.core.log.Log
|
||||
message: "{{ vars.nonexistent_variable }}"
|
||||
@@ -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.2
|
||||
|
||||
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");
|
||||
|
||||
|
||||
@@ -20,6 +20,12 @@ public class AbstractJdbcExecutionRunningStorage extends AbstractJdbcRepository
|
||||
this.jdbcRepository = jdbcRepository;
|
||||
}
|
||||
|
||||
public void save(ExecutionRunning executionRunning) {
|
||||
jdbcRepository.getDslContextWrapper().transaction(
|
||||
configuration -> save(DSL.using(configuration), executionRunning)
|
||||
);
|
||||
}
|
||||
|
||||
public void save(DSLContext dslContext, ExecutionRunning executionRunning) {
|
||||
Map<Field<Object>, Object> fields = this.jdbcRepository.persistFields(executionRunning);
|
||||
this.jdbcRepository.persist(executionRunning, dslContext, fields);
|
||||
|
||||
@@ -546,7 +546,7 @@ public class JdbcExecutor implements ExecutorInterface, Service {
|
||||
}
|
||||
|
||||
// create an SLA monitor if needed
|
||||
if (execution.getState().getCurrent() == State.Type.CREATED && !ListUtils.isEmpty(flow.getSla())) {
|
||||
if ((execution.getState().getCurrent() == State.Type.CREATED || execution.getState().failedThenRestarted()) && !ListUtils.isEmpty(flow.getSla())) {
|
||||
List<SLAMonitor> monitors = flow.getSla().stream()
|
||||
.filter(ExecutionMonitoringSLA.class::isInstance)
|
||||
.map(ExecutionMonitoringSLA.class::cast)
|
||||
@@ -562,7 +562,7 @@ public class JdbcExecutor implements ExecutorInterface, Service {
|
||||
|
||||
// handle concurrency limit, we need to use a different queue to be sure that execution running
|
||||
// are processed sequentially so inside a queue with no parallelism
|
||||
if (execution.getState().getCurrent() == State.Type.CREATED && flow.getConcurrency() != null) {
|
||||
if ((execution.getState().getCurrent() == State.Type.CREATED || execution.getState().failedThenRestarted()) && flow.getConcurrency() != null) {
|
||||
ExecutionRunning executionRunning = ExecutionRunning.builder()
|
||||
.tenantId(executor.getFlow().getTenantId())
|
||||
.namespace(executor.getFlow().getNamespace())
|
||||
@@ -1065,7 +1065,11 @@ public class JdbcExecutor implements ExecutorInterface, Service {
|
||||
executorService.log(log, false, executor);
|
||||
}
|
||||
|
||||
// the terminated state can only come from the execution queue, in this case we always have a flow in the executor
|
||||
// the terminated state can come from the execution queue, in this case we always have a flow in the executor
|
||||
// or from a worker task in an afterExecution block, in this case we need to load the flow
|
||||
if (executor.getFlow() == null && executor.getExecution().getState().isTerminated()) {
|
||||
executor = executor.withFlow(findFlow(executor.getExecution()));
|
||||
}
|
||||
boolean isTerminated = executor.getFlow() != null && executionService.isTerminated(executor.getFlow(), executor.getExecution());
|
||||
|
||||
// purge the executionQueue
|
||||
@@ -1121,8 +1125,16 @@ public class JdbcExecutor implements ExecutorInterface, Service {
|
||||
executor.getFlow().getId(),
|
||||
throwConsumer(queued -> {
|
||||
var newExecution = queued.withState(State.Type.RUNNING);
|
||||
metricRegistry.counter(MetricRegistry.METRIC_EXECUTOR_EXECUTION_POPPED_COUNT, MetricRegistry.METRIC_EXECUTOR_EXECUTION_POPPED_COUNT_DESCRIPTION, metricRegistry.tags(newExecution)).increment();
|
||||
ExecutionRunning executionRunning = ExecutionRunning.builder()
|
||||
.tenantId(newExecution.getTenantId())
|
||||
.namespace(newExecution.getNamespace())
|
||||
.flowId(newExecution.getFlowId())
|
||||
.execution(newExecution)
|
||||
.concurrencyState(ExecutionRunning.ConcurrencyState.RUNNING)
|
||||
.build();
|
||||
executionRunningStorage.save(executionRunning);
|
||||
executionQueue.emit(newExecution);
|
||||
metricRegistry.counter(MetricRegistry.METRIC_EXECUTOR_EXECUTION_POPPED_COUNT, MetricRegistry.METRIC_EXECUTOR_EXECUTION_POPPED_COUNT_DESCRIPTION, metricRegistry.tags(newExecution)).increment();
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -1207,13 +1219,13 @@ public class JdbcExecutor implements ExecutorInterface, Service {
|
||||
try {
|
||||
// Handle paused tasks
|
||||
if (executionDelay.getDelayType().equals(ExecutionDelay.DelayType.RESUME_FLOW) && !pair.getLeft().getState().isTerminated()) {
|
||||
FlowInterface flow = flowMetaStore.findByExecution(pair.getLeft()).orElseThrow();
|
||||
if (executionDelay.getTaskRunId() == null) {
|
||||
// if taskRunId is null, this means we restart a flow that was delayed at startup (scheduled on)
|
||||
Execution markAsExecution = pair.getKey().withState(executionDelay.getState());
|
||||
executor = executor.withExecution(markAsExecution, "pausedRestart");
|
||||
} else {
|
||||
// if there is a taskRun it means we restart a paused task
|
||||
FlowInterface flow = flowMetaStore.findByExecution(pair.getLeft()).orElseThrow();
|
||||
Execution markAsExecution = executionService.markAs(
|
||||
pair.getKey(),
|
||||
flow,
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
v-on="activeTab['v-on'] ?? {}"
|
||||
ref="tabContent"
|
||||
:is="activeTab.component"
|
||||
:namespace="namespaceToForward"
|
||||
@go-to-detail="blueprintId => selectedBlueprintId = blueprintId"
|
||||
:embed="activeTab.props && activeTab.props.embed !== undefined ? activeTab.props.embed : true"
|
||||
/>
|
||||
@@ -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,
|
||||
};
|
||||
},
|
||||
},
|
||||
@@ -209,6 +205,11 @@
|
||||
Object.entries(this.$attrs)
|
||||
.filter(([key]) => key !== "class")
|
||||
);
|
||||
},
|
||||
namespaceToForward(){
|
||||
return this.activeTab.props?.namespace ?? this.namespace;
|
||||
// in the special case of Namespace creation on Namespaces page, the tabs are loaded before the namespace creation
|
||||
// in this case this.props.namespace will be used
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
import {computed, getCurrentInstance, ref, Ref, watch} from "vue";
|
||||
import Utils, {useTheme} from "../../utils/utils";
|
||||
import {Buttons, Property, Shown} from "./utils/types";
|
||||
import {editor, KeyCode} from "monaco-editor/esm/vs/editor/editor.api";
|
||||
import * as monaco from "monaco-editor";
|
||||
import Items from "./segments/Items.vue";
|
||||
import {cssVariable} from "@kestra-io/ui-libs";
|
||||
import {LocationQuery, useRoute, useRouter} from "vue-router";
|
||||
@@ -370,7 +370,7 @@
|
||||
};
|
||||
|
||||
const theme = useTheme();
|
||||
const themeComputed: Ref<Omit<Partial<editor.IStandaloneThemeData>, "base"> & { base: ThemeBase }> = ref({
|
||||
const themeComputed: Ref<Omit<Partial<monaco.editor.IStandaloneThemeData>, "base"> & { base: ThemeBase }> = ref({
|
||||
base: Utils.getTheme()!,
|
||||
colors: {
|
||||
"editor.background": cssVariable("--ks-background-input")!
|
||||
@@ -392,7 +392,7 @@
|
||||
|
||||
}, {immediate: true});
|
||||
|
||||
const options: editor.IStandaloneEditorConstructionOptions = {
|
||||
const options: monaco.editor.IStandaloneEditorConstructionOptions = {
|
||||
lineNumbers: "off",
|
||||
folding: false,
|
||||
renderLineHighlight: "none",
|
||||
@@ -436,7 +436,27 @@
|
||||
|
||||
const monacoEditor = ref<typeof MonacoEditor>();
|
||||
|
||||
const editorDidMount = (mountedEditor: editor.IStandaloneCodeEditor) => {
|
||||
const updateQuery = () => {
|
||||
const newQuery = {
|
||||
...Object.fromEntries(queryParamsToKeep.value.map(key => {
|
||||
return [
|
||||
key,
|
||||
route.query[key]
|
||||
]
|
||||
})),
|
||||
...filterQueryString.value
|
||||
};
|
||||
if (_isEqual(route.query, newQuery)) {
|
||||
props.buttons.refresh?.callback?.();
|
||||
return; // Skip if the query hasn't changed
|
||||
}
|
||||
skipRouteWatcherOnce.value = true;
|
||||
router.push({
|
||||
query: newQuery
|
||||
});
|
||||
};
|
||||
|
||||
const editorDidMount = (mountedEditor: monaco.editor.IStandaloneCodeEditor) => {
|
||||
mountedEditor.onDidContentSizeChange((e) => {
|
||||
if (monacoEditor.value === undefined) {
|
||||
return;
|
||||
@@ -445,22 +465,42 @@
|
||||
e.contentHeight + "px";
|
||||
});
|
||||
|
||||
mountedEditor.onKeyDown((e) => {
|
||||
if (e.keyCode === KeyCode.Enter) {
|
||||
const suggestController = mountedEditor.getContribution("editor.contrib.suggestController") as any;
|
||||
|
||||
if (suggestController && suggestController.widget) {
|
||||
return;
|
||||
mountedEditor.addAction({
|
||||
id: "accept_kestra_filter",
|
||||
label: "Accept Kestra Filter",
|
||||
keybindingContext: "!suggestWidgetVisible",
|
||||
keybindings: [monaco.KeyCode.Enter],
|
||||
run: () => {
|
||||
const model = mountedEditor.getModel();
|
||||
if (!model) return;
|
||||
const currentValue = model.getValue();
|
||||
if (currentValue.trim().length > 0) {
|
||||
const position = mountedEditor.getPosition();
|
||||
const endPosition = model.getPositionAt(currentValue.length);
|
||||
if (
|
||||
position &&
|
||||
position.lineNumber === endPosition.lineNumber &&
|
||||
position.column === endPosition.column &&
|
||||
!currentValue.endsWith(" ")
|
||||
) {
|
||||
mountedEditor.executeEdits("", [
|
||||
{
|
||||
range: new monaco.Range(position.lineNumber, position.column, position.lineNumber, position.column),
|
||||
text: " ",
|
||||
forceMoveMarkers: true
|
||||
}
|
||||
]);
|
||||
|
||||
mountedEditor.trigger("enterPressed", "editor.action.triggerSuggest", {});
|
||||
}
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
updateQuery();
|
||||
}
|
||||
});
|
||||
|
||||
mountedEditor.onDidChangeModelContent(e => {
|
||||
if (e.changes.length === 1 && e.changes[0].text === " ") {
|
||||
const model = mountedEditor.getModel();
|
||||
if (model && model.getValue().charAt(e.changes[0].rangeOffset - 1) === ",") {
|
||||
if (e.changes.length === 1 && (e.changes[0].text === " " || e.changes[0].text === "\n")) {
|
||||
if (mountedEditor.getModel()?.getValue().charAt(e.changes[0].rangeOffset - 1) === ",") {
|
||||
mountedEditor.executeEdits("", [
|
||||
{
|
||||
range: {
|
||||
@@ -474,39 +514,10 @@
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove any newlines (e.g., with paste)
|
||||
if (e.changes.some(change => change.text.includes("\n"))) {
|
||||
const model = mountedEditor.getModel();
|
||||
if (model) {
|
||||
const currentValue = model.getValue();
|
||||
if (currentValue.includes("\n")) {
|
||||
const newValue = currentValue.replace(/\n/g, " ");
|
||||
model.setValue(newValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
watchDebounced(filterQueryString, () => {
|
||||
const newQuery = {
|
||||
...Object.fromEntries(queryParamsToKeep.value.map(key => {
|
||||
return [
|
||||
key,
|
||||
route.query[key]
|
||||
];
|
||||
})),
|
||||
...filterQueryString.value
|
||||
};
|
||||
if (_isEqual(route.query, newQuery)) {
|
||||
return; // Skip if the query hasn't changed
|
||||
}
|
||||
skipRouteWatcherOnce.value = true;
|
||||
router.push({
|
||||
query: newQuery
|
||||
});
|
||||
}, {immediate: true, debounce: 1000});
|
||||
watchDebounced(filterQueryString, updateQuery, {immediate: true, debounce: 1000});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -520,7 +531,7 @@
|
||||
border-bottom-right-radius: var(--el-border-radius-base);
|
||||
min-width: 0;
|
||||
|
||||
.mtk25, .mtk28{
|
||||
.mtk25, .mtk28 {
|
||||
background-color: var(--ks-badge-background);
|
||||
padding: 2px 6px;
|
||||
border-radius: var(--el-border-radius-base);
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -373,14 +372,27 @@
|
||||
});
|
||||
|
||||
if (props.input) {
|
||||
editor.addCommand(KeyMod.CtrlCmd | KeyCode.KeyH, () => {});
|
||||
editor.addCommand(KeyCode.F1, () => {});
|
||||
editor.addAction({
|
||||
id: "prevent-ctrl-h",
|
||||
label: "Prevent CTRL + H",
|
||||
keybindings: [KeyMod.CtrlCmd | KeyCode.KeyH],
|
||||
run: () => {}
|
||||
});
|
||||
|
||||
editor.addAction({
|
||||
id: "prevent-f1",
|
||||
label: "Prevent F1",
|
||||
keybindings: [KeyCode.F1],
|
||||
run: () => {}
|
||||
});
|
||||
|
||||
if (!props.readOnly) {
|
||||
editor.addCommand(
|
||||
KeyMod.CtrlCmd | KeyCode.KeyF,
|
||||
() => {},
|
||||
);
|
||||
editor.addAction({
|
||||
id: "prevent-ctrl-f",
|
||||
label: "Prevent CTRL + F",
|
||||
keybindings: [KeyMod.CtrlCmd | KeyCode.KeyF],
|
||||
run: () => {}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -568,11 +580,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 +603,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 +665,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 {
|
||||
|
||||
@@ -152,7 +152,7 @@
|
||||
@tab-loaded="onTabLoaded"
|
||||
:read-only="isReadOnly"
|
||||
:navbar="false"
|
||||
:original="flowYaml"
|
||||
:original="isNamespace ? undefined : flowYaml"
|
||||
:diff-side-by-side="false"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
:diff-side-by-side="false"
|
||||
>
|
||||
<template #absolute>
|
||||
<AITriggerButton
|
||||
<AITriggerButton
|
||||
:show="isCurrentTabFlow"
|
||||
:enabled="aiEnabled"
|
||||
:opened="aiAgentOpened"
|
||||
@@ -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">
|
||||
@@ -43,7 +38,7 @@
|
||||
v-if="aiAgentOpened"
|
||||
class="position-absolute prompt"
|
||||
@close="aiAgentOpened = false"
|
||||
:flow="flowContent"
|
||||
:flow="editorContent"
|
||||
@generated-yaml="(yaml: string) => {draftSource = yaml; aiAgentOpened = false}"
|
||||
/>
|
||||
</transition>
|
||||
@@ -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();
|
||||
@@ -126,11 +119,9 @@
|
||||
async function loadFile() {
|
||||
if (props.dirty || props.flow) return;
|
||||
|
||||
const fileNamespace = namespace.value ?? route.params?.namespace;
|
||||
if (!namespace.value) return;
|
||||
|
||||
if (!fileNamespace) return;
|
||||
|
||||
const content = await store.dispatch("namespace/readFile", {namespace: fileNamespace, path: props.path})
|
||||
const content = await store.dispatch("namespace/readFile", {namespace: namespace.value, path: props.path})
|
||||
store.commit("editor/setTabContent", {path: props.path, content})
|
||||
}
|
||||
|
||||
@@ -147,26 +138,27 @@
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("keydown", handleGlobalSave);
|
||||
window.removeEventListener("keydown", toggleAiShortcut);
|
||||
pluginsStore.editorPlugin = undefined;
|
||||
});
|
||||
|
||||
const editorRefElement = ref<InstanceType<typeof Editor>>();
|
||||
|
||||
const namespace = computed(() => store.state.flow.namespace);
|
||||
const flowStore = computed(() => store.state.flow.flow);
|
||||
const namespace = computed(() => flowStore.value?.namespace ?? route.params?.namespace);
|
||||
const isCreating = computed(() => store.state.flow.isCreating);
|
||||
const isCurrentTabFlow = computed(() => props.flow)
|
||||
const isReadOnly = computed(() => flowStore.value?.deleted || !store.getters["flow/isAllowedEdit"] || store.getters["flow/readOnlySystemLabel"]);
|
||||
|
||||
const timeout = ref<any>(null);
|
||||
|
||||
const flowContent = computed(() => {
|
||||
const editorContent = computed(() => {
|
||||
return draftSource.value ?? source.value;
|
||||
});
|
||||
|
||||
const pluginsStore = usePluginsStore();
|
||||
|
||||
function editorUpdate(newValue: string){
|
||||
if (flowContent.value === newValue) {
|
||||
if (editorContent.value === newValue) {
|
||||
return;
|
||||
}
|
||||
if (isCurrentTabFlow.value) {
|
||||
@@ -234,7 +226,7 @@
|
||||
await store.dispatch("namespace/createFile", {
|
||||
namespace: namespace.value,
|
||||
path: props.path,
|
||||
content: editorRefElement.value?.modelValue,
|
||||
content: editorContent.value || "",
|
||||
});
|
||||
store.commit("editor/setTabDirty", {
|
||||
path: props.path,
|
||||
@@ -276,16 +268,16 @@
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.prompt {
|
||||
bottom: 10%;
|
||||
width: calc(100% - 5rem);
|
||||
left: 3rem;
|
||||
max-width: 700px;
|
||||
background-color: var(--ks-background-panel);
|
||||
box-shadow: 0px 4px 4px 0px var(--ks-card-shadow);
|
||||
}
|
||||
.prompt {
|
||||
bottom: 10%;
|
||||
width: calc(100% - 5rem);
|
||||
left: 3rem;
|
||||
max-width: 700px;
|
||||
background-color: var(--ks-background-panel);
|
||||
box-shadow: 0px 4px 4px 0px var(--ks-card-shadow);
|
||||
}
|
||||
|
||||
.actions {
|
||||
bottom: 10%;
|
||||
}
|
||||
</style>
|
||||
.actions {
|
||||
bottom: 10%;
|
||||
}
|
||||
</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);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user