Compare commits

...

38 Commits

Author SHA1 Message Date
Loïc Mathieu
f76e55bafb chore: upgrade to version v0.15.7 2024-03-15 12:10:16 +01:00
Florian Hussonnois
686721c7ee chore: bump version to v0.15.6 2024-03-14 14:30:49 +01:00
brian-mulier-p
9f0e3bb14a fix(core): prevent flow validation from crashing (#3278) 2024-03-14 14:27:41 +01:00
Ludovic DEHON
1dc11fb44e fix(core): missing lombok annotation on new storage tasks 2024-03-14 13:53:04 +01:00
Loïc Mathieu
b2e41223b4 fix(core): null label value can crash the executor
A label with a null value will generates an exception while computing the list of variables inside the RunContext, failing to create a RunContext which can crash the executor.
2024-03-14 13:52:49 +01:00
YannC
579d30160c fix(ui): ExecutionRoot marge issue 2024-03-14 13:52:30 +01:00
Loïc Mathieu
bfe3548bd7 feat(core): add default inputs if not already set in variables
Polling triggers will create an execution that didn't contains default inputs, with this change default inputs will always be included in variables.

Fixes https://github.com/kestra-io/plugin-mqtt/issues/37
2024-03-14 13:51:43 +01:00
brian.mulier
eebcc3a010 chore(version): update to version 'v0.15.5' 2024-03-12 16:12:04 +01:00
YannC
6d65dda19a fix(ui): add top margin in tabs components 2024-03-12 16:00:15 +01:00
brian.mulier
6768950515 chore(deps): bump ui-libs to 0.0.39 2024-03-12 15:26:34 +01:00
YannC
93bbc83b5c fix(): set timeout to sse and now display loading/error on UI (#3259) 2024-03-12 15:26:25 +01:00
brian.mulier
7fbfbe1d00 fix(core): Pause task properly handled in restart
closes #2084
2024-03-12 15:26:17 +01:00
YannC
f53f788100 fix(core): Avoid creating empty files when splitting (#3254) 2024-03-12 15:26:07 +01:00
YannC
8e3a9e4380 fix(core): create dependency between forEachItem task and subflow (#3256) 2024-03-12 15:25:58 +01:00
brian.mulier
cb82e9f08e fix(webserver): change cookie decoder to netty one 2024-03-12 15:25:38 +01:00
brian.mulier
9deb02983c fix(webserver): rollback to http 1.1 2024-03-12 15:25:28 +01:00
brian.mulier
9b1939e129 fix(ui): grayed-out triggers when disabled (in source or through API) in topology
closes #3048
2024-03-12 15:25:15 +01:00
brian.mulier
4dfcbca7de fix(ui): revision author is now fetched only once we know which revision to display to prevent inconsistencies
closes kestra-io/kestra-ee#805
2024-03-12 15:25:02 +01:00
brian.mulier
103320e348 chore(version): update to version 'v0.15.4' 2024-03-08 16:24:02 +01:00
YannC
410093cefc chore(version): update to version 'v0.15.3' 2024-03-07 16:02:22 +01:00
YannC
bd936125bd test(core): DocumentationGeneratorTest, deprecated message 2024-03-07 16:01:33 +01:00
brian.mulier
68ded87434 fix(webserver): multi-cookies in a single header decoder
part of #3228
2024-03-07 15:55:07 +01:00
Florian Hussonnois
d76807235f fix(core): pebble render function must render boolean (#3218)
Fix: #3218
2024-03-07 14:32:18 +01:00
YannC
63708a79e3 fix(core): take timezone into account for new schedule triggers (#3230)
closes #3227
2024-03-07 14:32:18 +01:00
YannC
41c0018d4b fix(ui): manage panel for SuperAdmin without tenant (#3225) 2024-03-07 14:32:18 +01:00
YannC
2cbd86c4d1 fix(): quickwins (#3215)
* fix(ui): increase flow input size in blueprints creation

closes #913

* feat(ui): keep the latest pagination size selected

closes #3030

* fix(ui): Do not display revision selector when only one revision

closes #1681

* fix(ui): can not save if flow is same as original

closes #1331

* fix(ui): now load execution before following it

closes #682

* fix(ui): display trigger.date instead of variables.date on execution page

closes #2832

* feat(ui): add new last 48 hours filter

closes #3184
2024-03-07 14:32:17 +01:00
YannC
49b64fa853 fix(ui): make dependencies expand more clear (#3222)
closes #3160
2024-03-07 14:32:17 +01:00
Ludovic DEHON
95113c5e76 fix(ui): don't save settings on page load 2024-03-07 14:32:17 +01:00
Ludovic DEHON
1e5e300974 feat(ui): change menu & icon layout
close kestra-io/kestra#2161
2024-03-07 14:32:17 +01:00
Loïc Mathieu
f69dc3c835 fix(core): DocumentationGeneratorTest.ech() test assertion 2024-03-07 14:32:17 +01:00
Ludovic DEHON
651c7bf589 feat(ui): add full-screen button on drawer
close kestra-io/kestra#2627
2024-03-07 14:32:17 +01:00
YannC
7878bcc281 fix(core): validate task default (#3224)
closes #25
2024-03-07 14:30:00 +01:00
YannC
ee059106b2 fix(controller): return 404 when flow not found in follow API (#3219)
* fix(controller): return 404 when flow not found in follow API

closes #1299

* fix(): review changes
2024-03-07 14:30:00 +01:00
YannC
4d2728a3f6 chore(version): update to version 'v0.15.2' 2024-03-04 21:52:07 +01:00
Loïc Mathieu
87f7cde742 fix(ui): missing check permission to display flow CREATE and EXECUTE button
When a user didn't have FLOW CREATE permission, the 'Create' buton will disapear from the Dashboard and the Flows pages.
When a user didn't have EXECUTION CREATE permission, the 'Execute' button will disapear from the Flow detail and Execution detail pages.
2024-03-04 18:21:03 +01:00
YannC
2e18c87907 fix(ui): change editor width storage key 2024-03-04 18:17:07 +01:00
YannC
58352411b5 fix(controller): fix 404 issue when flow of a trigger has been deleted (#3209) 2024-03-04 17:53:13 +01:00
Ludovic DEHON
438619dd8c chore(version): update to version 'v0.15.1'. 2024-03-01 23:38:36 +01:00
72 changed files with 810 additions and 270 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -299,8 +299,9 @@ public class RunContext {
builder.put("outputs", outputs);
}
Map<String, Object> inputs = new HashMap<>();
if (execution.getInputs() != null) {
Map<String, Object> inputs = new HashMap<>(execution.getInputs());
inputs.putAll(execution.getInputs());
if (flow != null && flow.getInputs() != null) {
// if some inputs are of type secret, we decode them
for (Input<?> input : flow.getInputs()) {
@@ -314,6 +315,14 @@ public class RunContext {
}
}
}
}
if (flow != null && flow.getInputs() != null) {
// we add default inputs value from the flow if not already set, this will be useful for triggers
flow.getInputs().stream()
.filter(input -> input.getDefaults() != null && !inputs.containsKey(input.getId()))
.forEach(input -> inputs.put(input.getId(), input.getDefaults()));
}
if (!inputs.isEmpty()) {
builder.put("inputs", inputs);
}
@@ -324,6 +333,7 @@ public class RunContext {
if (execution.getLabels() != null) {
builder.put("labels", execution.getLabels()
.stream()
.filter(label -> label.value() != null)
.map(label -> new AbstractMap.SimpleEntry<>(
label.key(),
label.value()

View File

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

View File

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

View File

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

View File

@@ -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();
}
}

View File

@@ -0,0 +1,7 @@
package io.kestra.core.tasks.flows;
public interface ChildFlowInterface {
String getNamespace();
String getFlowId();
}

View File

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

View File

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

View File

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

View File

@@ -13,10 +13,7 @@ import io.kestra.core.serializers.JacksonMapper;
import io.micronaut.core.util.functional.ThrowingFunction;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
import lombok.*;
import lombok.experimental.SuperBuilder;
import java.io.BufferedReader;
@@ -57,6 +54,7 @@ import java.util.Map;
@ToString
@EqualsAndHashCode
@Getter
@NoArgsConstructor
public class DeduplicateItems extends Task implements RunnableTask<DeduplicateItems.Output> {
@Schema(

View File

@@ -15,10 +15,7 @@ import io.kestra.core.utils.TruthUtils;
import io.micronaut.core.util.functional.ThrowingFunction;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
import lombok.*;
import lombok.experimental.SuperBuilder;
import java.io.BufferedReader;
@@ -56,6 +53,7 @@ import java.util.Map;
@ToString
@EqualsAndHashCode
@Getter
@NoArgsConstructor
public class FilterItems extends Task implements RunnableTask<FilterItems.Output> {
@Schema(

View File

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

View File

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

View File

@@ -0,0 +1,56 @@
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> {
@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;
}
}
}

View File

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

View File

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

View File

@@ -1,17 +1,24 @@
package io.kestra.core.runners;
import io.kestra.core.encryption.EncryptionService;
import io.kestra.core.exceptions.IllegalVariableEvaluationException;
import io.kestra.core.metrics.MetricRegistry;
import io.kestra.core.models.Label;
import io.kestra.core.models.executions.Execution;
import io.kestra.core.models.executions.LogEntry;
import io.kestra.core.models.executions.TaskRun;
import io.kestra.core.models.executions.metrics.Counter;
import io.kestra.core.models.executions.metrics.Timer;
import io.kestra.core.models.flows.Flow;
import io.kestra.core.models.flows.State;
import io.kestra.core.models.flows.Type;
import io.kestra.core.models.flows.input.StringInput;
import io.kestra.core.models.tasks.common.EncryptedString;
import io.kestra.core.queues.QueueFactoryInterface;
import io.kestra.core.queues.QueueInterface;
import io.kestra.core.storages.StorageInterface;
import io.kestra.core.tasks.test.PollingTrigger;
import io.kestra.core.utils.IdUtils;
import io.kestra.core.utils.TestsUtils;
import io.micronaut.context.annotation.Property;
import io.micronaut.context.annotation.Value;
@@ -245,4 +252,24 @@ class RunContextTest extends AbstractMemoryRunnerTest {
// the output is automatically decrypted so the return has the decrypted value of the hello task output
assertThat(returnTask.getOutputs().get("value"), is("Hello World"));
}
@Test
void withDefaultInput() throws IllegalVariableEvaluationException {
Flow flow = Flow.builder().id("triggerWithDefaultInput").namespace("io.kestra.test").revision(1).inputs(List.of(StringInput.builder().id("test").type(Type.STRING).defaults("test").build())).build();
Execution execution = Execution.builder().id(IdUtils.create()).flowId("triggerWithDefaultInput").namespace("io.kestra.test").state(new State()).build();
RunContext runContext = runContextFactory.of(flow, execution);
assertThat(runContext.render("{{inputs.test}}"), is("test"));
}
@Test
void withNullLabel() throws IllegalVariableEvaluationException {
Flow flow = Flow.builder().id("triggerWithDefaultInput").namespace("io.kestra.test").revision(1).inputs(List.of(StringInput.builder().id("test").type(Type.STRING).defaults("test").build())).build();
Execution execution = Execution.builder().id(IdUtils.create()).flowId("triggerWithDefaultInput").namespace("io.kestra.test").state(new State()).labels(List.of(new Label("key", null))).build();
RunContext runContext = runContextFactory.of(flow, execution);
assertThat(runContext.render("{{inputs.test}}"), is("test"));
}
}

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
version=0.15.0
version=0.15.7
jacksonVersion=2.16.1
micronautVersion=4.3.4

View File

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,13 +5,13 @@
<li v-if="isAllowedEdit">
<a :href="`${finalApiUrl}/executions/${execution.id}`" target="_blank">
<el-button :icon="Api">
{{ $t('api') }}
{{ $t("api") }}
</el-button>
</a>
</li>
<li v-if="canDelete">
<el-button :icon="Delete" @click="deleteExecution">
{{ $t('delete') }}
{{ $t("delete") }}
</el-button>
</li>
<li v-if="isAllowedEdit">
@@ -26,8 +26,13 @@
</template>
</top-nav-bar>
<template v-if="ready">
<tabs :route-name="$route.params && $route.params.id ? 'executions/update': ''" @follow="follow" :tabs="tabs" />
<tabs
:route-name="$route.params && $route.params.id ? 'executions/update': ''"
@follow="follow"
:tabs="tabs"
/>
</template>
<div v-else class="full-space" v-loading="!ready" />
</template>
<script setup>
@@ -71,7 +76,7 @@
this.follow();
window.addEventListener("popstate", this.follow)
},
mounted () {
mounted() {
this.previousExecutionId = this.$route.params.id
},
watch: {
@@ -105,7 +110,11 @@
) {
this.$store.dispatch(
"flow/loadFlow",
{namespace: execution.namespace, id: execution.flowId, revision: execution.flowRevision}
{
namespace: execution.namespace,
id: execution.flowId,
revision: execution.flowRevision
}
);
this.$store.dispatch("flow/loadRevisions", {
namespace: execution.namespace,
@@ -115,6 +124,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() {
@@ -159,12 +178,14 @@
];
},
editFlow() {
this.$router.push({name:"flows/update", params: {
namespace: this.$route.params.namespace,
id: this.$route.params.flowId,
tab: "editor",
tenant: this.$route.params.tenant
}})
this.$router.push({
name: "flows/update", params: {
namespace: this.$route.params.namespace,
id: this.$route.params.flowId,
tab: "editor",
tenant: this.$route.params.tenant
}
})
},
deleteExecution() {
if (this.execution) {
@@ -268,3 +289,8 @@
}
};
</script>
<style>
.full-space {
flex: 1 1 auto;
}
</style>

View File

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

View File

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

View File

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

View File

@@ -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
}
},
};

View File

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

View File

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

View File

@@ -40,6 +40,10 @@
value: "P1D",
label: "datepicker.last24hours"
},
{
value: "P2D",
label: "datepicker.last48hours"
},
{
value: "P7D",
label: "datepicker.last7days"

View File

@@ -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)
}
}]);
}

View File

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

View File

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

View File

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

View File

@@ -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);
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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)
},

View File

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

View File

@@ -707,6 +707,10 @@ form.ks-horizontal {
}
}
&.full-screen {
width: 99% !important;
}
.el-drawer__header {
padding: var(--spacer);
margin-bottom: 0;

View File

@@ -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\">Thats all we know.</span>"
"content": "The requested URL was not found on this server. <span class=\"text-muted\">Thats 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"
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1 @@
io.micronaut.http.netty.cookies.NettyLaxServerCookieDecoder

View File

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