mirror of
https://github.com/kestra-io/kestra.git
synced 2025-12-25 11:12:12 -05:00
Compare commits
31 Commits
plugin/tem
...
v0.15.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eebcc3a010 | ||
|
|
6d65dda19a | ||
|
|
6768950515 | ||
|
|
93bbc83b5c | ||
|
|
7fbfbe1d00 | ||
|
|
f53f788100 | ||
|
|
8e3a9e4380 | ||
|
|
cb82e9f08e | ||
|
|
9deb02983c | ||
|
|
9b1939e129 | ||
|
|
4dfcbca7de | ||
|
|
103320e348 | ||
|
|
410093cefc | ||
|
|
bd936125bd | ||
|
|
68ded87434 | ||
|
|
d76807235f | ||
|
|
63708a79e3 | ||
|
|
41c0018d4b | ||
|
|
2cbd86c4d1 | ||
|
|
49b64fa853 | ||
|
|
95113c5e76 | ||
|
|
1e5e300974 | ||
|
|
f69dc3c835 | ||
|
|
651c7bf589 | ||
|
|
7878bcc281 | ||
|
|
ee059106b2 | ||
|
|
4d2728a3f6 | ||
|
|
87f7cde742 | ||
|
|
2e18c87907 | ||
|
|
58352411b5 | ||
|
|
438619dd8c |
@@ -37,11 +37,17 @@ micronaut:
|
||||
- /ui/.+
|
||||
- /health
|
||||
- /prometheus
|
||||
http-version: HTTP_1_1
|
||||
caches:
|
||||
default:
|
||||
maximum-weight: 10485760
|
||||
|
||||
http:
|
||||
client:
|
||||
read-idle-timeout: 60s
|
||||
connect-timeout: 30s
|
||||
read-timeout: 60s
|
||||
http-version: HTTP_1_1
|
||||
services:
|
||||
api:
|
||||
url: https://api.kestra.io
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package io.kestra.core.models.flows;
|
||||
|
||||
import io.kestra.core.validations.TaskDefaultValidation;
|
||||
import io.micronaut.core.annotation.Introspected;
|
||||
import io.micronaut.core.convert.format.MapFormat;
|
||||
import io.micronaut.core.naming.conventions.StringConvention;
|
||||
@@ -13,6 +14,7 @@ import java.util.Map;
|
||||
@Builder(toBuilder = true)
|
||||
@AllArgsConstructor
|
||||
@Introspected
|
||||
@TaskDefaultValidation
|
||||
public class TaskDefault {
|
||||
private final String type;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package io.kestra.core.models.hierarchies;
|
||||
|
||||
import io.kestra.core.models.triggers.AbstractTrigger;
|
||||
import io.kestra.core.models.triggers.Trigger;
|
||||
import io.micronaut.core.annotation.Introspected;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
@@ -9,18 +10,20 @@ import lombok.ToString;
|
||||
@Getter
|
||||
@Introspected
|
||||
public abstract class AbstractGraphTrigger extends AbstractGraph {
|
||||
private final AbstractTrigger trigger;
|
||||
private final AbstractTrigger triggerDeclaration;
|
||||
private final Trigger trigger;
|
||||
|
||||
public AbstractGraphTrigger(AbstractTrigger trigger) {
|
||||
public AbstractGraphTrigger(AbstractTrigger triggerDeclaration, Trigger trigger) {
|
||||
super();
|
||||
|
||||
this.triggerDeclaration = triggerDeclaration;
|
||||
this.trigger = trigger;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUid() {
|
||||
if (this.trigger != null) {
|
||||
return this.trigger.getId();
|
||||
if (this.triggerDeclaration != null) {
|
||||
return this.triggerDeclaration.getId();
|
||||
}
|
||||
|
||||
return this.uid;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package io.kestra.core.models.hierarchies;
|
||||
|
||||
import io.kestra.core.models.triggers.AbstractTrigger;
|
||||
import io.kestra.core.models.triggers.Trigger;
|
||||
|
||||
|
||||
public class GraphTrigger extends AbstractGraphTrigger {
|
||||
public GraphTrigger(AbstractTrigger trigger) {
|
||||
super(trigger);
|
||||
public GraphTrigger(AbstractTrigger triggerDeclaration, Trigger trigger) {
|
||||
super(triggerDeclaration, trigger);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,12 +254,12 @@ public class Schedule extends AbstractTrigger implements PollingTriggerInterface
|
||||
// is after the end, then we calculate again the nextDate
|
||||
// based on now()
|
||||
if (backfill != null && nextDate != null && nextDate.isAfter(backfill.getEnd())) {
|
||||
nextDate = computeNextEvaluationDate(executionTime, ZonedDateTime.now()).orElse(null);
|
||||
nextDate = computeNextEvaluationDate(executionTime, convertDateTime(ZonedDateTime.now())).orElse(null);
|
||||
}
|
||||
}
|
||||
// no previous present & no backfill or recover missed schedules, just provide now
|
||||
else {
|
||||
nextDate = computeNextEvaluationDate(executionTime, ZonedDateTime.now()).orElse(null);
|
||||
nextDate = computeNextEvaluationDate(executionTime, convertDateTime(ZonedDateTime.now())).orElse(null);
|
||||
}
|
||||
|
||||
// if max delay reached, we calculate a new date except if we are doing a backfill
|
||||
@@ -280,7 +280,7 @@ public class Schedule extends AbstractTrigger implements PollingTriggerInterface
|
||||
public ZonedDateTime nextEvaluationDate() {
|
||||
// it didn't take into account the schedule condition, but as they are taken into account inside eval() it's OK.
|
||||
ExecutionTime executionTime = this.executionTime();
|
||||
return computeNextEvaluationDate(executionTime, ZonedDateTime.now()).orElse(ZonedDateTime.now());
|
||||
return computeNextEvaluationDate(executionTime, convertDateTime(ZonedDateTime.now())).orElse(convertDateTime(ZonedDateTime.now()));
|
||||
}
|
||||
|
||||
public ZonedDateTime previousEvaluationDate(ConditionContext conditionContext) {
|
||||
@@ -301,7 +301,7 @@ public class Schedule extends AbstractTrigger implements PollingTriggerInterface
|
||||
conditionContext.getRunContext().logger().warn("Unable to evaluate the conditions for the next evaluation date for trigger '{}', conditions will not be evaluated", this.getId());
|
||||
}
|
||||
}
|
||||
return computePreviousEvaluationDate(executionTime, ZonedDateTime.now()).orElse(ZonedDateTime.now());
|
||||
return computePreviousEvaluationDate(executionTime, convertDateTime(ZonedDateTime.now())).orElse(convertDateTime(ZonedDateTime.now()));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -28,10 +28,9 @@ public class VariableRenderer {
|
||||
private static final Pattern RAW_PATTERN = Pattern.compile("\\{%[-]*\\s*raw\\s*[-]*%\\}(.*?)\\{%[-]*\\s*endraw\\s*[-]*%\\}");
|
||||
public static final int MAX_RENDERING_AMOUNT = 100;
|
||||
|
||||
private PebbleEngine pebbleEngine;
|
||||
private final PebbleEngine pebbleEngine;
|
||||
private final VariableConfiguration variableConfiguration;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Inject
|
||||
public VariableRenderer(ApplicationContext applicationContext, @Nullable VariableConfiguration variableConfiguration) {
|
||||
this.variableConfiguration = variableConfiguration != null ? variableConfiguration : new VariableConfiguration();
|
||||
@@ -102,17 +101,10 @@ public class VariableRenderer {
|
||||
Writer writer = new JsonWriter(new StringWriter());
|
||||
compiledTemplate.evaluate(writer, variables);
|
||||
result = writer.toString();
|
||||
} catch (IOException | PebbleException e) {
|
||||
String alternativeRender = this.alternativeRender(e, inline, variables);
|
||||
if (alternativeRender == null) {
|
||||
if (e instanceof PebbleException) {
|
||||
throw properPebbleException((PebbleException) e);
|
||||
}
|
||||
|
||||
throw new IllegalVariableEvaluationException(e);
|
||||
} else {
|
||||
result = alternativeRender;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new IllegalVariableEvaluationException(e);
|
||||
} catch (PebbleException e) {
|
||||
throw properPebbleException(e);
|
||||
}
|
||||
|
||||
// post-process raw tags
|
||||
@@ -123,10 +115,6 @@ public class VariableRenderer {
|
||||
return result;
|
||||
}
|
||||
|
||||
protected String alternativeRender(Exception e, String inline, Map<String, Object> variables) throws IllegalVariableEvaluationException {
|
||||
return null;
|
||||
}
|
||||
|
||||
public String renderRecursively(String inline, Map<String, Object> variables) throws IllegalVariableEvaluationException {
|
||||
return this.renderRecursively(0, inline, variables);
|
||||
}
|
||||
@@ -180,7 +168,8 @@ public class VariableRenderer {
|
||||
return Optional.of(this.render(string, variables, recursive));
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
// Return the given object if it cannot be rendered.
|
||||
return Optional.ofNullable(object);
|
||||
}
|
||||
|
||||
public List<Object> renderList(List<Object> list, Map<String, Object> variables) throws IllegalVariableEvaluationException {
|
||||
|
||||
@@ -39,10 +39,9 @@ public class RenderFunction implements Function {
|
||||
recursiveArg = true;
|
||||
}
|
||||
|
||||
if (!(recursiveArg instanceof Boolean)) {
|
||||
if (!(recursiveArg instanceof Boolean recursive)) {
|
||||
throw new PebbleException(null, "The 'render' function expects an optional argument 'recursive' with type boolean.", lineNumber, self.getName());
|
||||
}
|
||||
Boolean recursive = (Boolean) recursiveArg;
|
||||
|
||||
EvaluationContextImpl evaluationContext = (EvaluationContextImpl) context;
|
||||
Map<String, Object> variables = evaluationContext.getScopeChain().getGlobalScopes().stream()
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
package io.kestra.core.services;
|
||||
|
||||
import io.kestra.core.exceptions.IllegalVariableEvaluationException;
|
||||
import io.kestra.core.models.executions.Execution;
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.models.hierarchies.*;
|
||||
import io.kestra.core.models.triggers.Trigger;
|
||||
import io.kestra.core.repositories.FlowRepositoryInterface;
|
||||
import io.kestra.core.repositories.TriggerRepositoryInterface;
|
||||
import io.kestra.core.utils.GraphUtils;
|
||||
import io.kestra.core.utils.Rethrow;
|
||||
import io.micronaut.data.model.Pageable;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -19,20 +23,38 @@ public class GraphService {
|
||||
@Inject
|
||||
private FlowRepositoryInterface flowRepository;
|
||||
@Inject
|
||||
private TriggerRepositoryInterface triggerRepository;
|
||||
@Inject
|
||||
private TaskDefaultService taskDefaultService;
|
||||
|
||||
public FlowGraph flowGraph(Flow flow, List<String> expandedSubflows) throws IllegalVariableEvaluationException {
|
||||
return FlowGraph.of(this.of(flow, Optional.ofNullable(expandedSubflows).orElse(Collections.emptyList()), new HashMap<>()));
|
||||
return this.flowGraph(flow, expandedSubflows, null);
|
||||
}
|
||||
|
||||
public FlowGraph flowGraph(Flow flow, List<String> expandedSubflows, Execution execution) throws IllegalVariableEvaluationException {
|
||||
return FlowGraph.of(this.of(flow, Optional.ofNullable(expandedSubflows).orElse(Collections.emptyList()), new HashMap<>(), execution));
|
||||
}
|
||||
|
||||
public GraphCluster of(Flow flow, List<String> expandedSubflows, Map<String, Flow> flowByUid) throws IllegalVariableEvaluationException {
|
||||
return this.of(null, flow, expandedSubflows, flowByUid);
|
||||
return this.of(flow, expandedSubflows, flowByUid, null);
|
||||
}
|
||||
|
||||
public GraphCluster of(Flow flow, List<String> expandedSubflows, Map<String, Flow> flowByUid, Execution execution) throws IllegalVariableEvaluationException {
|
||||
return this.of(null, flow, expandedSubflows, flowByUid, execution);
|
||||
}
|
||||
|
||||
public GraphCluster of(GraphCluster baseGraph, Flow flow, List<String> expandedSubflows, Map<String, Flow> flowByUid) throws IllegalVariableEvaluationException {
|
||||
return this.of(baseGraph, flow, expandedSubflows, flowByUid, null);
|
||||
}
|
||||
|
||||
public GraphCluster of(GraphCluster baseGraph, Flow flow, List<String> expandedSubflows, Map<String, Flow> flowByUid, Execution execution) throws IllegalVariableEvaluationException {
|
||||
String tenantId = flow.getTenantId();
|
||||
flow = taskDefaultService.injectDefaults(flow);
|
||||
GraphCluster graphCluster = GraphUtils.of(baseGraph, flow, null);
|
||||
List<Trigger> triggers = null;
|
||||
if (flow.getTriggers() != null) {
|
||||
triggers = triggerRepository.find(Pageable.UNPAGED, null, tenantId, flow.getNamespace(), flow.getId());
|
||||
}
|
||||
GraphCluster graphCluster = GraphUtils.of(baseGraph, flow, execution, triggers);
|
||||
|
||||
|
||||
Stream<Map.Entry<GraphCluster, SubflowGraphTask>> subflowToReplaceByParent = graphCluster.allNodesByParent().entrySet().stream()
|
||||
|
||||
@@ -107,7 +107,7 @@ public abstract class StorageService {
|
||||
|
||||
writers.forEach(throwConsumer(RandomAccessFile::close));
|
||||
|
||||
return files;
|
||||
return files.stream().filter(p -> p.toFile().length() > 0).toList();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package io.kestra.core.tasks.flows;
|
||||
|
||||
public interface ChildFlowInterface {
|
||||
String getNamespace();
|
||||
|
||||
String getFlowId();
|
||||
}
|
||||
@@ -173,7 +173,7 @@ import static io.kestra.core.utils.Rethrow.throwFunction;
|
||||
)
|
||||
}
|
||||
)
|
||||
public class ForEachItem extends Task implements FlowableTask<VoidOutput> {
|
||||
public class ForEachItem extends Task implements FlowableTask<VoidOutput>, ChildFlowInterface {
|
||||
@NotEmpty
|
||||
@PluginProperty(dynamic = true)
|
||||
@Schema(title = "The items to be split into batches and processed. Make sure to set it to Kestra's internal storage URI. This can be either the output from a previous task, formatted as `{{ outputs.task_id.uri }}`, or a FILE type input parameter, like `{{ inputs.myfile }}`. This task is optimized for files where each line represents a single item. Suitable file types include Amazon ION-type files (commonly produced by Query tasks), newline-separated JSON files, or CSV files formatted with one row per line and without a header. For files in other formats such as Excel, CSV, Avro, Parquet, XML, or JSON, it's recommended to first convert them to the ION format. This can be done using the conversion tasks available in the `io.kestra.plugin.serdes` module, which will transform files from their original format to ION.")
|
||||
|
||||
@@ -123,7 +123,7 @@ public class Pause extends Sequential implements FlowableTask<VoidOutput> {
|
||||
|
||||
private boolean needPause(TaskRun parentTaskRun) {
|
||||
return parentTaskRun.getState().getCurrent() == State.Type.RUNNING &&
|
||||
parentTaskRun.getState().getHistories().get(parentTaskRun.getState().getHistories().size() - 2).getState() != State.Type.PAUSED;
|
||||
parentTaskRun.getState().getHistories().stream().noneMatch(history -> history.getState() == State.Type.PAUSED);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -60,7 +60,7 @@ import java.util.stream.Collectors;
|
||||
)
|
||||
}
|
||||
)
|
||||
public class Subflow extends Task implements ExecutableTask<Subflow.Output> {
|
||||
public class Subflow extends Task implements ExecutableTask<Subflow.Output>, ChildFlowInterface {
|
||||
|
||||
static final String PLUGIN_FLOW_OUTPUTS_ENABLED = "outputs.enabled";
|
||||
|
||||
|
||||
@@ -9,27 +9,33 @@ import io.kestra.core.models.tasks.ExecutableTask;
|
||||
import io.kestra.core.models.tasks.FlowableTask;
|
||||
import io.kestra.core.models.tasks.Task;
|
||||
import io.kestra.core.models.triggers.AbstractTrigger;
|
||||
import io.kestra.core.models.triggers.Trigger;
|
||||
import io.kestra.core.tasks.flows.Dag;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.apache.commons.lang3.tuple.Triple;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class GraphUtils {
|
||||
public static FlowGraph flowGraph(Flow flow, Execution execution) throws IllegalVariableEvaluationException {
|
||||
return FlowGraph.of(GraphUtils.of(flow, execution));
|
||||
return GraphUtils.flowGraph(flow, execution, null);
|
||||
}
|
||||
|
||||
public static GraphCluster of(GraphCluster graph, Flow flow, Execution execution) throws IllegalVariableEvaluationException {
|
||||
public static FlowGraph flowGraph(Flow flow, Execution execution, List<Trigger> triggers) throws IllegalVariableEvaluationException {
|
||||
return FlowGraph.of(GraphUtils.of(flow, execution, triggers));
|
||||
}
|
||||
|
||||
public static GraphCluster of(GraphCluster graph, Flow flow, Execution execution, List<Trigger> triggers) throws IllegalVariableEvaluationException {
|
||||
if (graph == null) {
|
||||
graph = new GraphCluster();
|
||||
}
|
||||
|
||||
if (flow.getTriggers() != null) {
|
||||
GraphCluster triggers = GraphUtils.triggers(graph, flow.getTriggers());
|
||||
graph.addEdge(triggers.getEnd(), graph.getRoot(), new Relation());
|
||||
GraphCluster triggersClusters = GraphUtils.triggers(graph, flow.getTriggers(), triggers);
|
||||
graph.addEdge(triggersClusters.getEnd(), graph.getRoot(), new Relation());
|
||||
}
|
||||
|
||||
GraphUtils.sequential(
|
||||
@@ -44,16 +50,29 @@ public class GraphUtils {
|
||||
}
|
||||
|
||||
public static GraphCluster of(Flow flow, Execution execution) throws IllegalVariableEvaluationException {
|
||||
return GraphUtils.of(new GraphCluster(), flow, execution);
|
||||
return GraphUtils.of(flow, execution, null);
|
||||
}
|
||||
|
||||
public static GraphCluster triggers(GraphCluster graph, List<AbstractTrigger> triggers) throws IllegalVariableEvaluationException {
|
||||
public static GraphCluster of(Flow flow, Execution execution, List<Trigger> triggers) throws IllegalVariableEvaluationException {
|
||||
return GraphUtils.of(new GraphCluster(), flow, execution, triggers);
|
||||
}
|
||||
|
||||
public static GraphCluster triggers(GraphCluster graph, List<AbstractTrigger> triggersDeclarations, List<Trigger> triggers) throws IllegalVariableEvaluationException {
|
||||
GraphCluster triggerCluster = new GraphCluster("Triggers");
|
||||
|
||||
graph.addNode(triggerCluster);
|
||||
|
||||
triggers.forEach(trigger -> {
|
||||
GraphTrigger triggerNode = new GraphTrigger(trigger);
|
||||
Map<String, Trigger> triggersById = Optional.ofNullable(triggers)
|
||||
.map(Collection::stream)
|
||||
.map(s -> s.collect(Collectors.toMap(
|
||||
Trigger::getTriggerId,
|
||||
Function.identity(),
|
||||
(a, b) -> a.getNamespace().length() <= b.getNamespace().length() ? a : b
|
||||
)))
|
||||
.orElse(Collections.emptyMap());
|
||||
|
||||
triggersDeclarations.forEach(trigger -> {
|
||||
GraphTrigger triggerNode = new GraphTrigger(trigger, triggersById.get(trigger.getId()));
|
||||
triggerCluster.addNode(triggerNode);
|
||||
triggerCluster.addEdge(triggerCluster.getRoot(), triggerNode, new Relation());
|
||||
triggerCluster.addEdge(triggerNode, triggerCluster.getEnd(), new Relation());
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package io.kestra.core.validations;
|
||||
|
||||
import jakarta.validation.Constraint;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Constraint(validatedBy = {})
|
||||
public @interface TaskDefaultValidation {
|
||||
String message() default "invalid taskDefault";
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package io.kestra.core.validations.validator;
|
||||
|
||||
import io.kestra.core.models.flows.TaskDefault;
|
||||
import io.kestra.core.models.validations.ModelValidator;
|
||||
import io.kestra.core.serializers.YamlFlowParser;
|
||||
import io.kestra.core.services.TaskDefaultService;
|
||||
import io.micronaut.core.annotation.AnnotationValue;
|
||||
import io.micronaut.core.annotation.Introspected;
|
||||
import io.micronaut.core.annotation.NonNull;
|
||||
import io.micronaut.core.annotation.Nullable;
|
||||
import io.micronaut.validation.validator.constraints.ConstraintValidator;
|
||||
import io.micronaut.validation.validator.constraints.ConstraintValidatorContext;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import io.kestra.core.validations.TaskDefaultValidation;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
@Singleton
|
||||
@Introspected
|
||||
public class TaskDefaultValidator implements ConstraintValidator<TaskDefaultValidation, TaskDefault> {
|
||||
@Inject
|
||||
private ModelValidator modelValidator;
|
||||
|
||||
@Inject
|
||||
private TaskDefaultService taskDefaultService;
|
||||
|
||||
@Inject
|
||||
private YamlFlowParser yamlFlowParser;
|
||||
|
||||
@Override
|
||||
public boolean isValid(@Nullable TaskDefault value, @NonNull AnnotationValue<TaskDefaultValidation> annotationMetadata, @NonNull ConstraintValidatorContext context) {
|
||||
if (value == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
List<String> violations = new ArrayList<>();
|
||||
|
||||
if (value.getValues() == null) {
|
||||
violations.add("Null values map found");
|
||||
context.messageTemplate("Invalid Task Default: " + String.join(", ", violations));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the "values" map is empty
|
||||
for (Map.Entry<String, Object> entry : value.getValues().entrySet()) {
|
||||
if (entry.getValue() == null) {
|
||||
violations.add("Null value found in values with key " + entry.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
if (!violations.isEmpty()) {
|
||||
context.messageTemplate("Invalid Task Default: " + String.join(", ", violations));
|
||||
|
||||
return false;
|
||||
} else {
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import io.kestra.core.exceptions.IllegalVariableEvaluationException;
|
||||
import io.kestra.core.exceptions.InternalException;
|
||||
import io.kestra.core.models.executions.Execution;
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.models.triggers.Trigger;
|
||||
import io.kestra.core.repositories.TriggerRepositoryInterface;
|
||||
import io.kestra.core.runners.AbstractMemoryRunnerTest;
|
||||
import io.kestra.core.serializers.YamlFlowParser;
|
||||
import io.kestra.core.services.GraphService;
|
||||
@@ -11,6 +13,7 @@ import io.kestra.core.tasks.flows.Subflow;
|
||||
import io.kestra.core.tasks.flows.Switch;
|
||||
import io.kestra.core.utils.GraphUtils;
|
||||
import io.kestra.core.utils.TestsUtils;
|
||||
import io.micronaut.data.model.Pageable;
|
||||
import jakarta.inject.Inject;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@@ -31,6 +34,9 @@ class FlowGraphTest extends AbstractMemoryRunnerTest {
|
||||
@Inject
|
||||
private GraphService graphService;
|
||||
|
||||
@Inject
|
||||
private TriggerRepositoryInterface triggerRepositoryInterface;
|
||||
|
||||
@Test
|
||||
void simple() throws IllegalVariableEvaluationException {
|
||||
Flow flow = this.parse("flows/valids/return.yaml");
|
||||
@@ -205,11 +211,17 @@ class FlowGraphTest extends AbstractMemoryRunnerTest {
|
||||
@Test
|
||||
void trigger() throws IllegalVariableEvaluationException {
|
||||
Flow flow = this.parse("flows/valids/trigger-flow-listener.yaml");
|
||||
FlowGraph flowGraph = GraphUtils.flowGraph(flow, null);
|
||||
triggerRepositoryInterface.save(
|
||||
Trigger.of(flow, flow.getTriggers().get(0)).toBuilder().disabled(true).build()
|
||||
);
|
||||
|
||||
FlowGraph flowGraph = graphService.flowGraph(flow, null);
|
||||
|
||||
assertThat(flowGraph.getNodes().size(), is(6));
|
||||
assertThat(flowGraph.getEdges().size(), is(5));
|
||||
assertThat(flowGraph.getClusters().size(), is(1));
|
||||
AbstractGraph triggerGraph = flowGraph.getNodes().stream().filter(e -> e instanceof GraphTrigger).findFirst().orElseThrow();
|
||||
assertThat(((GraphTrigger) triggerGraph).getTrigger().getDisabled(), is(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -72,6 +72,10 @@ public abstract class AbstractTriggerRepositoryTest {
|
||||
assertThat(find.size(), is(4));
|
||||
assertThat(find.get(0).getNamespace(), is(namespace));
|
||||
|
||||
find = triggerRepository.find(Pageable.from(1, 4, Sort.of(Sort.Order.asc("namespace"))), null, null, null, searchedTrigger.getFlowId());
|
||||
assertThat(find.size(), is(1));
|
||||
assertThat(find.get(0).getFlowId(), is(searchedTrigger.getFlowId()));
|
||||
|
||||
find = triggerRepository.find(Pageable.from(1, 100, Sort.of(Sort.Order.asc(triggerRepository.sortMapping().apply("triggerId")))), null, null, namespacePrefix);
|
||||
assertThat(find.size(), is(1));
|
||||
assertThat(find.get(0).getTriggerId(), is(trigger.getTriggerId()));
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package io.kestra.core.runners.pebble.functions;
|
||||
|
||||
import io.kestra.core.exceptions.IllegalVariableEvaluationException;
|
||||
import io.kestra.core.runners.VariableRenderer;
|
||||
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
|
||||
import jakarta.inject.Inject;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.*;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@MicronautTest
|
||||
class RenderFunctionTest {
|
||||
@Inject
|
||||
VariableRenderer variableRenderer;
|
||||
|
||||
@Test
|
||||
void shouldRenderForString() throws IllegalVariableEvaluationException {
|
||||
String rendered = variableRenderer.render("{{ render(input) }}", Map.of("input", "test"));
|
||||
Assertions.assertEquals("test", rendered);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRenderForInteger() throws IllegalVariableEvaluationException {
|
||||
String rendered = variableRenderer.render("{{ render(input) }}", Map.of("input", 42));
|
||||
Assertions.assertEquals("42", rendered);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRenderForLong() throws IllegalVariableEvaluationException {
|
||||
String rendered = variableRenderer.render("{{ render(input) }}", Map.of("input", 42L));
|
||||
Assertions.assertEquals("42", rendered);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRenderForBoolean() throws IllegalVariableEvaluationException {
|
||||
String rendered = variableRenderer.render("{{ render(input) }}", Map.of("input", true));
|
||||
Assertions.assertEquals("true", rendered);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRenderForNull() throws IllegalVariableEvaluationException {
|
||||
String rendered = variableRenderer.render("{{ render(input) }}", new HashMap<>(){{put("input", null);}});
|
||||
Assertions.assertEquals("", rendered);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRenderForDateTime() throws IllegalVariableEvaluationException {
|
||||
Instant now = Instant.now();
|
||||
LocalDateTime datetime = LocalDateTime.ofInstant(now, ZoneOffset.UTC);
|
||||
String rendered = variableRenderer.render("{{ render(input) }}", Map.of("input", datetime));
|
||||
Assertions.assertEquals(datetime.toString(), rendered);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRenderForDuration() throws IllegalVariableEvaluationException {
|
||||
String rendered = variableRenderer.render("{{ render(input) }}", Map.of("input", Duration.ofSeconds(5)));
|
||||
Assertions.assertEquals(Duration.ofSeconds(5).toString(), rendered);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package io.kestra.core.validations;
|
||||
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.models.flows.TaskDefault;
|
||||
import io.kestra.core.models.validations.ModelValidator;
|
||||
import io.kestra.core.serializers.YamlFlowParser;
|
||||
import io.kestra.core.utils.TestsUtils;
|
||||
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
|
||||
@MicronautTest
|
||||
class TaskDefaultValidationTest {
|
||||
@Inject
|
||||
private ModelValidator modelValidator;
|
||||
|
||||
@Test
|
||||
void nullValue() {
|
||||
TaskDefault taskDefault = TaskDefault.builder()
|
||||
.type("io.kestra.tests")
|
||||
.build();
|
||||
|
||||
Optional<ConstraintViolationException> validate = modelValidator.isValid(taskDefault);
|
||||
|
||||
assertThat(validate.isPresent(), is(true));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
id: restart_pause_last_failed
|
||||
namespace: io.kestra.tests
|
||||
|
||||
tasks:
|
||||
- id: a
|
||||
type: io.kestra.core.tasks.log.Log
|
||||
message: "{{ task.id }}"
|
||||
- id: b
|
||||
type: io.kestra.core.tasks.log.Log
|
||||
message: "{{ task.id }}"
|
||||
- id: pause
|
||||
type: io.kestra.core.tasks.flows.Pause
|
||||
delay: PT1S
|
||||
tasks:
|
||||
- id: c
|
||||
type: io.kestra.core.tasks.log.Log
|
||||
message: "{{taskrun.attemptsCount == 1 ? 'ok' : ko}}"
|
||||
- id: d
|
||||
type: io.kestra.core.tasks.log.Log
|
||||
message: "{{ task.id }}"
|
||||
@@ -1,4 +1,4 @@
|
||||
version=0.15.0
|
||||
version=0.15.5
|
||||
|
||||
jacksonVersion=2.16.1
|
||||
micronautVersion=4.3.4
|
||||
|
||||
@@ -65,6 +65,21 @@ public class MemoryTriggerRepository implements TriggerRepositoryInterface {
|
||||
|
||||
@Override
|
||||
public ArrayListTotal<Trigger> find(Pageable from, String query, String tenantId, String namespace, String flowId) {
|
||||
throw new UnsupportedOperationException();
|
||||
List<Trigger> filteredTriggers = triggers.stream().filter(trigger -> {
|
||||
if (tenantId != null && !tenantId.equals(trigger.getTenantId())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (namespace != null && !namespace.equals(trigger.getNamespace())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (flowId != null && !flowId.equals(trigger.getFlowId())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}).toList();
|
||||
return new ArrayListTotal<>(filteredTriggers, filteredTriggers.size());
|
||||
}
|
||||
}
|
||||
|
||||
34
ui/package-lock.json
generated
34
ui/package-lock.json
generated
@@ -8,7 +8,7 @@
|
||||
"name": "kestra",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@kestra-io/ui-libs": "^0.0.36",
|
||||
"@kestra-io/ui-libs": "^0.0.39",
|
||||
"@popperjs/core": "npm:@sxzz/popperjs-es@2.11.7",
|
||||
"@vue-flow/background": "^1.2.0",
|
||||
"@vue-flow/controls": "1.0.6",
|
||||
@@ -121,6 +121,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -299,9 +300,9 @@
|
||||
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
|
||||
},
|
||||
"node_modules/@kestra-io/ui-libs": {
|
||||
"version": "0.0.36",
|
||||
"resolved": "https://registry.npmjs.org/@kestra-io/ui-libs/-/ui-libs-0.0.36.tgz",
|
||||
"integrity": "sha512-yJJa0+tVlcWVllMVHoFQVrWzR7nIyF/+6aN8u+OPnMaHR0zSUx9MwaxF6u/YYPoBw6J4zq4ysn3pspq/DGB4ag==",
|
||||
"version": "0.0.39",
|
||||
"resolved": "https://registry.npmjs.org/@kestra-io/ui-libs/-/ui-libs-0.0.39.tgz",
|
||||
"integrity": "sha512-uX5Iqio6Ni6woUDuuEPP2fkImP6Y041FGR88Lt0Q6gfCEhxs4yW7WqCWpyvYRBHz4FT6Pb/qYscTIlXcYZIWKA==",
|
||||
"peerDependencies": {
|
||||
"@vue-flow/background": "^1.2.0",
|
||||
"@vue-flow/controls": "1.0.6",
|
||||
@@ -494,12 +495,6 @@
|
||||
"integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/linkify-it": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.5.tgz",
|
||||
"integrity": "sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/lodash": {
|
||||
"version": "4.14.198",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.198.tgz",
|
||||
@@ -513,22 +508,6 @@
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/markdown-it": {
|
||||
"version": "13.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-13.0.7.tgz",
|
||||
"integrity": "sha512-U/CBi2YUUcTHBt5tjO2r5QV/x0Po6nsYwQU4Y04fBS6vfoImaiZ6f8bi3CjTCxBPQSO1LMyUqkByzi8AidyxfA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/linkify-it": "*",
|
||||
"@types/mdurl": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/mdurl": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.5.tgz",
|
||||
"integrity": "sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/minimatch": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz",
|
||||
@@ -2258,6 +2237,7 @@
|
||||
"version": "0.19.8",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.8.tgz",
|
||||
"integrity": "sha512-l7iffQpT2OrZfH2rXIp7/FkmaeZM0vxbxN9KfiCwGYuZqzMg/JdvX26R31Zxn/Pxvsrg3Y9N6XTcnknqDyyv4w==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
@@ -5176,7 +5156,7 @@
|
||||
"version": "5.3.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
|
||||
"integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@kestra-io/ui-libs": "^0.0.36",
|
||||
"@kestra-io/ui-libs": "^0.0.39",
|
||||
"@popperjs/core": "npm:@sxzz/popperjs-es@2.11.7",
|
||||
"@vue-flow/background": "^1.2.0",
|
||||
"@vue-flow/controls": "1.0.6",
|
||||
|
||||
61
ui/src/components/Drawer.vue
Normal file
61
ui/src/components/Drawer.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
:model-value="props.modelValue"
|
||||
@update:model-value="emit('update:modelValue', $event)"
|
||||
destroy-on-close
|
||||
lock-scroll
|
||||
size=""
|
||||
:append-to-body="true"
|
||||
:class="{'full-screen': fullScreen}"
|
||||
ref="editorDomElement"
|
||||
>
|
||||
<template #header>
|
||||
<span>
|
||||
{{ title }}
|
||||
<slot name="header" />
|
||||
</span>
|
||||
<el-button link class="full-screen">
|
||||
<Fullscreen :title="$t('toggle fullscreen')" @click="toggleFullScreen" />
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<slot name="footer" />
|
||||
</template>
|
||||
|
||||
<template #default>
|
||||
<slot />
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref} from "vue";
|
||||
import Fullscreen from "vue-material-design-icons/Fullscreen.vue"
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: undefined
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue"])
|
||||
|
||||
const fullScreen = ref(false);
|
||||
|
||||
const toggleFullScreen = () => {
|
||||
fullScreen.value = !fullScreen.value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
button.full-screen {
|
||||
font-size: 24px;
|
||||
}
|
||||
</style>
|
||||
@@ -99,7 +99,7 @@
|
||||
title: this.title || "Error",
|
||||
message: h("div", children),
|
||||
position: "bottom-right",
|
||||
type: "error",
|
||||
type: this.message.variant,
|
||||
duration: 0,
|
||||
dangerouslyUseHTMLString: true,
|
||||
customClass: "error-notification" + (children.length > 1 ? " large" : "")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<el-tabs class="router-link" :class="{top: top}" v-model="activeName">
|
||||
<el-tab-pane
|
||||
v-for="tab in tabs"
|
||||
v-for="tab in tabs.filter(t => !t.hidden)"
|
||||
:key="tab.name"
|
||||
:label="tab.title"
|
||||
:name="tab.name || 'default'"
|
||||
@@ -104,7 +104,7 @@
|
||||
return {[this.activeTab.containerClass] : true};
|
||||
}
|
||||
|
||||
return {"container" : true}
|
||||
return {"container" : true, "mt-4": true};
|
||||
},
|
||||
activeTab() {
|
||||
return this.tabs
|
||||
|
||||
@@ -120,6 +120,7 @@
|
||||
<el-table-column column-key="disable" class-name="row-action">
|
||||
<template #default="scope">
|
||||
<el-switch
|
||||
v-if="!scope.row.missingSource"
|
||||
size="small"
|
||||
:active-text="$t('enabled')"
|
||||
:model-value="!scope.row.disabled"
|
||||
@@ -127,6 +128,9 @@
|
||||
class="switch-text"
|
||||
:active-action-icon="Check"
|
||||
/>
|
||||
<el-tooltip v-else :content="'flow source not found'">
|
||||
<AlertCircle class="trigger-issue-icon" />
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -154,6 +158,7 @@
|
||||
import action from "../../models/action";
|
||||
import TopNavBar from "../layout/TopNavBar.vue";
|
||||
import Check from "vue-material-design-icons/Check.vue";
|
||||
import AlertCircle from "vue-material-design-icons/AlertCircle.vue";
|
||||
</script>
|
||||
<script>
|
||||
import NamespaceSelect from "../namespace/NamespaceSelect.vue";
|
||||
@@ -251,9 +256,21 @@
|
||||
},
|
||||
triggersMerged() {
|
||||
return this.triggers.map(triggers => {
|
||||
return {...triggers.abstractTrigger, ...triggers.triggerContext, codeDisabled: triggers.abstractTrigger.disabled}
|
||||
return {
|
||||
...triggers?.abstractTrigger,
|
||||
...triggers.triggerContext,
|
||||
codeDisabled: triggers?.abstractTrigger?.disabled,
|
||||
// if we have no abstract trigger, it means that flow or trigger definition hasn't been found
|
||||
missingSource: !triggers.abstractTrigger
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</script>
|
||||
<style>
|
||||
.trigger-issue-icon{
|
||||
color: var(--bs-warning);
|
||||
font-size: 1.4em;
|
||||
}
|
||||
</style>
|
||||
@@ -28,13 +28,9 @@
|
||||
</el-form-item>
|
||||
</collapse>
|
||||
|
||||
<el-drawer
|
||||
<drawer
|
||||
v-if="isModalOpen"
|
||||
v-model="isModalOpen"
|
||||
destroy-on-close
|
||||
lock-scroll
|
||||
:append-to-body="true"
|
||||
size=""
|
||||
:title="$t('eval.title')"
|
||||
>
|
||||
<template #footer>
|
||||
@@ -52,7 +48,7 @@
|
||||
<p><strong>{{ debugError }}</strong></p>
|
||||
<pre class="mb-0">{{ debugStackTrace }}</pre>
|
||||
</el-alert>
|
||||
</el-drawer>
|
||||
</drawer>
|
||||
|
||||
<el-table
|
||||
:data="outputsPaginated"
|
||||
@@ -99,6 +95,7 @@
|
||||
import Pagination from "../layout/Pagination.vue";
|
||||
import {apiUrl} from "override/utils/route";
|
||||
import SubFlowLink from "../flows/SubFlowLink.vue";
|
||||
import Drawer from "../Drawer.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -107,6 +104,7 @@
|
||||
VarValue,
|
||||
Editor,
|
||||
Collapse,
|
||||
Drawer
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -25,9 +25,9 @@
|
||||
</ul>
|
||||
</template>
|
||||
</top-nav-bar>
|
||||
<template v-if="ready">
|
||||
<tabs :route-name="$route.params && $route.params.id ? 'executions/update': ''" @follow="follow" :tabs="tabs" />
|
||||
</template>
|
||||
<div class="full-space" v-loading="!ready">
|
||||
<tabs :route-name="$route.params && $route.params.id ? 'executions/update': ''" @follow="follow" :tabs="tabs" v-if="ready" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -115,6 +115,16 @@
|
||||
|
||||
this.$store.commit("execution/setExecution", execution);
|
||||
}
|
||||
// sse.onerror doesnt return the details of the error
|
||||
// but as our emitter can only throw an error on 404
|
||||
// we can safely assume that the error
|
||||
this.sse.onerror = () => {
|
||||
this.$store.dispatch("core/showMessage", {
|
||||
variant: "error",
|
||||
title: this.$t("error"),
|
||||
message: this.$t("errors.404.flow or execution"),
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
closeSSE() {
|
||||
@@ -268,3 +278,8 @@
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
.full-space {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
</style>
|
||||
@@ -2,16 +2,12 @@
|
||||
<el-button size="small" type="primary" :icon="EyeOutline" @click="getFilePreview">
|
||||
Preview
|
||||
</el-button>
|
||||
<el-drawer
|
||||
<drawer
|
||||
v-if="selectedPreview === value && filePreview"
|
||||
v-model="isPreviewOpen"
|
||||
destroy-on-close
|
||||
lock-scroll
|
||||
size=""
|
||||
:append-to-body="true"
|
||||
>
|
||||
<template #header>
|
||||
<h3>{{ $t("preview") }}</h3>
|
||||
{{ $t("preview") }}
|
||||
</template>
|
||||
<template #default>
|
||||
<el-alert v-if="filePreview.truncated" show-icon type="warning" :closable="false" class="mb-2">
|
||||
@@ -58,7 +54,7 @@
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -70,9 +66,10 @@
|
||||
import ListPreview from "../ListPreview.vue";
|
||||
import {mapGetters, mapState} from "vuex";
|
||||
import Markdown from "../layout/Markdown.vue";
|
||||
import Drawer from "../Drawer.vue";
|
||||
|
||||
export default {
|
||||
components: {Markdown, ListPreview, Editor},
|
||||
components: {Markdown, ListPreview, Editor, Drawer},
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
|
||||
@@ -6,17 +6,13 @@
|
||||
{{ $t('metrics') }}
|
||||
</el-dropdown-item>
|
||||
|
||||
<el-drawer
|
||||
<drawer
|
||||
v-if="isOpen"
|
||||
v-model="isOpen"
|
||||
:title="$t('metrics')"
|
||||
destroy-on-close
|
||||
:append-to-body="true"
|
||||
size=""
|
||||
direction="ltr"
|
||||
>
|
||||
<metrics-table ref="table" :task-run-id="taskRun.id" :execution="execution" />
|
||||
</el-drawer>
|
||||
</drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -26,10 +22,12 @@
|
||||
|
||||
<script>
|
||||
import MetricsTable from "./MetricsTable.vue";
|
||||
import Drawer from "../Drawer.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MetricsTable
|
||||
MetricsTable,
|
||||
Drawer
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -7,21 +7,17 @@
|
||||
{{ $t('outputs') }}
|
||||
</el-dropdown-item>
|
||||
|
||||
<el-drawer
|
||||
<drawer
|
||||
v-if="isOpen"
|
||||
v-model="isOpen"
|
||||
:title="$t('outputs')"
|
||||
destroy-on-close
|
||||
:append-to-body="true"
|
||||
size=""
|
||||
direction="ltr"
|
||||
>
|
||||
<vars
|
||||
:execution="execution"
|
||||
class="table-unrounded mt-1"
|
||||
:data="outputs"
|
||||
/>
|
||||
</el-drawer>
|
||||
</drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -30,10 +26,12 @@
|
||||
|
||||
<script>
|
||||
import Vars from "../executions/Vars.vue";
|
||||
import Drawer from "../Drawer.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Vars,
|
||||
Drawer,
|
||||
},
|
||||
props: {
|
||||
outputs: {
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
|
||||
<div v-if="execution.trigger" class="mt-4">
|
||||
<h5>{{ $t("trigger") }}</h5>
|
||||
<vars :execution="execution" :data="execution.trigger" />
|
||||
<vars :execution="execution" :data="triggerVariables" />
|
||||
</div>
|
||||
|
||||
<div v-if="execution.inputs" class="mt-4">
|
||||
@@ -183,6 +183,14 @@
|
||||
})
|
||||
})
|
||||
return inputs;
|
||||
},
|
||||
// This is used to display correctly trigger variables
|
||||
triggerVariables() {
|
||||
let trigger = this.execution.trigger
|
||||
trigger["trigger"] = this.execution.trigger.variables
|
||||
delete trigger["variables"]
|
||||
|
||||
return trigger
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
|
||||
<p v-html="$t(replayOrRestart + ' confirm', {id: execution.id})" />
|
||||
|
||||
<el-form>
|
||||
<el-form v-if="revisionsOptions && revisionsOptions.length > 1">
|
||||
<p class="text-muted">
|
||||
{{ $t("restart change revision") }}
|
||||
</p>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<el-table stripe table-layout="auto" fixed :data="variables">
|
||||
<el-table-column prop="key" rowspan="3" :label="$t('name')">
|
||||
<el-table-column prop="key" min-width="500" :label="$t('name')">
|
||||
<template #default="scope">
|
||||
<code>{{ scope.row.key }}</code>
|
||||
<code class="key-col">{{ scope.row.key }}</code>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
@@ -50,3 +50,8 @@
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
.key-col {
|
||||
min-width: 200px;
|
||||
}
|
||||
</style>
|
||||
@@ -40,6 +40,10 @@
|
||||
value: "P1D",
|
||||
label: "datepicker.last24hours"
|
||||
},
|
||||
{
|
||||
value: "P2D",
|
||||
label: "datepicker.last48hours"
|
||||
},
|
||||
{
|
||||
value: "P7D",
|
||||
label: "datepicker.last7days"
|
||||
|
||||
@@ -20,14 +20,17 @@
|
||||
const store = useStore();
|
||||
const axios = inject("axios")
|
||||
const router = getCurrentInstance().appContext.config.globalProperties.$router;
|
||||
const t = getCurrentInstance().appContext.config.globalProperties.$t;
|
||||
|
||||
const loaded = ref([]);
|
||||
const dependencies = ref({
|
||||
nodes: [],
|
||||
edges: []
|
||||
});
|
||||
const expanded = ref([]);
|
||||
|
||||
const isLoading = ref(false);
|
||||
const initialLoad = ref(true);
|
||||
|
||||
const load = (options) => {
|
||||
isLoading.value = true;
|
||||
@@ -41,8 +44,21 @@
|
||||
dependencies.value.edges.push(...response.data.edges)
|
||||
}
|
||||
|
||||
if (!initialLoad.value) {
|
||||
let newNodes = new Set(response.data.nodes.map(n => n.uid))
|
||||
let oldNodes = new Set(getNodes.value.map(n => n.id))
|
||||
console.log(response.data.nodes)
|
||||
console.log(getNodes.value)
|
||||
store.dispatch("core/showMessage", {
|
||||
variant: "success",
|
||||
title: t("dependencies loaded"),
|
||||
message: t("loaded x dependencies", [...newNodes].filter(node => !oldNodes.has(node)).length),
|
||||
})
|
||||
}
|
||||
|
||||
removeEdges(getEdges.value)
|
||||
removeNodes(getNodes.value)
|
||||
initialLoad.value = false
|
||||
|
||||
nextTick(() => {
|
||||
generateGraph();
|
||||
@@ -59,6 +75,7 @@
|
||||
};
|
||||
|
||||
const expand = (data) => {
|
||||
expanded.value.push(data.node.uid)
|
||||
load({namespace: data.namespace, id: data.flowId})
|
||||
};
|
||||
|
||||
@@ -110,7 +127,8 @@
|
||||
flowId: node.id,
|
||||
current: node.namespace === route.params.namespace && node.id === route.params.id,
|
||||
color: "pink",
|
||||
link: true
|
||||
link: true,
|
||||
expandEnabled: !expanded.value.includes(node.uid)
|
||||
}
|
||||
}]);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<router-link v-if="flow" :to="{name: 'flows/create', query: {copy: true}}">
|
||||
<router-link v-if="flow && canCreate" :to="{name: 'flows/create', query: {copy: true}}">
|
||||
<el-button :icon="icon.ContentCopy" size="large">
|
||||
{{ $t('copy') }}
|
||||
</el-button>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
/>
|
||||
</el-select>
|
||||
<el-row :gutter="15">
|
||||
<el-col :span="12">
|
||||
<el-col :span="12" v-if="revisionLeft !== undefined">
|
||||
<div class="revision-select mb-3">
|
||||
<el-select v-model="revisionLeft">
|
||||
<el-option
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
<crud class="mt-3" permission="FLOW" :detail="{namespace: $route.params.namespace, flowId: $route.params.id, revision: revisionNumber(revisionLeft)}" />
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-col :span="12" v-if="revisionRight !== undefined">
|
||||
<div class="revision-select mb-3">
|
||||
<el-select v-model="revisionRight">
|
||||
<el-option
|
||||
@@ -74,13 +74,13 @@
|
||||
:show-doc="false"
|
||||
/>
|
||||
|
||||
<el-drawer v-if="isModalOpen" v-model="isModalOpen" destroy-on-close :append-to-body="true" size="">
|
||||
<drawer v-if="isModalOpen" v-model="isModalOpen">
|
||||
<template #header>
|
||||
<h5>{{ $t("revision") + `: ` + revision }}</h5>
|
||||
</template>
|
||||
|
||||
<editor v-model="revisionYaml" lang="yaml" />
|
||||
</el-drawer>
|
||||
</drawer>
|
||||
</div>
|
||||
<div v-else>
|
||||
<el-alert class="mb-0" show-icon :closable="false">
|
||||
@@ -99,10 +99,11 @@
|
||||
import YamlUtils from "../../utils/yamlUtils";
|
||||
import Editor from "../../components/inputs/Editor.vue";
|
||||
import Crud from "override/components/auth/Crud.vue";
|
||||
import Drawer from "../Drawer.vue";
|
||||
import {saveFlowTemplate} from "../../utils/flowTemplate";
|
||||
|
||||
export default {
|
||||
components: {Editor, Crud},
|
||||
components: {Editor, Crud, Drawer},
|
||||
created() {
|
||||
this.load();
|
||||
},
|
||||
@@ -218,8 +219,8 @@
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
revisionLeft: 0,
|
||||
revisionRight: 0,
|
||||
revisionLeft: undefined,
|
||||
revisionRight: undefined,
|
||||
revision: undefined,
|
||||
revisionId: undefined,
|
||||
revisionYaml: undefined,
|
||||
|
||||
@@ -158,13 +158,9 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-drawer
|
||||
<drawer
|
||||
v-if="isOpen"
|
||||
v-model="isOpen"
|
||||
destroy-on-close
|
||||
lock-scroll
|
||||
size=""
|
||||
:append-to-body="true"
|
||||
>
|
||||
<template #header>
|
||||
<code>{{ triggerId }}</code>
|
||||
@@ -172,7 +168,7 @@
|
||||
|
||||
<markdown v-if="triggerDefinition && triggerDefinition.description" :source="triggerDefinition.description" />
|
||||
<vars :data="modalData" />
|
||||
</el-drawer>
|
||||
</drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -193,12 +189,13 @@
|
||||
import Kicon from "../Kicon.vue"
|
||||
import DateAgo from "../layout/DateAgo.vue";
|
||||
import Vars from "../executions/Vars.vue";
|
||||
import Drawer from "../Drawer.vue";
|
||||
import permission from "../../models/permission";
|
||||
import action from "../../models/action";
|
||||
import moment from "moment";
|
||||
|
||||
export default {
|
||||
components: {Markdown, Kicon, DateAgo, Vars},
|
||||
components: {Markdown, Kicon, DateAgo, Vars, Drawer},
|
||||
data() {
|
||||
return {
|
||||
triggerId: undefined,
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link :to="{name: 'flows/create'}">
|
||||
<router-link :to="{name: 'flows/create'}" v-if="canCreate">
|
||||
<el-button :icon="Plus" type="primary">
|
||||
{{ $t('create') }}
|
||||
</el-button>
|
||||
@@ -290,6 +290,9 @@
|
||||
canCheck() {
|
||||
return this.canRead || this.canDelete || this.canUpdate;
|
||||
},
|
||||
canCreate() {
|
||||
return this.user && this.user.isAllowed(permission.FLOW, action.CREATE, this.$route.query.namespace);
|
||||
},
|
||||
canRead() {
|
||||
return this.user && this.user.isAllowed(permission.FLOW, action.READ, this.$route.query.namespace);
|
||||
},
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
<template>
|
||||
<div class="w-100 d-flex flex-column align-items-center">
|
||||
<el-drawer
|
||||
<drawer
|
||||
v-if="isEditOpen"
|
||||
v-model="isEditOpen"
|
||||
destroy-on-close
|
||||
size=""
|
||||
:append-to-body="true"
|
||||
>
|
||||
<template #header>
|
||||
<code>inputs</code>
|
||||
@@ -40,7 +37,7 @@
|
||||
:definitions="inputSchema.schema.definitions"
|
||||
/>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</drawer>
|
||||
<div class="w-100">
|
||||
<div>
|
||||
<div class="d-flex w-100" v-for="(input, index) in newInputs" :key="index">
|
||||
@@ -74,8 +71,10 @@
|
||||
</script>
|
||||
<script>
|
||||
import {mapState} from "vuex";
|
||||
import Drawer from "../Drawer.vue";
|
||||
|
||||
export default {
|
||||
components: {Drawer},
|
||||
emits: ["update:modelValue"],
|
||||
props: {
|
||||
inputs: {
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
<template>
|
||||
<div class="w-100">
|
||||
<el-drawer
|
||||
<drawer
|
||||
v-if="isEditOpen"
|
||||
v-model="isEditOpen"
|
||||
destroy-on-close
|
||||
size=""
|
||||
:append-to-body="true"
|
||||
>
|
||||
<template #header>
|
||||
<code>variables</code>
|
||||
@@ -43,7 +40,7 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-drawer>
|
||||
</drawer>
|
||||
<div class="w-100">
|
||||
<div v-if="variables">
|
||||
<div class="d-flex w-100" v-for="(value, index) in newVariables" :key="index">
|
||||
@@ -83,9 +80,10 @@
|
||||
|
||||
<script>
|
||||
import Editor from "../inputs/Editor.vue";
|
||||
import Drawer from "../Drawer.vue";
|
||||
|
||||
export default {
|
||||
components: {Editor},
|
||||
components: {Editor, Drawer},
|
||||
emits: ["update:modelValue"],
|
||||
props: {
|
||||
variables: {
|
||||
|
||||
@@ -6,13 +6,9 @@
|
||||
ref="taskEdit"
|
||||
>
|
||||
<span v-if="component !== 'el-button' && !isHidden">{{ $t("show task source") }}</span>
|
||||
<el-drawer
|
||||
<drawer
|
||||
v-if="isModalOpen"
|
||||
v-model="isModalOpen"
|
||||
destroy-on-close
|
||||
lock-scroll
|
||||
size=""
|
||||
:append-to-body="true"
|
||||
>
|
||||
<template #header>
|
||||
<code>{{ taskId || task?.id || $t("add task") }}</code>
|
||||
@@ -81,7 +77,7 @@
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-drawer>
|
||||
</drawer>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
@@ -94,6 +90,7 @@
|
||||
import YamlUtils from "../../utils/yamlUtils";
|
||||
import Editor from "../inputs/Editor.vue";
|
||||
import TaskEditor from "./TaskEditor.vue";
|
||||
import Drawer from "../Drawer.vue";
|
||||
import {canSaveFlowTemplate, saveFlowTemplate} from "../../utils/flowTemplate";
|
||||
import {mapGetters, mapState} from "vuex";
|
||||
import Utils from "../../utils/utils";
|
||||
@@ -102,7 +99,7 @@
|
||||
import {SECTIONS} from "../../utils/constants";
|
||||
|
||||
export default {
|
||||
components: {Editor, TaskEditor, Markdown, ValidationError},
|
||||
components: {Editor, TaskEditor, Drawer, Markdown, ValidationError},
|
||||
emits: ["update:task", "close"],
|
||||
props: {
|
||||
component: {
|
||||
|
||||
@@ -8,13 +8,9 @@
|
||||
</template>
|
||||
</el-input>
|
||||
|
||||
|
||||
<el-drawer
|
||||
<drawer
|
||||
v-if="isOpen"
|
||||
v-model="isOpen"
|
||||
destroy-on-close
|
||||
size=""
|
||||
:append-to-body="true"
|
||||
>
|
||||
<template #header>
|
||||
<code>{{ root }}</code>
|
||||
@@ -54,7 +50,7 @@
|
||||
{{ $t("save") }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -64,9 +60,11 @@
|
||||
|
||||
<script>
|
||||
import Task from "./Task"
|
||||
import Drawer from "../../Drawer.vue"
|
||||
|
||||
export default {
|
||||
mixins: [Task],
|
||||
components: {Drawer},
|
||||
data() {
|
||||
return {
|
||||
isOpen: false,
|
||||
|
||||
@@ -8,13 +8,9 @@
|
||||
</template>
|
||||
</el-input>
|
||||
|
||||
|
||||
<el-drawer
|
||||
<drawer
|
||||
v-if="isOpen"
|
||||
v-model="isOpen"
|
||||
destroy-on-close
|
||||
size=""
|
||||
:append-to-body="true"
|
||||
>
|
||||
<template #header>
|
||||
<code>{{ root }}</code>
|
||||
@@ -33,7 +29,7 @@
|
||||
{{ $t('save') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -43,8 +39,11 @@
|
||||
|
||||
<script>
|
||||
import Task from "./Task"
|
||||
import Drawer from "../../Drawer.vue"
|
||||
|
||||
export default {
|
||||
mixins: [Task],
|
||||
components: {Drawer},
|
||||
data() {
|
||||
return {
|
||||
isOpen: false,
|
||||
|
||||
@@ -8,13 +8,10 @@
|
||||
</template>
|
||||
</el-input>
|
||||
|
||||
<el-drawer
|
||||
<drawer
|
||||
v-if="isOpen"
|
||||
v-model="isOpen"
|
||||
:title="root"
|
||||
destroy-on-close
|
||||
size=""
|
||||
:append-to-body="true"
|
||||
>
|
||||
<template #header>
|
||||
<code>{{ root }}</code>
|
||||
@@ -33,7 +30,7 @@
|
||||
{{ $t('save') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -46,10 +43,11 @@
|
||||
import Task from "./Task"
|
||||
import YamlUtils from "../../../utils/yamlUtils";
|
||||
import TaskEditor from "../TaskEditor.vue"
|
||||
import Drawer from "../../Drawer.vue"
|
||||
|
||||
export default {
|
||||
mixins: [Task],
|
||||
components: {TaskEditor},
|
||||
components: {TaskEditor, Drawer},
|
||||
emits: ["update:modelValue"],
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -8,12 +8,9 @@
|
||||
</template>
|
||||
</el-input>
|
||||
|
||||
<el-drawer
|
||||
<drawer
|
||||
v-if="isOpen"
|
||||
v-model="isOpen"
|
||||
destroy-on-close
|
||||
size=""
|
||||
:append-to-body="true"
|
||||
>
|
||||
<template #header>
|
||||
<code>{{ root }}</code>
|
||||
@@ -31,7 +28,7 @@
|
||||
{{ $t('save') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -44,10 +41,11 @@
|
||||
import Task from "./Task"
|
||||
import YamlUtils from "../../../utils/yamlUtils";
|
||||
import TaskEditor from "../TaskEditor.vue"
|
||||
import Drawer from "../../Drawer.vue"
|
||||
|
||||
export default {
|
||||
mixins: [Task],
|
||||
components: {TaskEditor},
|
||||
components: {TaskEditor, Drawer},
|
||||
emits: ["update:modelValue"],
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<top-nav-bar v-if="!embed" :title="routeInfo.title">
|
||||
<template #additional-right>
|
||||
<template #additional-right v-if="canCreate">
|
||||
<ul>
|
||||
<li>
|
||||
<router-link :to="{name: 'flows/create'}">
|
||||
@@ -333,6 +333,9 @@
|
||||
title: this.$t("homeDashboard.title"),
|
||||
};
|
||||
},
|
||||
canCreate() {
|
||||
return this.user.isAllowedGlobal(permission.FLOW, action.CREATE)
|
||||
},
|
||||
defaultFilters() {
|
||||
return {
|
||||
startDate: this.$moment(this.startDate).toISOString(true),
|
||||
|
||||
@@ -77,6 +77,7 @@
|
||||
navbar: {type: Boolean, default: true},
|
||||
input: {type: Boolean, default: false},
|
||||
fullHeight: {type: Boolean, default: true},
|
||||
customHeight: {type: Number, default: 7},
|
||||
theme: {type: String, default: undefined},
|
||||
placeholder: {type: [String, Number], default: ""},
|
||||
diffSideBySide: {type: Boolean, default: true},
|
||||
@@ -298,7 +299,7 @@
|
||||
|
||||
if (!this.fullHeight) {
|
||||
editor.onDidContentSizeChange(e => {
|
||||
this.$refs.container.style.height = (e.contentHeight + 7) + "px";
|
||||
this.$refs.container.style.height = (e.contentHeight + this.customHeight) + "px";
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
import Utils from "@kestra-io/ui-libs/src/utils/Utils";
|
||||
import {apiUrl} from "override/utils/route";
|
||||
import EditorButtons from "./EditorButtons.vue";
|
||||
import Drawer from "../Drawer.vue";
|
||||
|
||||
const store = useStore();
|
||||
const router = getCurrentInstance().appContext.config.globalProperties.$router;
|
||||
@@ -167,12 +168,13 @@
|
||||
}
|
||||
|
||||
const editorDomElement = ref(null);
|
||||
const editorWidthStorageKey = "editor-width";
|
||||
const editorWidthStorageKey = "editor-size";
|
||||
const editorWidth = ref(localStorage.getItem(editorWidthStorageKey));
|
||||
const validationDomElement = ref(null);
|
||||
const isLoading = ref(false);
|
||||
const haveChange = ref(props.isDirty)
|
||||
const flowYaml = ref("")
|
||||
const flowYamlOrigin = ref("")
|
||||
const newTrigger = ref(null)
|
||||
const isNewTriggerOpen = ref(false)
|
||||
const newError = ref(null)
|
||||
@@ -235,7 +237,7 @@
|
||||
|
||||
const initYamlSource = async () => {
|
||||
flowYaml.value = props.flow.source;
|
||||
|
||||
flowYamlOrigin.value = props.flow.source;
|
||||
if (flowHaveTasks()) {
|
||||
if ([editorViewTypes.TOPOLOGY, editorViewTypes.SOURCE_TOPOLOGY].includes(viewType.value)) {
|
||||
await fetchGraph();
|
||||
@@ -759,7 +761,7 @@
|
||||
:is-read-only="props.isReadOnly"
|
||||
:can-delete="canDelete()"
|
||||
:is-allowed-edit="isAllowedEdit()"
|
||||
:have-change="haveChange"
|
||||
:have-change="flowYaml !== flowYamlOrigin"
|
||||
:flow-have-tasks="flowHaveTasks()"
|
||||
:errors="flowErrors"
|
||||
:warnings="flowWarnings"
|
||||
@@ -825,13 +827,10 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-drawer
|
||||
<drawer
|
||||
v-if="isNewErrorOpen"
|
||||
v-model="isNewErrorOpen"
|
||||
title="Add a global error handler"
|
||||
destroy-on-close
|
||||
size=""
|
||||
:append-to-body="true"
|
||||
>
|
||||
<el-form label-position="top">
|
||||
<task-editor
|
||||
@@ -845,14 +844,11 @@
|
||||
{{ $t("save") }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
<el-drawer
|
||||
</drawer>
|
||||
<drawer
|
||||
v-if="isNewTriggerOpen"
|
||||
v-model="isNewTriggerOpen"
|
||||
title="Add a trigger"
|
||||
destroy-on-close
|
||||
size=""
|
||||
:append-to-body="true"
|
||||
>
|
||||
<el-form label-position="top">
|
||||
<task-editor
|
||||
@@ -866,13 +862,10 @@
|
||||
{{ $t("save") }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
<el-drawer
|
||||
</drawer>
|
||||
<drawer
|
||||
v-if="isEditMetadataOpen"
|
||||
v-model="isEditMetadataOpen"
|
||||
destroy-on-close
|
||||
size=""
|
||||
:append-to-body="true"
|
||||
>
|
||||
<template #header>
|
||||
<code>flow metadata</code>
|
||||
@@ -896,7 +889,7 @@
|
||||
{{ $t("save") }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</drawer>
|
||||
</div>
|
||||
<el-dialog v-if="confirmOutdatedSaveDialog" v-model="confirmOutdatedSaveDialog" destroy-on-close :append-to-body="true">
|
||||
<template #header>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import LogLevelSelector from "../logs/LogLevelSelector.vue";
|
||||
import TaskRunDetails from "../logs/TaskRunDetails.vue";
|
||||
import Collapse from "../layout/Collapse.vue";
|
||||
import Drawer from "../Drawer.vue";
|
||||
|
||||
// Topology
|
||||
import {
|
||||
@@ -340,12 +341,9 @@
|
||||
|
||||
<!-- Drawer to task informations (logs, description, ..) -->
|
||||
<!-- Assuming selectedTask is always the id and the required data for the opened drawer -->
|
||||
<el-drawer
|
||||
<drawer
|
||||
v-if="isDrawerOpen && selectedTask"
|
||||
v-model="isDrawerOpen"
|
||||
destroy-on-close
|
||||
size=""
|
||||
:append-to-body="true"
|
||||
>
|
||||
<template #header>
|
||||
<code>{{ selectedTask.id }}</code>
|
||||
@@ -373,7 +371,7 @@
|
||||
<div v-if="isShowDescriptionOpen">
|
||||
<markdown class="markdown-tooltip" :source="selectedTask.description" />
|
||||
</div>
|
||||
</el-drawer>
|
||||
</drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -32,12 +32,21 @@
|
||||
|
||||
<style lang="scss" scoped>
|
||||
#environment {
|
||||
margin-bottom: 0.5em;
|
||||
background-color: v-bind('color');
|
||||
margin-bottom: 1.5rem;
|
||||
text-align: center;
|
||||
margin-top: -1.25rem;
|
||||
|
||||
strong {
|
||||
color: var(--bs-body-bg);
|
||||
border: 1px solid v-bind('color');
|
||||
border-radius: var(--bs-border-radius);
|
||||
color: var(--bs-body-color);
|
||||
padding: 0.125rem 0.25rem;
|
||||
font-size: var(--font-size-sm);
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
max-width: 90%;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -7,28 +7,26 @@
|
||||
/>
|
||||
</a>
|
||||
|
||||
<el-drawer
|
||||
<drawer
|
||||
v-if="isOpen"
|
||||
v-model="isOpen"
|
||||
:title="title"
|
||||
destroy-on-close
|
||||
class="sm"
|
||||
size=""
|
||||
:append-to-body="true"
|
||||
>
|
||||
<markdown class="markdown-tooltip" :source="description" />
|
||||
</el-drawer>
|
||||
</drawer>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HelpCircle from "vue-material-design-icons/HelpCircle.vue";
|
||||
import Markdown from "./Markdown.vue";
|
||||
import Drawer from "../Drawer.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HelpCircle,
|
||||
Markdown
|
||||
Markdown,
|
||||
Drawer
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<CheckboxBlankCircle v-if="hasUnread" class="new" title="" />
|
||||
</el-button>
|
||||
|
||||
<el-drawer size="50%" v-if="isOpen" v-model="isOpen" destroy-on-close :append-to-body="true" class="sm" :title="$t('feeds.title')">
|
||||
<drawer v-if="isOpen" v-model="isOpen" :title="$t('feeds.title')">
|
||||
<div class="post" v-for="(feed, index) in feeds" :key="feed.id">
|
||||
<div v-if="feed.image" class="mt-2">
|
||||
<img class="float-end" :src="feed.image" alt="">
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
<el-divider v-if="index !== feeds.length - 1" />
|
||||
</div>
|
||||
</el-drawer>
|
||||
</drawer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -32,6 +32,7 @@
|
||||
import CheckboxBlankCircle from "vue-material-design-icons/CheckboxBlankCircle.vue";
|
||||
import Markdown from "./Markdown.vue";
|
||||
import DateAgo from "./DateAgo.vue";
|
||||
import Drawer from "../Drawer.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -39,7 +40,8 @@
|
||||
OpenInNew,
|
||||
CheckboxBlankCircle,
|
||||
Markdown,
|
||||
DateAgo
|
||||
DateAgo,
|
||||
Drawer
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -39,6 +39,8 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import {storageKeys} from "../../utils/constants";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
total: {type: Number, default: 0},
|
||||
@@ -61,14 +63,22 @@
|
||||
},
|
||||
methods: {
|
||||
initState() {
|
||||
let internalSize = parseInt(localStorage.getItem(storageKeys.PAGINATION_SIZE) || this.$route.query.size || this.size)
|
||||
let internalPage = parseInt(this.$route.query.page || this.page)
|
||||
this.$emit("page-changed", {
|
||||
page: internalPage,
|
||||
size: internalSize,
|
||||
});
|
||||
|
||||
return {
|
||||
internalSize: parseInt(this.$route.query.size || this.size),
|
||||
internalPage: parseInt(this.$route.query.page || this.page)
|
||||
internalSize: internalSize,
|
||||
internalPage: internalPage
|
||||
}
|
||||
},
|
||||
pageSizeChange(value) {
|
||||
pageSizeChange: function (value) {
|
||||
this.internalPage = 1;
|
||||
this.internalSize = value;
|
||||
localStorage.setItem(storageKeys.PAGINATION_SIZE, value);
|
||||
this.$emit("page-changed", {
|
||||
page: 1,
|
||||
size: this.internalSize,
|
||||
|
||||
@@ -214,7 +214,7 @@
|
||||
this.autofoldTextEditor = localStorage.getItem("autofoldTextEditor") === "true";
|
||||
this.guidedTour = localStorage.getItem("tourDoneOrSkip") === "true";
|
||||
this.logDisplay = localStorage.getItem("logDisplay") || logDisplayTypes.DEFAULT;
|
||||
this.editorFontSize = localStorage.getItem("editorFontSize") || 12;
|
||||
this.editorFontSize = parseInt(localStorage.getItem("editorFontSize")) || 12;
|
||||
this.editorFontFamily = localStorage.getItem("editorFontFamily") || "'Source Code Pro', monospace";
|
||||
this.executeFlowBehaviour = localStorage.getItem("executeFlowBehaviour") || "same tab";
|
||||
this.envName = store.getters["layout/envName"] || this.configs?.environment?.name;
|
||||
|
||||
@@ -44,6 +44,9 @@ export default {
|
||||
canSave() {
|
||||
return canSaveFlowTemplate(true, this.user, this.item, this.dataType);
|
||||
},
|
||||
canCreate() {
|
||||
return this.dataType === "flow" && this.user.isAllowed(permission.FLOW, action.CREATE, this.item.namespace)
|
||||
},
|
||||
canExecute() {
|
||||
return this.dataType === "flow" && this.user.isAllowed(permission.EXECUTION, action.CREATE, this.item.namespace)
|
||||
},
|
||||
|
||||
@@ -15,13 +15,14 @@
|
||||
<Environment />
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<span class="version">{{ configs.version }}</span>
|
||||
</template>
|
||||
<template #footer />
|
||||
|
||||
<template #toggle-icon>
|
||||
<chevron-right v-if="collapsed" />
|
||||
<chevron-left v-else />
|
||||
<el-button>
|
||||
<chevron-double-right v-if="collapsed" />
|
||||
<chevron-double-left v-else />
|
||||
</el-button>
|
||||
<span class="version">{{ configs.version }}</span>
|
||||
</template>
|
||||
</sidebar-menu>
|
||||
</template>
|
||||
@@ -29,28 +30,28 @@
|
||||
<script>
|
||||
import {SidebarMenu} from "vue-sidebar-menu";
|
||||
import Environment from "../../components/layout/Environment.vue";
|
||||
import ChevronLeft from "vue-material-design-icons/ChevronLeft.vue";
|
||||
import ChevronRight from "vue-material-design-icons/ChevronRight.vue";
|
||||
import ChevronDoubleLeft from "vue-material-design-icons/ChevronDoubleLeft.vue";
|
||||
import ChevronDoubleRight from "vue-material-design-icons/ChevronDoubleRight.vue";
|
||||
import FileTreeOutline from "vue-material-design-icons/FileTreeOutline.vue";
|
||||
import ContentCopy from "vue-material-design-icons/ContentCopy.vue";
|
||||
import TimelineClockOutline from "vue-material-design-icons/TimelineClockOutline.vue";
|
||||
import TimelineTextOutline from "vue-material-design-icons/TimelineTextOutline.vue";
|
||||
import NotebookOutline from "vue-material-design-icons/NotebookOutline.vue";
|
||||
import Ballot from "vue-material-design-icons/Ballot.vue";
|
||||
import ChartTimeline from "vue-material-design-icons/ChartTimeline.vue";
|
||||
import BallotOutline from "vue-material-design-icons/BallotOutline.vue";
|
||||
import FolderEditOutline from "vue-material-design-icons/FolderEditOutline.vue";
|
||||
import AccountSupervisorOutline from "vue-material-design-icons/AccountSupervisorOutline.vue";
|
||||
import ShieldAccountVariantOutline from "vue-material-design-icons/ShieldAccountVariantOutline.vue";
|
||||
import CogOutline from "vue-material-design-icons/CogOutline.vue";
|
||||
import ViewDashboardVariantOutline from "vue-material-design-icons/ViewDashboardVariantOutline.vue";
|
||||
import TimerCogOutline from "vue-material-design-icons/TimerCogOutline.vue";
|
||||
import {mapState} from "vuex";
|
||||
import AccountHardHatOutline from "vue-material-design-icons/AccountHardHatOutline.vue";
|
||||
import ChartBoxOutline from "vue-material-design-icons/ChartBoxOutline.vue";
|
||||
import ServerOutline from "vue-material-design-icons/ServerOutline.vue";
|
||||
import {shallowRef} from "vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
ChevronDoubleLeft,
|
||||
ChevronDoubleRight,
|
||||
SidebarMenu,
|
||||
Environment
|
||||
},
|
||||
@@ -146,7 +147,7 @@
|
||||
routes: this.routeStartWith("taskruns"),
|
||||
title: this.$t("taskruns"),
|
||||
icon: {
|
||||
element: shallowRef(TimelineTextOutline),
|
||||
element: shallowRef(ChartTimeline),
|
||||
class: "menu-icon"
|
||||
},
|
||||
hidden: !this.configs.isTaskRunEnabled
|
||||
@@ -156,7 +157,7 @@
|
||||
routes: this.routeStartWith("logs"),
|
||||
title: this.$t("logs"),
|
||||
icon: {
|
||||
element: shallowRef(NotebookOutline),
|
||||
element: shallowRef(TimelineTextOutline),
|
||||
class: "menu-icon"
|
||||
},
|
||||
},
|
||||
@@ -165,7 +166,7 @@
|
||||
routes: this.routeStartWith("blueprints"),
|
||||
title: this.$t("blueprints.title"),
|
||||
icon: {
|
||||
element: shallowRef(Ballot),
|
||||
element: shallowRef(BallotOutline),
|
||||
class: "menu-icon"
|
||||
},
|
||||
},
|
||||
@@ -173,7 +174,7 @@
|
||||
title: this.$t("administration"),
|
||||
routes: this.routeStartWith("admin"),
|
||||
icon: {
|
||||
element: shallowRef(AccountSupervisorOutline),
|
||||
element: shallowRef(ShieldAccountVariantOutline),
|
||||
class: "menu-icon"
|
||||
},
|
||||
child: [
|
||||
@@ -191,7 +192,7 @@
|
||||
routes: this.routeStartWith("admin/workers"),
|
||||
title: this.$t("workers"),
|
||||
icon: {
|
||||
element: shallowRef(AccountHardHatOutline),
|
||||
element: shallowRef(ServerOutline),
|
||||
class: "menu-icon"
|
||||
},
|
||||
},
|
||||
@@ -302,13 +303,14 @@
|
||||
span.version {
|
||||
transition: 0.2s all;
|
||||
white-space: nowrap;
|
||||
font-size: var(--el-font-size-extra-small);
|
||||
font-size: var(--font-size-xs);
|
||||
text-align: center;
|
||||
display: block;
|
||||
color: var(--bs-gray-400);
|
||||
color: var(--bs-gray-600);
|
||||
width: auto;
|
||||
|
||||
html.dark & {
|
||||
color: var(--bs-gray-600);
|
||||
color: var(--bs-gray-800);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,11 +355,21 @@
|
||||
}
|
||||
|
||||
.vsm--toggle-btn {
|
||||
padding-top: 4px;
|
||||
padding-top: 16px;
|
||||
padding-bottom: 16px;
|
||||
font-size: 20px;
|
||||
background: transparent;
|
||||
color: var(--bs-secondary);
|
||||
height: 30px;
|
||||
border-top: 1px solid var(--bs-border-color);
|
||||
|
||||
.el-button {
|
||||
padding: 8px;
|
||||
margin-right: 15px;
|
||||
transition: margin-right 0.2s ease;
|
||||
html.dark & {
|
||||
background: var(--bs-gray-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -410,8 +422,13 @@
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.el-button {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
span.version {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -707,6 +707,10 @@ form.ks-horizontal {
|
||||
}
|
||||
}
|
||||
|
||||
&.full-screen {
|
||||
width: 99% !important;
|
||||
}
|
||||
|
||||
.el-drawer__header {
|
||||
padding: var(--spacer);
|
||||
margin-bottom: 0;
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
"last1hour": "Last 1 hour",
|
||||
"last12hours": "Last 12 hours",
|
||||
"last24hours": "Last 24 hours",
|
||||
"last48hours": "Last 48 hours",
|
||||
"last7days": "Last 7 days",
|
||||
"last30days": "Last 30 days",
|
||||
"last365days": "Last 365 days",
|
||||
@@ -293,7 +294,8 @@
|
||||
"errors": {
|
||||
"404": {
|
||||
"title": "Page not found",
|
||||
"content": "The requested URL was not found on this server. <span class=\"text-muted\">That’s all we know.</span>"
|
||||
"content": "The requested URL was not found on this server. <span class=\"text-muted\">That’s all we know.</span>",
|
||||
"flow or execution": "The flow or execution you are looking for does not exist."
|
||||
}
|
||||
},
|
||||
"copy logs": "Copy logs",
|
||||
@@ -597,7 +599,9 @@
|
||||
"Set labels": "Set labels",
|
||||
"Set labels to execution": "Add or update the labels of the execution <code>{id}</code>",
|
||||
"Set labels done": "Successfully set the labels of the execution",
|
||||
"bulk set labels": "Are you sure you want to set labels to <code>{executionCount}</code> executions(s)?"
|
||||
"bulk set labels": "Are you sure you want to set labels to <code>{executionCount}</code> executions(s)?",
|
||||
"dependencies loaded": "Dependencies loaded",
|
||||
"loaded x dependencies": "{count} dependencies loaded"
|
||||
},
|
||||
"fr": {
|
||||
"id": "Identifiant",
|
||||
@@ -890,7 +894,8 @@
|
||||
"errors": {
|
||||
"404": {
|
||||
"title": "Page introuvable",
|
||||
"content": "L'URL demandé est introuvable sur ce serveur. <span class=\"text-muted\">C'est tout ce que nous savons.</span>"
|
||||
"content": "L'URL demandé est introuvable sur ce serveur. <span class=\"text-muted\">C'est tout ce que nous savons.</span>",
|
||||
"flow or execution": "Le flow ou l'exécution demandé est introuvable."
|
||||
}
|
||||
},
|
||||
"copy logs": "Copier les logs",
|
||||
@@ -1185,7 +1190,9 @@
|
||||
"Set labels": "Ajouter des labels",
|
||||
"Set labels to execution": "Ajouter ou mettre à jour des labels à l'exécution <code>{id}</code>",
|
||||
"Set labels done": "Labels ajoutés avec succès à l'exécution",
|
||||
"bulk set labels": "Etes-vous sûr de vouloir ajouter des labels à <code>{executionCount}</code> exécutions(s)?"
|
||||
"bulk set labels": "Etes-vous sûr de vouloir ajouter des labels à <code>{executionCount}</code> exécutions(s)?",
|
||||
"dependencies loaded": "Dépendances chargées",
|
||||
"loaded x dependencies": "{count} dépendances chargées"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,8 @@ export const storageKeys = {
|
||||
SELECTED_TENANT: "selectedTenant",
|
||||
EXECUTE_FLOW_BEHAVIOUR: "executeFlowBehaviour",
|
||||
DEFAULT_NAMESPACE: "defaultNamespace",
|
||||
LATEST_NAMESPACE: "latestNamespace"
|
||||
LATEST_NAMESPACE: "latestNamespace",
|
||||
PAGINATION_SIZE: "paginationSize",
|
||||
}
|
||||
|
||||
export const executeFlowBehaviours = {
|
||||
|
||||
@@ -27,11 +27,11 @@ import io.kestra.core.runners.RunContextFactory;
|
||||
import io.kestra.core.runners.RunnerUtils;
|
||||
import io.kestra.core.services.ConditionService;
|
||||
import io.kestra.core.services.ExecutionService;
|
||||
import io.kestra.core.services.GraphService;
|
||||
import io.kestra.core.storages.StorageContext;
|
||||
import io.kestra.core.storages.StorageInterface;
|
||||
import io.kestra.core.tenant.TenantService;
|
||||
import io.kestra.core.utils.Await;
|
||||
import io.kestra.core.utils.GraphUtils;
|
||||
import io.kestra.webserver.responses.BulkErrorResponse;
|
||||
import io.kestra.webserver.responses.BulkResponse;
|
||||
import io.kestra.webserver.responses.PagedResults;
|
||||
@@ -58,6 +58,7 @@ import io.micronaut.http.annotation.PathVariable;
|
||||
import io.micronaut.http.annotation.Post;
|
||||
import io.micronaut.http.annotation.Put;
|
||||
import io.micronaut.http.annotation.QueryValue;
|
||||
import io.micronaut.http.exceptions.HttpStatusException;
|
||||
import io.micronaut.http.multipart.StreamingFileUpload;
|
||||
import io.micronaut.http.server.types.files.StreamedFile;
|
||||
import io.micronaut.http.sse.Event;
|
||||
@@ -94,6 +95,7 @@ import java.nio.charset.UnsupportedCharsetException;
|
||||
import java.time.Duration;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -114,6 +116,9 @@ public class ExecutionController {
|
||||
@Inject
|
||||
protected ExecutionRepositoryInterface executionRepository;
|
||||
|
||||
@Inject
|
||||
private GraphService graphService;
|
||||
|
||||
@Inject
|
||||
private RunnerUtils runnerUtils;
|
||||
|
||||
@@ -216,7 +221,7 @@ public class ExecutionController {
|
||||
);
|
||||
|
||||
return flow
|
||||
.map(throwFunction(value -> GraphUtils.flowGraph(value, execution)))
|
||||
.map(throwFunction(value -> graphService.flowGraph(value, null, execution)))
|
||||
.orElse(null);
|
||||
}))
|
||||
.orElse(null);
|
||||
@@ -1003,7 +1008,7 @@ public class ExecutionController {
|
||||
);
|
||||
}
|
||||
|
||||
for(Execution execution : executions) {
|
||||
for (Execution execution : executions) {
|
||||
Execution resumeExecution = this.executionService.resume(execution, State.Type.RUNNING);
|
||||
this.executionQueue.emit(resumeExecution);
|
||||
}
|
||||
@@ -1103,11 +1108,25 @@ public class ExecutionController {
|
||||
return Flux
|
||||
.<Event<Execution>>create(emitter -> {
|
||||
// already finished execution
|
||||
Execution execution = Await.until(
|
||||
() -> executionRepository.findById(tenantService.resolveTenant(), executionId).orElse(null),
|
||||
Duration.ofMillis(500)
|
||||
);
|
||||
Flow flow = flowRepository.findByExecution(execution);
|
||||
Execution execution = null;
|
||||
try {
|
||||
execution = Await.until(
|
||||
() -> executionRepository.findById(tenantService.resolveTenant(), executionId).orElse(null),
|
||||
Duration.ofMillis(500),
|
||||
Duration.ofSeconds(10)
|
||||
);
|
||||
} catch (TimeoutException e) {
|
||||
emitter.error(new HttpStatusException(HttpStatus.NOT_FOUND, "Unable to find the execution " + executionId));
|
||||
return;
|
||||
}
|
||||
|
||||
Flow flow;
|
||||
try {
|
||||
flow = flowRepository.findByExecution(execution);
|
||||
} catch (IllegalStateException e) {
|
||||
emitter.error(new HttpStatusException(HttpStatus.NOT_FOUND, "Unable to find the flow for the execution " + executionId));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isStopFollow(flow, execution)) {
|
||||
emitter.next(Event.of(execution).id("end"));
|
||||
@@ -1273,7 +1292,8 @@ public class ExecutionController {
|
||||
return HttpResponse.ok(BulkResponse.builder().count(executions.size()).build());
|
||||
}
|
||||
|
||||
public record SetLabelsByIdsRequest(List<String> executionsId, List<Label> executionLabels) {}
|
||||
public record SetLabelsByIdsRequest(List<String> executionsId, List<Label> executionLabels) {
|
||||
}
|
||||
|
||||
@ExecuteOn(TaskExecutors.IO)
|
||||
@Post(uri = "/labels/by-query")
|
||||
|
||||
@@ -81,6 +81,13 @@ public class TriggerController {
|
||||
if (flow.isEmpty()) {
|
||||
// Warn instead of throwing to avoid blocking the trigger UI
|
||||
log.warn(String.format("Flow %s not found for trigger %s", tc.getFlowId(), tc.getTriggerId()));
|
||||
triggers.add(Triggers.builder()
|
||||
.abstractTrigger(null)
|
||||
.triggerContext(tc)
|
||||
.build()
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
AbstractTrigger abstractTrigger = flow.get().getTriggers().stream().filter(t -> t.getId().equals(tc.getTriggerId())).findFirst().orElse(null);
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
io.micronaut.http.netty.cookies.NettyLaxServerCookieDecoder
|
||||
@@ -438,6 +438,72 @@ class ExecutionControllerTest extends JdbcH2ControllerTest {
|
||||
.forEach(state -> assertThat(state.getCurrent(), is(State.Type.SUCCESS)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void restartFromLastFailedWithPause() throws TimeoutException {
|
||||
final String flowId = "restart_pause_last_failed";
|
||||
|
||||
// Run execution until it ends
|
||||
Execution firstExecution = runnerUtils.runOne(null, TESTS_FLOW_NS, flowId, null, null);
|
||||
|
||||
assertThat(firstExecution.getTaskRunList().get(2).getState().getCurrent(), is(State.Type.FAILED));
|
||||
assertThat(firstExecution.getState().getCurrent(), is(State.Type.FAILED));
|
||||
|
||||
// Update task's command to make second execution successful
|
||||
Optional<Flow> flow = flowRepositoryInterface.findById(null, TESTS_FLOW_NS, flowId);
|
||||
assertThat(flow.isPresent(), is(true));
|
||||
|
||||
// Restart execution and wait until it finishes
|
||||
Execution finishedRestartedExecution = runnerUtils.awaitExecution(
|
||||
execution -> execution.getId().equals(firstExecution.getId()) &&
|
||||
execution.getTaskRunList().size() == 5 &&
|
||||
execution.getState().isTerminated(),
|
||||
() -> {
|
||||
Execution restartedExec = client.toBlocking().retrieve(
|
||||
HttpRequest
|
||||
.POST("/api/v1/executions/" + firstExecution.getId() + "/restart", ImmutableMap.of()),
|
||||
Execution.class
|
||||
);
|
||||
|
||||
assertThat(restartedExec, notNullValue());
|
||||
assertThat(restartedExec.getId(), is(firstExecution.getId()));
|
||||
assertThat(restartedExec.getParentId(), nullValue());
|
||||
assertThat(restartedExec.getTaskRunList().size(), is(4));
|
||||
assertThat(restartedExec.getState().getCurrent(), is(State.Type.RESTARTED));
|
||||
|
||||
IntStream
|
||||
.range(0, 2)
|
||||
.mapToObj(value -> restartedExec.getTaskRunList().get(value)).forEach(taskRun -> {
|
||||
assertThat(taskRun.getState().getCurrent(), is(State.Type.SUCCESS));
|
||||
assertThat(taskRun.getAttempts().size(), is(1));
|
||||
|
||||
assertThat(restartedExec.getTaskRunList().get(2).getState().getCurrent(), is(State.Type.RUNNING));
|
||||
assertThat(restartedExec.getTaskRunList().get(3).getState().getCurrent(), is(State.Type.RESTARTED));
|
||||
assertThat(restartedExec.getTaskRunList().get(2).getAttempts(), nullValue());
|
||||
assertThat(restartedExec.getTaskRunList().get(3).getAttempts().size(), is(1));
|
||||
});
|
||||
},
|
||||
Duration.ofSeconds(15)
|
||||
);
|
||||
|
||||
assertThat(finishedRestartedExecution, notNullValue());
|
||||
assertThat(finishedRestartedExecution.getId(), is(firstExecution.getId()));
|
||||
assertThat(finishedRestartedExecution.getParentId(), nullValue());
|
||||
assertThat(finishedRestartedExecution.getTaskRunList().size(), is(5));
|
||||
|
||||
assertThat(finishedRestartedExecution.getTaskRunList().get(0).getAttempts().size(), is(1));
|
||||
assertThat(finishedRestartedExecution.getTaskRunList().get(1).getAttempts().size(), is(1));
|
||||
assertThat(finishedRestartedExecution.getTaskRunList().get(2).getAttempts(), nullValue());
|
||||
assertThat(finishedRestartedExecution.getTaskRunList().get(2).getState().getHistories().stream().filter(state -> state.getState() == State.Type.PAUSED).count(), is(1L));
|
||||
assertThat(finishedRestartedExecution.getTaskRunList().get(3).getAttempts().size(), is(2));
|
||||
assertThat(finishedRestartedExecution.getTaskRunList().get(4).getAttempts().size(), is(1));
|
||||
|
||||
finishedRestartedExecution
|
||||
.getTaskRunList()
|
||||
.stream()
|
||||
.map(TaskRun::getState)
|
||||
.forEach(state -> assertThat(state.getCurrent(), is(State.Type.SUCCESS)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void downloadFile() throws TimeoutException {
|
||||
Execution execution = runnerUtils.runOne(null, TESTS_FLOW_NS, "inputs", null, (flow, execution1) -> runnerUtils.typedInputs(flow, execution1, inputs));
|
||||
|
||||
Reference in New Issue
Block a user