Compare commits

...

21 Commits

Author SHA1 Message Date
Loïc Mathieu
33ece57996 chore(version): update to version 'v0.13.5'. 2023-12-14 10:08:09 +01:00
Loïc Mathieu
5804ff7b26 fix(core): skip execution for flows with no list of tasks (#2601) 2023-12-14 10:07:21 +01:00
YannC
2888d929aa chore(version): update to version 'v0.13.4'. 2023-11-30 22:40:35 +01:00
Loïc Mathieu
8a75fd3fe5 fix(worker): worker execution result (#2616)
* fix(core, jdbc, runner-memory): set Executable taskrun to RUNNING earlier

Or RUNNING will be set when the subflow is RUNNING and when it is terminated erasing previous RUNNING start date.

* fix(core): avoid duplicate state change in the Flow task
2023-11-30 22:33:21 +01:00
YannC
3b1bed8bf4 fix(ui): avoid admin menu blinking (#2606)
* fix(ui): avoid admin menu blinking

close #2580

* fix(ui): review changes

* fix(): not blinking with tenant
2023-11-30 22:09:21 +01:00
Loïc Mathieu
01128f0927 fix(core): possible NPE on execution.findChilds() (#2607) 2023-11-30 22:09:17 +01:00
brian.mulier
bface3755a fix(ui): left menu child is also selected when navigating in their subroutes 2023-11-30 22:08:27 +01:00
YannC
8fe518ceae fix(ui): use correct key when change display columns on execution list (#2615)
* fix(ui): use correct key when change display columns on execution list

* fix(): remove log
2023-11-30 22:08:24 +01:00
Ludovic DEHON
28e39ab756 feat(core): add Elastic Common Schema (ecs) log format dependencies
close #2611
2023-11-30 22:08:18 +01:00
Ludovic DEHON
24843cfc17 fix(core): prevent npe on subflow 2023-11-30 22:08:15 +01:00
YannC
2e1c3733a4 fix(): prerender variables (#2588)
* fix(): prerender variables

* test(): fix test

* fix(): review changes

* fix(): review changes

* fix(core): return optional of object instad of empty() when no type match
2023-11-30 22:08:09 +01:00
Loïc Mathieu
61db3ccedb chore(version): update to version 'v0.13.3' 2023-11-28 15:23:07 +01:00
YannC
6a03dc9e41 chore(version): update to version 'v0.13.2' 2023-11-24 08:50:56 +01:00
Loïc Mathieu
b2a107bc55 fix(ui): the workers API is global to all tenants 2023-11-24 08:50:40 +01:00
YannC
97a6afbe2e chore(version): update to version 'v0.13.1' 2023-11-21 19:44:48 +01:00
brian.mulier
cc800b4528 fix(ui): every unwanted things are now hidden in VSCode editor
closes #2336
closes #2338
closes #2339
2023-11-21 19:43:29 +01:00
YannC
da50cf4287 fix(): display file preview (#2569) 2023-11-21 19:42:55 +01:00
YannC
1ecfed1559 fix(): apply class correctly (#2578)
close #2576
2023-11-21 19:42:25 +01:00
brian.mulier
4a3e250019 fix(core): input streams are now properly closed to prevent exhausting connections on remote storages
closes kestra-io/storage-s3#33
2023-11-21 19:39:00 +01:00
YannC
2917c178a5 fix(): display actions buttons on topology view (#2575)
* fix(): display actions buttons on topology view

* fix(): better button position
2023-11-21 19:37:50 +01:00
YannC
0ceaeb4a97 fix(): subflow logs not working (#2552) 2023-11-21 19:36:50 +01:00
39 changed files with 443 additions and 188 deletions

View File

@@ -22,6 +22,7 @@ dependencies {
implementation group: 'net.jodah', name: 'failsafe', version: '2.4.4' implementation group: 'net.jodah', name: 'failsafe', version: '2.4.4'
implementation 'com.github.oshi:oshi-core:6.4.6' implementation 'com.github.oshi:oshi-core:6.4.6'
implementation 'io.pebbletemplates:pebble:3.2.1' implementation 'io.pebbletemplates:pebble:3.2.1'
implementation group: 'co.elastic.logging', name: 'logback-ecs-encoder', version: '1.5.0'
// scheduler // scheduler
implementation group: 'com.cronutils', name: 'cron-utils', version: '9.2.1' implementation group: 'com.cronutils', name: 'cron-utils', version: '9.2.1'

View File

@@ -696,7 +696,7 @@ public class Execution implements DeletedInterface, TenantInterface {
* @return List of parent {@link TaskRun} * @return List of parent {@link TaskRun}
*/ */
public List<TaskRun> findChilds(TaskRun taskRun) { public List<TaskRun> findChilds(TaskRun taskRun) {
if (taskRun.getParentTaskRunId() == null) { if (taskRun.getParentTaskRunId() == null || this.taskRunList == null) {
return new ArrayList<>(); return new ArrayList<>();
} }

View File

@@ -1,5 +1,7 @@
package io.kestra.core.models.flows; package io.kestra.core.models.flows;
import com.fasterxml.jackson.databind.JsonNode;
import io.kestra.core.models.executions.Execution;
import io.micronaut.core.annotation.Introspected; import io.micronaut.core.annotation.Introspected;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.Getter; import lombok.Getter;
@@ -7,6 +9,9 @@ import lombok.NoArgsConstructor;
import lombok.ToString; import lombok.ToString;
import lombok.experimental.SuperBuilder; import lombok.experimental.SuperBuilder;
import java.util.List;
import java.util.Optional;
@SuperBuilder(toBuilder = true) @SuperBuilder(toBuilder = true)
@Getter @Getter
@NoArgsConstructor @NoArgsConstructor
@@ -15,4 +20,22 @@ import lombok.experimental.SuperBuilder;
@EqualsAndHashCode @EqualsAndHashCode
public class FlowWithException extends FlowWithSource { public class FlowWithException extends FlowWithSource {
String exception; String exception;
public static Optional<FlowWithException> from(JsonNode jsonNode, Exception exception) {
if (jsonNode.hasNonNull("id") && jsonNode.hasNonNull("namespace")) {
var flow = FlowWithException.builder()
.id(jsonNode.get("id").asText())
.tenantId(jsonNode.hasNonNull("tenant_id") ? jsonNode.get("tenant_id").asText() : null)
.namespace(jsonNode.get("namespace").asText())
.revision(jsonNode.hasNonNull("revision") ? jsonNode.get("revision").asInt() : 1)
.deleted(jsonNode.hasNonNull("deleted") ? jsonNode.get("deleted").asBoolean() : false)
.exception(exception.getMessage())
.tasks(List.of())
.build();
return Optional.of(flow);
}
// if there is no id and namespace, we return null as we cannot create a meaningful FlowWithException
return Optional.empty();
}
} }

View File

@@ -99,7 +99,7 @@ public final class ExecutableUtils {
return WorkerTaskExecution.builder() return WorkerTaskExecution.builder()
.task(currentTask) .task(currentTask)
.taskRun(currentTaskRun) .taskRun(currentTaskRun.withState(State.Type.RUNNING))
.execution(execution) .execution(execution)
.iteration(iteration) .iteration(iteration)
.build(); .build();
@@ -117,11 +117,11 @@ public final class ExecutableUtils {
State.Type currentState = taskRun.getState().getCurrent(); State.Type currentState = taskRun.getState().getCurrent();
Optional<State.Type> previousState = taskRun.getState().getHistories().size() > 1 ? Optional.of(taskRun.getState().getHistories().get(taskRun.getState().getHistories().size() - 2).getState()) : Optional.empty(); Optional<State.Type> previousState = taskRun.getState().getHistories().size() > 1 ? Optional.of(taskRun.getState().getHistories().get(taskRun.getState().getHistories().size() - 2).getState()) : Optional.empty();
int currentStateIteration = getIterationCounter(iterations, currentState, maxIterations) + 1; int currentStateIteration = iterations.getOrDefault(currentState.toString(), 0);
iterations.put(currentState.toString(), currentStateIteration); iterations.put(currentState.toString(), currentStateIteration + 1);
if (previousState.isPresent() && previousState.get() != currentState) { if (previousState.isPresent() && previousState.get() != currentState) {
int previousStateIterations = getIterationCounter(iterations, previousState.get(), maxIterations) - 1; int previousStateIterations = iterations.getOrDefault(previousState.get() .toString(), maxIterations);
iterations.put(previousState.get().toString(), previousStateIterations); iterations.put(previousState.get().toString(), previousStateIterations - 1);
} }
// update the state to success if current == max // update the state to success if current == max

View File

@@ -41,7 +41,7 @@ public class Executor {
} }
public Boolean canBeProcessed() { public Boolean canBeProcessed() {
return !(this.getException() != null || this.getFlow() == null || this.getFlow() instanceof FlowWithException || this.getExecution().isDeleted()); return !(this.getException() != null || this.getFlow() == null || this.getFlow() instanceof FlowWithException || this.getFlow().getTasks() == null || this.getExecution().isDeleted());
} }
public Executor withFlow(Flow flow) { public Executor withFlow(Flow flow) {

View File

@@ -107,7 +107,6 @@ public class NamespaceFilesService {
private void copy(String tenantId, String namespace, Path basePath, List<URI> files) throws IOException { private void copy(String tenantId, String namespace, Path basePath, List<URI> files) throws IOException {
files files
.forEach(throwConsumer(f -> { .forEach(throwConsumer(f -> {
InputStream inputStream = storageInterface.get(tenantId, uri(namespace, f));
Path destination = Paths.get(basePath.toString(), f.getPath()); Path destination = Paths.get(basePath.toString(), f.getPath());
if (!destination.getParent().toFile().exists()) { if (!destination.getParent().toFile().exists()) {
@@ -115,7 +114,9 @@ public class NamespaceFilesService {
destination.getParent().toFile().mkdirs(); destination.getParent().toFile().mkdirs();
} }
Files.copy(inputStream, destination); try (InputStream inputStream = storageInterface.get(tenantId, uri(namespace, f))) {
Files.copy(inputStream, destination);
}
})); }));
} }
} }

View File

@@ -598,7 +598,7 @@ public class RunContext {
URI uri = URI.create(this.taskStateFilePathPrefix(state, isNamespace, useTaskRun)); URI uri = URI.create(this.taskStateFilePathPrefix(state, isNamespace, useTaskRun));
URI resolve = uri.resolve(uri.getPath() + "/" + name); URI resolve = uri.resolve(uri.getPath() + "/" + name);
return this.storageInterface.get(getTenantId(), resolve); return this.storageInterface.get(getTenantId(), resolve);
} }
public URI putTaskStateFile(byte[] content, String state, String name) throws IOException { public URI putTaskStateFile(byte[] content, String state, String name) throws IOException {

View File

@@ -5,11 +5,6 @@ import com.github.jknack.handlebars.Handlebars;
import com.github.jknack.handlebars.HandlebarsException; import com.github.jknack.handlebars.HandlebarsException;
import com.github.jknack.handlebars.Template; import com.github.jknack.handlebars.Template;
import com.github.jknack.handlebars.helper.*; import com.github.jknack.handlebars.helper.*;
import io.pebbletemplates.pebble.PebbleEngine;
import io.pebbletemplates.pebble.error.AttributeNotFoundException;
import io.pebbletemplates.pebble.error.PebbleException;
import io.pebbletemplates.pebble.extension.AbstractExtension;
import io.pebbletemplates.pebble.template.PebbleTemplate;
import io.kestra.core.exceptions.IllegalVariableEvaluationException; import io.kestra.core.exceptions.IllegalVariableEvaluationException;
import io.kestra.core.runners.handlebars.VariableRendererPlugins; import io.kestra.core.runners.handlebars.VariableRendererPlugins;
import io.kestra.core.runners.handlebars.helpers.*; import io.kestra.core.runners.handlebars.helpers.*;
@@ -18,6 +13,15 @@ import io.kestra.core.runners.pebble.JsonWriter;
import io.kestra.core.runners.pebble.PebbleLruCache; import io.kestra.core.runners.pebble.PebbleLruCache;
import io.micronaut.context.ApplicationContext; import io.micronaut.context.ApplicationContext;
import io.micronaut.context.annotation.ConfigurationProperties; import io.micronaut.context.annotation.ConfigurationProperties;
import io.micronaut.core.annotation.Nullable;
import io.pebbletemplates.pebble.PebbleEngine;
import io.pebbletemplates.pebble.error.AttributeNotFoundException;
import io.pebbletemplates.pebble.error.PebbleException;
import io.pebbletemplates.pebble.extension.AbstractExtension;
import io.pebbletemplates.pebble.template.PebbleTemplate;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import lombok.Getter;
import java.io.IOException; import java.io.IOException;
import java.io.StringWriter; import java.io.StringWriter;
@@ -26,11 +30,6 @@ import java.util.*;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import io.micronaut.core.annotation.Nullable;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import lombok.Getter;
@Singleton @Singleton
public class VariableRenderer { public class VariableRenderer {
private static final Pattern RAW_PATTERN = Pattern.compile("\\{%[-]*\\s*raw\\s*[-]*%\\}(.*?)\\{%[-]*\\s*endraw\\s*[-]*%\\}"); private static final Pattern RAW_PATTERN = Pattern.compile("\\{%[-]*\\s*raw\\s*[-]*%\\}(.*?)\\{%[-]*\\s*endraw\\s*[-]*%\\}");
@@ -93,6 +92,10 @@ public class VariableRenderer {
} }
public String recursiveRender(String inline, Map<String, Object> variables) throws IllegalVariableEvaluationException { public String recursiveRender(String inline, Map<String, Object> variables) throws IllegalVariableEvaluationException {
return recursiveRender(inline, variables, true);
}
public String recursiveRender(String inline, Map<String, Object> variables, Boolean preprocessVariables) throws IllegalVariableEvaluationException {
if (inline == null) { if (inline == null) {
return null; return null;
} }
@@ -111,6 +114,11 @@ public class VariableRenderer {
return uuid; return uuid;
}); });
// pre-process variables
if (preprocessVariables) {
variables = this.preprocessVariables(variables);
}
boolean isSame = false; boolean isSame = false;
String current = ""; String current = "";
PebbleTemplate compiledTemplate; PebbleTemplate compiledTemplate;
@@ -131,28 +139,43 @@ public class VariableRenderer {
} }
try { try {
Template template = handlebars.compileInline(currentTemplate); Template template = handlebars.compileInline(currentTemplate);
current = template.apply(variables); current = template.apply(variables);
} catch (HandlebarsException | IOException hbE) { } catch (HandlebarsException | IOException hbE) {
throw new IllegalVariableEvaluationException( throw new IllegalVariableEvaluationException(
"Pebble evaluation failed with '" + e.getMessage() + "' " + "Pebble evaluation failed with '" + e.getMessage() + "' " +
"and Handlebars fallback failed also with '" + hbE.getMessage() + "'" , "and Handlebars fallback failed also with '" + hbE.getMessage() + "'",
e e
); );
} }
} }
isSame = currentTemplate.equals(current); isSame = currentTemplate.equals(current);
currentTemplate = current; currentTemplate = current;
} }
// post-process raw tags // post-process raw tags
for(var entry: replacers.entrySet()) { for (var entry : replacers.entrySet()) {
current = current.replace(entry.getKey(), entry.getValue()); current = current.replace(entry.getKey(), entry.getValue());
} }
return current; return current;
} }
public Map<String, Object> preprocessVariables(Map<String, Object> variables) throws IllegalVariableEvaluationException {
Map<String, Object> currentVariables = variables;
Map<String, Object> previousVariables;
boolean isSame = false;
while (!isSame) {
previousVariables = currentVariables;
currentVariables = this.render(currentVariables, variables, false);
isSame = previousVariables.equals(currentVariables);
}
return currentVariables;
}
public IllegalVariableEvaluationException properPebbleException(PebbleException e) { public IllegalVariableEvaluationException properPebbleException(PebbleException e) {
if (e instanceof AttributeNotFoundException current) { if (e instanceof AttributeNotFoundException current) {
return new IllegalVariableEvaluationException( return new IllegalVariableEvaluationException(
@@ -166,17 +189,27 @@ public class VariableRenderer {
return new IllegalVariableEvaluationException(e); return new IllegalVariableEvaluationException(e);
} }
// By default, render() will render variables
// so we keep the previous behavior
public String render(String inline, Map<String, Object> variables) throws IllegalVariableEvaluationException { public String render(String inline, Map<String, Object> variables) throws IllegalVariableEvaluationException {
return this.recursiveRender(inline, variables); return this.recursiveRender(inline, variables, true);
} }
public String render(String inline, Map<String, Object> variables, boolean preprocessVariables) throws IllegalVariableEvaluationException {
return this.recursiveRender(inline, variables, preprocessVariables);
}
// Default behavior
public Map<String, Object> render(Map<String, Object> in, Map<String, Object> variables) throws IllegalVariableEvaluationException { public Map<String, Object> render(Map<String, Object> in, Map<String, Object> variables) throws IllegalVariableEvaluationException {
return render(in, variables, true);
}
public Map<String, Object> render(Map<String, Object> in, Map<String, Object> variables, boolean preprocessVariables) throws IllegalVariableEvaluationException {
Map<String, Object> map = new HashMap<>(); Map<String, Object> map = new HashMap<>();
for (Map.Entry<String, Object> r : in.entrySet()) { for (Map.Entry<String, Object> r : in.entrySet()) {
String key = this.render(r.getKey(), variables); String key = this.render(r.getKey(), variables, preprocessVariables);
Object value = renderObject(r.getValue(), variables).orElse(r.getValue()); Object value = renderObject(r.getValue(), variables, preprocessVariables).orElse(r.getValue());
map.putIfAbsent( map.putIfAbsent(
key, key,
@@ -188,23 +221,25 @@ public class VariableRenderer {
} }
@SuppressWarnings({"unchecked", "rawtypes"}) @SuppressWarnings({"unchecked", "rawtypes"})
private Optional<Object> renderObject(Object object, Map<String, Object> variables) throws IllegalVariableEvaluationException { private Optional<Object> renderObject(Object object, Map<String, Object> variables, boolean preprocessVariables) throws IllegalVariableEvaluationException {
if (object instanceof Map) { if (object instanceof Map) {
return Optional.of(this.render((Map) object, variables)); return Optional.of(this.render((Map) object, variables, preprocessVariables));
} else if (object instanceof Collection) { } else if (object instanceof Collection) {
return Optional.of(this.renderList((List) object, variables)); return Optional.of(this.renderList((List) object, variables, preprocessVariables));
} else if (object instanceof String) { } else if (object instanceof String) {
return Optional.of(this.render((String) object, variables)); return Optional.of(this.render((String) object, variables, preprocessVariables));
} else if (object == null) {
return Optional.empty();
} }
return Optional.empty(); return Optional.of(object);
} }
public List<Object> renderList(List<Object> list, Map<String, Object> variables) throws IllegalVariableEvaluationException { private List<Object> renderList(List<Object> list, Map<String, Object> variables, boolean preprocessVariables) throws IllegalVariableEvaluationException {
List<Object> result = new ArrayList<>(); List<Object> result = new ArrayList<>();
for (Object inline : list) { for (Object inline : list) {
this.renderObject(inline, variables) this.renderObject(inline, variables, preprocessVariables)
.ifPresent(result::add); .ifPresent(result::add);
} }

View File

@@ -1,7 +1,6 @@
package io.kestra.core.runners.pebble.functions; package io.kestra.core.runners.pebble.functions;
import io.kestra.core.storages.StorageInterface; import io.kestra.core.storages.StorageInterface;
import io.kestra.core.tenant.TenantService;
import io.kestra.core.utils.Slugify; import io.kestra.core.utils.Slugify;
import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.error.PebbleException;
import io.pebbletemplates.pebble.extension.Function; import io.pebbletemplates.pebble.extension.Function;
@@ -11,6 +10,7 @@ import jakarta.inject.Inject;
import jakarta.inject.Singleton; import jakarta.inject.Singleton;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.net.URI; import java.net.URI;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.List; import java.util.List;
@@ -47,7 +47,9 @@ public class ReadFileFunction implements Function {
private String readFromNamespaceFile(EvaluationContext context, String path) throws IOException { private String readFromNamespaceFile(EvaluationContext context, String path) throws IOException {
Map<String, String> flow = (Map<String, String>) context.getVariable("flow"); Map<String, String> flow = (Map<String, String>) context.getVariable("flow");
URI namespaceFile = URI.create(storageInterface.namespaceFilePrefix(flow.get("namespace")) + "/" + path); URI namespaceFile = URI.create(storageInterface.namespaceFilePrefix(flow.get("namespace")) + "/" + path);
return new String(storageInterface.get(flow.get("tenantId"), namespaceFile).readAllBytes(), StandardCharsets.UTF_8); try (InputStream inputStream = storageInterface.get(flow.get("tenantId"), namespaceFile)) {
return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
}
} }
private String readFromInternalStorageUri(EvaluationContext context, String path) throws IOException { private String readFromInternalStorageUri(EvaluationContext context, String path) throws IOException {
@@ -69,7 +71,9 @@ public class ReadFileFunction implements Function {
} }
} }
URI internalStorageFile = URI.create(path); URI internalStorageFile = URI.create(path);
return new String(storageInterface.get(flow.get("tenantId"), internalStorageFile).readAllBytes(), StandardCharsets.UTF_8); try (InputStream inputStream = storageInterface.get(flow.get("tenantId"), internalStorageFile)) {
return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
}
} }
private boolean validateFileUri(String namespace, String flowId, String executionId, String path) { private boolean validateFileUri(String namespace, String flowId, String executionId, String path) {

View File

@@ -4,10 +4,7 @@ import io.kestra.core.runners.RunContext;
import io.kestra.core.storages.StorageSplitInterface; import io.kestra.core.storages.StorageSplitInterface;
import io.micronaut.core.convert.format.ReadableBytesTypeConverter; import io.micronaut.core.convert.format.ReadableBytesTypeConverter;
import java.io.BufferedReader; import java.io.*;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.RandomAccessFile;
import java.net.URI; import java.net.URI;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;

View File

@@ -42,8 +42,7 @@ public interface StorageInterface {
* @return true if the uri points to a file/object that exist in the internal storage. * @return true if the uri points to a file/object that exist in the internal storage.
*/ */
default boolean exists(String tenantId, URI uri) { default boolean exists(String tenantId, URI uri) {
try { try (InputStream ignored = get(tenantId, uri)){
get(tenantId, uri);
return true; return true;
} catch (IOException ieo) { } catch (IOException ieo) {
return false; return false;

View File

@@ -175,7 +175,10 @@ public class Flow extends Task implements ExecutableTask<Flow.Output> {
taskRun = taskRun.withOutputs(builder.build().toMap()); taskRun = taskRun.withOutputs(builder.build().toMap());
taskRun = taskRun.withState(ExecutableUtils.guessState(execution, this.transmitFailed, State.Type.SUCCESS)); State.Type finalState = ExecutableUtils.guessState(execution, this.transmitFailed, State.Type.SUCCESS);
if (taskRun.getState().getCurrent() != finalState) {
taskRun = taskRun.withState(finalState);
}
return Optional.of(ExecutableUtils.workerTaskResult(taskRun)); return Optional.of(ExecutableUtils.workerTaskResult(taskRun));
} }

View File

@@ -54,9 +54,9 @@ public abstract class AbstractState extends Task {
protected Map<String, Object> get(RunContext runContext) throws IllegalVariableEvaluationException, IOException { protected Map<String, Object> get(RunContext runContext) throws IllegalVariableEvaluationException, IOException {
InputStream taskStateFile = runContext.getTaskStateFile("tasks-states", runContext.render(this.name), this.namespace, this.taskrunValue); try (InputStream taskStateFile = runContext.getTaskStateFile("tasks-states", runContext.render(this.name), this.namespace, this.taskrunValue)) {
return JacksonMapper.ofJson(false).readValue(taskStateFile, TYPE_REFERENCE);
return JacksonMapper.ofJson(false).readValue(taskStateFile, TYPE_REFERENCE); }
} }
protected Pair<URI, Map<String, Object>> merge(RunContext runContext, Map<String, Object> map) throws IllegalVariableEvaluationException, IOException { protected Pair<URI, Map<String, Object>> merge(RunContext runContext, Map<String, Object> map) throws IllegalVariableEvaluationException, IOException {

View File

@@ -16,6 +16,7 @@ import io.kestra.core.runners.RunContext;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.URI; import java.net.URI;
import java.util.List; import java.util.List;
@@ -128,7 +129,9 @@ public class Concat extends Task implements RunnableTask<Concat.Output> {
finalFiles.forEach(throwConsumer(s -> { finalFiles.forEach(throwConsumer(s -> {
URI from = new URI(runContext.render(s)); URI from = new URI(runContext.render(s));
IOUtils.copyLarge(runContext.uriToInputStream(from), fileOutputStream); try (InputStream inputStream = runContext.uriToInputStream(from)) {
IOUtils.copyLarge(inputStream, fileOutputStream);
}
if (separator != null) { if (separator != null) {
IOUtils.copy(new ByteArrayInputStream(this.separator.getBytes()), fileOutputStream); IOUtils.copy(new ByteArrayInputStream(this.separator.getBytes()), fileOutputStream);

View File

@@ -165,7 +165,7 @@ public class FlowTopologyService {
.filter(t -> t instanceof ExecutableTask) .filter(t -> t instanceof ExecutableTask)
.map(t -> (ExecutableTask<?>) t) .map(t -> (ExecutableTask<?>) t)
.anyMatch(t -> .anyMatch(t ->
t.subflowId().namespace().equals(child.getNamespace()) && t.subflowId().flowId().equals(child.getId()) t.subflowId() != null && t.subflowId().namespace().equals(child.getNamespace()) && t.subflowId().flowId().equals(child.getId())
); );
} catch (Exception e) { } catch (Exception e) {
log.warn("Failed to detect flow task on namespace:'" + parent.getNamespace() + "', flowId:'" + parent.getId() + "'", e); log.warn("Failed to detect flow task on namespace:'" + parent.getNamespace() + "', flowId:'" + parent.getId() + "'", e);

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<included>
<appender name="CONSOLE_ECS_OUT" class="ch.qos.logback.core.ConsoleAppender">
<target>System.out</target>
<immediateFlush>true</immediateFlush>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>DENY</onMatch>
</filter>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>WARN</level>
<onMatch>DENY</onMatch>
</filter>
<encoder class="co.elastic.logging.logback.EcsEncoder" />
</appender>
<appender name="CONSOLE_ECS_ERR" class="ch.qos.logback.core.ConsoleAppender">
<target>System.err</target>
<immediateFlush>true</immediateFlush>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>WARN</level>
</filter>
<encoder class="co.elastic.logging.logback.EcsEncoder" />
</appender>
</included>

View File

@@ -35,8 +35,8 @@ class FlowGraphTest extends AbstractMemoryRunnerTest {
Flow flow = this.parse("flows/valids/return.yaml"); Flow flow = this.parse("flows/valids/return.yaml");
FlowGraph flowGraph = GraphUtils.flowGraph(flow, null); FlowGraph flowGraph = GraphUtils.flowGraph(flow, null);
assertThat(flowGraph.getNodes().size(), is(5)); assertThat(flowGraph.getNodes().size(), is(6));
assertThat(flowGraph.getEdges().size(), is(4)); assertThat(flowGraph.getEdges().size(), is(5));
assertThat(flowGraph.getClusters().size(), is(0)); assertThat(flowGraph.getClusters().size(), is(0));
assertThat(((AbstractGraphTask) flowGraph.getNodes().get(2)).getTask().getId(), is("date")); assertThat(((AbstractGraphTask) flowGraph.getNodes().get(2)).getTask().getId(), is("date"));

View File

@@ -30,6 +30,7 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.*; import static org.hamcrest.Matchers.*;
@Property(name = "kestra.tasks.tmp-dir.path", value = "/tmp/sub/dir/tmp/") @Property(name = "kestra.tasks.tmp-dir.path", value = "/tmp/sub/dir/tmp/")
@@ -112,7 +113,7 @@ class RunContextTest extends AbstractMemoryRunnerTest {
void variables() throws TimeoutException { void variables() throws TimeoutException {
Execution execution = runnerUtils.runOne(null, "io.kestra.tests", "return"); Execution execution = runnerUtils.runOne(null, "io.kestra.tests", "return");
assertThat(execution.getTaskRunList(), hasSize(3)); assertThat(execution.getTaskRunList(), hasSize(4));
assertThat( assertThat(
ZonedDateTime.from(ZonedDateTime.parse((String) execution.getTaskRunList().get(0).getOutputs().get("value"))), ZonedDateTime.from(ZonedDateTime.parse((String) execution.getTaskRunList().get(0).getOutputs().get("value"))),
@@ -120,6 +121,7 @@ class RunContextTest extends AbstractMemoryRunnerTest {
); );
assertThat(execution.getTaskRunList().get(1).getOutputs().get("value"), is("task-id")); assertThat(execution.getTaskRunList().get(1).getOutputs().get("value"), is("task-id"));
assertThat(execution.getTaskRunList().get(2).getOutputs().get("value"), is("return")); assertThat(execution.getTaskRunList().get(2).getOutputs().get("value"), is("return"));
assertThat((String) execution.getTaskRunList().get(3).getOutputs().get("value"), containsString("toto"));
} }
@Test @Test

View File

@@ -5,13 +5,13 @@ import io.kestra.core.exceptions.IllegalVariableEvaluationException;
import io.kestra.core.runners.VariableRenderer; import io.kestra.core.runners.VariableRenderer;
import io.kestra.core.utils.Rethrow; import io.kestra.core.utils.Rethrow;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest; import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import jakarta.inject.Inject;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*; import static org.hamcrest.Matchers.*;

View File

@@ -105,8 +105,8 @@ public class ForEachItemCaseTest {
assertThat(outputs.get("iterations"), notNullValue()); assertThat(outputs.get("iterations"), notNullValue());
Map<String, Integer> iterations = (Map<String, Integer>) outputs.get("iterations"); Map<String, Integer> iterations = (Map<String, Integer>) outputs.get("iterations");
assertThat(iterations.get("max"), is(3)); assertThat(iterations.get("max"), is(3));
assertThat(iterations.get("CREATED"), is(0)); assertThat(iterations.get("CREATED"), nullValue());// if we didn't wait we will only observe RUNNING and SUCCESS
assertThat(iterations.get("RUNNING"), nullValue()); // if we didn't wait we will only observe CREATED and SUCCESS assertThat(iterations.get("RUNNING"), is(0));
assertThat(iterations.get("SUCCESS"), is(3)); assertThat(iterations.get("SUCCESS"), is(3));
// assert that not all subflows ran (depending on the speed of execution there can be some) // assert that not all subflows ran (depending on the speed of execution there can be some)

View File

@@ -10,7 +10,7 @@ inputs:
variables: variables:
var-1: var-1 var-1: var-1
var-2: '{{ var-1 }}' var-2: "{{ 'var-1' }}"
taskDefaults: taskDefaults:
- type: io.kestra.core.tasks.log.Log - type: io.kestra.core.tasks.log.Log

View File

@@ -1,6 +1,9 @@
id: return id: return
namespace: io.kestra.tests namespace: io.kestra.tests
variables:
period: "{{ schedule.date ?? execution.startDate }}"
tasks: tasks:
- id: date - id: date
type: io.kestra.core.tasks.debugs.Return type: io.kestra.core.tasks.debugs.Return
@@ -10,4 +13,7 @@ tasks:
format: "{{task.id}}" format: "{{task.id}}"
- id: flow-id - id: flow-id
type: io.kestra.core.tasks.debugs.Return type: io.kestra.core.tasks.debugs.Return
format: "{{flow.id}}" format: "{{flow.id}}"
- id: variables
type: io.kestra.core.tasks.debugs.Return
format: "{{ vars.period | replace({':': 'toto'}) }}"

View File

@@ -1,4 +1,4 @@
version=0.13.0 version=0.13.5
jacksonVersion=2.15.2 jacksonVersion=2.15.2
micronautVersion=3.10.1 micronautVersion=3.10.1

View File

@@ -61,14 +61,7 @@ public abstract class AbstractJdbcFlowRepository extends AbstractJdbcRepository
} catch (DeserializationException e) { } catch (DeserializationException e) {
try { try {
JsonNode jsonNode = JdbcMapper.of().readTree(source); JsonNode jsonNode = JdbcMapper.of().readTree(source);
return FlowWithException.builder() return FlowWithException.from(jsonNode, e).orElseThrow(() -> e);
.id(jsonNode.get("id").asText())
.tenantId(jsonNode.get("tenant_id") != null ? jsonNode.get("tenant_id").asText() : null)
.namespace(jsonNode.get("namespace").asText())
.revision(jsonNode.get("revision").asInt())
.exception(e.getMessage())
.tasks(List.of())
.build();
} catch (JsonProcessingException ex) { } catch (JsonProcessingException ex) {
throw new DeserializationException(ex, source); throw new DeserializationException(ex, source);
} }

View File

@@ -406,7 +406,7 @@ public class JdbcExecutor implements ExecutorInterface {
// send a running worker task result to track running vs created status // send a running worker task result to track running vs created status
if (workerTaskExecution.getTask().waitForExecution()) { if (workerTaskExecution.getTask().waitForExecution()) {
sendWorkerTaskResultForWorkerTaskExecution(execution, workerTaskExecution, workerTaskExecution.getTaskRun().withState(State.Type.RUNNING)); sendWorkerTaskResultForWorkerTaskExecution(execution, workerTaskExecution, workerTaskExecution.getTaskRun());
} }
}); });
} }
@@ -426,7 +426,7 @@ public class JdbcExecutor implements ExecutorInterface {
.ifPresent(workerTaskExecution -> { .ifPresent(workerTaskExecution -> {
// If we didn't wait for the flow execution, the worker task execution has already been created by the Executor service. // If we didn't wait for the flow execution, the worker task execution has already been created by the Executor service.
if (workerTaskExecution.getTask().waitForExecution()) { if (workerTaskExecution.getTask().waitForExecution()) {
sendWorkerTaskResultForWorkerTaskExecution(execution, workerTaskExecution, workerTaskExecution.getTaskRun().withState(State.Type.RUNNING).withState(execution.getState().getCurrent())); sendWorkerTaskResultForWorkerTaskExecution(execution, workerTaskExecution, workerTaskExecution.getTaskRun().withState(execution.getState().getCurrent()));
} }
workerTaskExecutionStorage.delete(workerTaskExecution); workerTaskExecutionStorage.delete(workerTaskExecution);

View File

@@ -268,7 +268,7 @@ public abstract class JdbcRunnerTest {
skipExecutionCaseTest.skipExecution(); skipExecutionCaseTest.skipExecution();
} }
@Test @RetryingTest(5)
void forEachItem() throws URISyntaxException, IOException, InterruptedException, TimeoutException { void forEachItem() throws URISyntaxException, IOException, InterruptedException, TimeoutException {
forEachItemCaseTest.forEachItem(); forEachItemCaseTest.forEachItem();
} }

View File

@@ -237,7 +237,7 @@ public class MemoryExecutor implements ExecutorInterface {
// send a running worker task result to track running vs created status // send a running worker task result to track running vs created status
if (workerTaskExecution.getTask().waitForExecution()) { if (workerTaskExecution.getTask().waitForExecution()) {
sendWorkerTaskResultForWorkerTaskExecution(execution, workerTaskExecution, workerTaskExecution.getTaskRun().withState(State.Type.RUNNING)); sendWorkerTaskResultForWorkerTaskExecution(execution, workerTaskExecution, workerTaskExecution.getTaskRun());
} }
}); });
} }
@@ -259,7 +259,7 @@ public class MemoryExecutor implements ExecutorInterface {
// If we didn't wait for the flow execution, the worker task execution has already been created by the Executor service. // If we didn't wait for the flow execution, the worker task execution has already been created by the Executor service.
if (workerTaskExecution.getTask().waitForExecution()) { if (workerTaskExecution.getTask().waitForExecution()) {
sendWorkerTaskResultForWorkerTaskExecution(execution, workerTaskExecution, workerTaskExecution.getTaskRun().withState(State.Type.RUNNING).withState(execution.getState().getCurrent())); sendWorkerTaskResultForWorkerTaskExecution(execution, workerTaskExecution, workerTaskExecution.getTaskRun().withState(execution.getState().getCurrent()));
} }
WORKERTASKEXECUTIONS_WATCHER.remove(execution.getId()); WORKERTASKEXECUTIONS_WATCHER.remove(execution.getId());

View File

@@ -30,6 +30,31 @@ const extensionsToFetch = Object.entries(versionByExtensionIdToFetch).map(([exte
})); }));
// used to configure VSCode startup // used to configure VSCode startup
const sidebarTabs = [
{"id": "workbench.view.explorer", "pinned": true, "visible": true, "order": 0},
{"id": "workbench.view.search", "pinned": true, "visible": true, "order": 1},
{"id": "workbench.view.scm", "pinned": false, "visible": false, "order": 2},
{"id": "workbench.view.debug", "pinned": false,"visible": false,"order": 3},
{"id": "workbench.view.extensions", "pinned": true,"visible": true,"order": 4},
{"id": "workbench.view.remote", "pinned": false,"visible": false,"order": 4},
{"id": "workbench.view.extension.test", "pinned": false,"visible": false,"order": 6},
{"id": "workbench.view.extension.references-view", "pinned": false,"visible": false,"order": 7},
{"id": "workbench.panel.chatSidebar", "pinned": false,"visible": false,"order": 100},
{"id": "userDataProfiles", "pinned": false, "visible": false},
{"id": "workbench.view.sync", "pinned": false,"visible": false},
{"id": "workbench.view.editSessions", "pinned": false, "visible": false}
];
const bottomBarTabs = [
{"id":"workbench.panel.markers", "pinned": false,"visible": false,"order": 0},
{"id":"workbench.panel.output", "pinned": false,"visible": false,"order": 1},
{"id":"workbench.panel.repl", "pinned": false,"visible": false,"order": 2},
{"id":"terminal", "pinned": false,"visible": false,"order": 3},
{"id":"workbench.panel.testResults", "pinned": false,"visible": false,"order": 3},
{"id":"~remote.forwardedPortsContainer", "pinned": false,"visible": false,"order": 5},
{"id":"refactorPreview", "pinned": false,"visible": false}
];
window.product = { window.product = {
productConfiguration: { productConfiguration: {
nameShort: "Kestra VSCode", nameShort: "Kestra VSCode",
@@ -77,5 +102,17 @@ window.product = {
"workbench.colorTheme": THEME === "dark" ? "Sweet Dracula" : "Default Light Modern", "workbench.colorTheme": THEME === "dark" ? "Sweet Dracula" : "Default Light Modern",
// provide the Kestra root URL to extension // provide the Kestra root URL to extension
"kestra.api.url": KESTRA_API_URL "kestra.api.url": KESTRA_API_URL
},
profile: {
name: "Kestra VSCode",
contents: JSON.stringify({
globalState: JSON.stringify({
"storage": {
"workbench.activity.pinnedViewlets2": sidebarTabs,
"workbench.activity.showAccounts": "false",
"workbench.panel.pinnedPanels": bottomBarTabs
}
})
})
} }
}; };

View File

@@ -447,7 +447,7 @@
}, },
methods: { methods: {
onDisplayColumnsChange(event) { onDisplayColumnsChange(event) {
localStorage.setItem("displayExecutionsColumns", event); localStorage.setItem(this.storageKey, event);
this.displayColumns = event; this.displayColumns = event;
}, },
displayColumn(column) { displayColumn(column) {
@@ -608,4 +608,4 @@
}, },
} }
}; };
</script> </script>

View File

@@ -142,7 +142,7 @@
executionId: this.executionId, executionId: this.executionId,
path: this.value, path: this.value,
maxRows: this.maxPreview, maxRows: this.maxPreview,
encoding: this.encoding encoding: this.encoding.value
}) })
.then(() => { .then(() => {
this.isPreviewOpen = true; this.isPreviewOpen = true;

View File

@@ -456,15 +456,15 @@
} }
.bottom-right { .bottom-right {
bottom: var(--spacer); bottom: 0px;
right: var(--spacer); right: 0px;
ul { ul {
display: flex; display: flex;
list-style: none; list-style: none;
padding: 0; padding: 0;
margin: 0; margin: 0;
gap: calc(var(--spacer) / 2); //gap: calc(var(--spacer) / 2);
} }
} }
} }

View File

@@ -0,0 +1,121 @@
<template>
<div class="to-action-button">
<div v-if="isAllowedEdit || canDelete" class="mx-2">
<el-dropdown>
<el-button type="default" :disabled="isReadOnly">
<DotsVertical title=""/>
{{ $t("actions") }}
</el-button>
<template #dropdown>
<el-dropdown-menu class="m-dropdown-menu">
<el-dropdown-item
v-if="!isCreating && canDelete"
:icon="Delete"
size="large"
@click="forwardEvent('delete-flow', $event)"
>
{{ $t("delete") }}
</el-dropdown-item>
<el-dropdown-item
v-if="!isCreating"
:icon="ContentCopy"
size="large"
@click="forwardEvent('copy', $event)"
>
{{ $t("copy") }}
</el-dropdown-item>
<el-dropdown-item
v-if="isAllowedEdit"
:icon="Exclamation"
size="large"
@click="forwardEvent('open-new-error', null)"
:disabled="!flowHaveTasks"
>
{{ $t("add global error handler") }}
</el-dropdown-item>
<el-dropdown-item
v-if="isAllowedEdit"
:icon="LightningBolt"
size="large"
@click="forwardEvent('open-new-trigger', null)"
:disabled="!flowHaveTasks"
>
{{ $t("add trigger") }}
</el-dropdown-item>
<el-dropdown-item
v-if="isAllowedEdit"
:icon="FileEdit"
size="large"
@click="forwardEvent('open-edit-metadata', null)"
>
{{ $t("edit metadata") }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<div>
<el-button
:icon="ContentSave"
@click="forwardEvent('save', $event)"
v-if="isAllowedEdit"
:type="flowError ? 'danger' : 'primary'"
:disabled="!haveChange && !isCreating"
class="edit-flow-save-button"
>
{{ $t("save") }}
</el-button>
</div>
</div>
</template>
<script setup>
import DotsVertical from "vue-material-design-icons/DotsVertical.vue";
import Delete from "vue-material-design-icons/Delete.vue";
import ContentCopy from "vue-material-design-icons/ContentCopy.vue";
import Exclamation from "vue-material-design-icons/Exclamation.vue";
import LightningBolt from "vue-material-design-icons/LightningBolt.vue";
import FileEdit from "vue-material-design-icons/FileEdit.vue";
import ContentSave from "vue-material-design-icons/ContentSave.vue";
</script>
<script>
import {defineComponent} from "vue";
export default defineComponent({
props: {
isCreating: {
type: Boolean,
default: false
},
isReadOnly: {
type: Boolean,
default: false
},
canDelete: {
type: Boolean,
default: false
},
isAllowedEdit: {
type: Boolean,
default: false
},
haveChange: {
type: Boolean,
default: false
},
flowHaveTasks: {
type: Boolean,
default: false
},
flowError: {
type: String,
default: null
}
},
methods: {
forwardEvent(type, event) {
this.$emit(type, event);
}
}
})
</script>

View File

@@ -28,6 +28,7 @@
import {editorViewTypes} from "../../utils/constants"; import {editorViewTypes} from "../../utils/constants";
import Utils from "@kestra-io/ui-libs/src/utils/Utils"; import Utils from "@kestra-io/ui-libs/src/utils/Utils";
import {apiUrl} from "override/utils/route"; import {apiUrl} from "override/utils/route";
import EditorButtons from "./EditorButtons.vue";
const store = useStore(); const store = useStore();
const router = getCurrentInstance().appContext.config.globalProperties.$router; const router = getCurrentInstance().appContext.config.globalProperties.$router;
@@ -675,75 +676,22 @@
<ValidationError ref="validationDomElement" tooltip-placement="bottom-start" size="small" class="ms-2" :error="flowError" :warnings="flowWarnings" /> <ValidationError ref="validationDomElement" tooltip-placement="bottom-start" size="small" class="ms-2" :error="flowError" :warnings="flowWarnings" />
</template> </template>
<template #buttons> <template #buttons>
<ul> <EditorButtons
<li v-if="isAllowedEdit || canDelete"> v-if="![editorViewTypes.TOPOLOGY, editorViewTypes.SOURCE_TOPOLOGY].includes(viewType)"
<el-dropdown> :is-creating="props.isCreating"
<el-button type="default" :disabled="isReadOnly"> :is-read-only="props.isReadOnly"
<DotsVertical title="" /> :can-delete="canDelete"
{{ $t("actions") }} :is-allowed-edit="isAllowedEdit"
</el-button> :have-change="haveChange"
<template #dropdown> :flow-have-tasks="flowHaveTasks()"
<el-dropdown-menu class="m-dropdown-menu"> :flow-error="flowError"
<el-dropdown-item @delete-flow="deleteFlow"
v-if="!props.isCreating && canDelete" @save="save"
:icon="Delete" @copy="() => router.push({name: 'flows/create', query: {copy: true}})"
size="large" @open-new-error="isNewErrorOpen = true;"
@click="deleteFlow" @open-new-trigger="isNewTriggerOpen = true;"
> @open-edit-metadata="isEditMetadataOpen = true;"
{{ $t("delete") }} />
</el-dropdown-item>
<el-dropdown-item
v-if="!props.isCreating"
:icon="ContentCopy"
size="large"
@click="() => router.push({name: 'flows/create', query: {copy: true}})"
>
{{ $t("copy") }}
</el-dropdown-item>
<el-dropdown-item
v-if="isAllowedEdit"
:icon="Exclamation"
size="large"
@click="isNewErrorOpen = true;"
:disabled="!flowHaveTasks()"
>
{{ $t("add global error handler") }}
</el-dropdown-item>
<el-dropdown-item
v-if="isAllowedEdit"
:icon="LightningBolt"
size="large"
@click="isNewTriggerOpen = true;"
:disabled="!flowHaveTasks()"
>
{{ $t("add trigger") }}
</el-dropdown-item>
<el-dropdown-item
v-if="isAllowedEdit"
:icon="FileEdit"
size="large"
@click="isEditMetadataOpen = true;"
>
{{ $t("edit metadata") }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</li>
<li>
<el-button
:icon="ContentSave"
@click="save"
v-if="isAllowedEdit"
:type="flowError ? 'danger' : 'primary'"
:disabled="!haveChange && !isCreating"
class="edit-flow-save-button"
>
{{ $t("save") }}
</el-button>
</li>
</ul>
</template> </template>
</editor> </editor>
<div class="slider" @mousedown="dragEditor" v-if="combinedEditor" /> <div class="slider" @mousedown="dragEditor" v-if="combinedEditor" />
@@ -859,6 +807,22 @@
class="to-topology-button" class="to-topology-button"
@switch-view="switchViewType" @switch-view="switchViewType"
/> />
<EditorButtons
v-if="[editorViewTypes.TOPOLOGY, editorViewTypes.SOURCE_TOPOLOGY].includes(viewType)"
:is-creating="props.isCreating"
:is-read-only="props.isReadOnly"
:can-delete="canDelete"
:is-allowed-edit="isAllowedEdit"
:have-change="haveChange"
:flow-have-tasks="flowHaveTasks()"
:flow-error="flowError"
@delete-flow="deleteFlow"
@save="save"
@copy="() => router.push({name: 'flows/create', query: {copy: true}})"
@open-new-error="isNewErrorOpen = true;"
@open-new-trigger="isNewTriggerOpen = true;"
@open-edit-metadata="isEditMetadataOpen = true;"
/>
</el-card> </el-card>
</template> </template>
@@ -879,6 +843,13 @@
right: 45px; right: 45px;
} }
.to-action-button {
position: absolute;
bottom: 30px;
right: 45px;
display: flex;
}
.editor-combined { .editor-combined {
height: 100%; height: 100%;
width: 50%; width: 50%;

View File

@@ -53,6 +53,8 @@
:allow-auto-expand-subflows="false" :allow-auto-expand-subflows="false"
:target-execution-id="currentTaskRun.outputs.executionId" :target-execution-id="currentTaskRun.outputs.executionId"
:class="$el.classList.contains('even') ? '' : 'even'" :class="$el.classList.contains('even') ? '' : 'even'"
:show-progress-bar="showProgressBar"
:show-logs="showLogs"
/> />
</DynamicScrollerItem> </DynamicScrollerItem>
</template> </template>
@@ -315,6 +317,16 @@
this.logsWithIndexByAttemptUid[this.attemptUid(taskRun.id, this.selectedAttemptNumberByTaskRunId[taskRun.id])])) && this.logsWithIndexByAttemptUid[this.attemptUid(taskRun.id, this.selectedAttemptNumberByTaskRunId[taskRun.id])])) &&
this.showLogs this.showLogs
}, },
followExecution(executionId) {
this.$store
.dispatch("execution/followExecution", {id: executionId})
.then(sse => {
this.executionSSE = sse;
this.executionSSE.onmessage = async (event) => {
this.followedExecution = JSON.parse(event.data);
}
});
},
followLogs(executionId) { followLogs(executionId) {
this.$store this.$store
.dispatch("execution/followLogs", {id: executionId}) .dispatch("execution/followLogs", {id: executionId})

View File

@@ -1,7 +1,7 @@
<template> <template>
<sidebar-menu <sidebar-menu
id="side-menu" id="side-menu"
:menu="menu" :menu="localMenu"
@update:collapsed="onToggleCollapse" @update:collapsed="onToggleCollapse"
width="268px" width="268px"
:collapsed="collapsed" :collapsed="collapsed"
@@ -44,6 +44,7 @@
import TimerCogOutline from "vue-material-design-icons/TimerCogOutline.vue"; import TimerCogOutline from "vue-material-design-icons/TimerCogOutline.vue";
import {mapState} from "vuex"; import {mapState} from "vuex";
import AccountHardHatOutline from "vue-material-design-icons/AccountHardHatOutline.vue"; import AccountHardHatOutline from "vue-material-design-icons/AccountHardHatOutline.vue";
import {shallowRef} from "vue";
export default { export default {
components: { components: {
@@ -54,6 +55,16 @@
}, },
emits: ["menu-collapse"], emits: ["menu-collapse"],
methods: { methods: {
flattenMenu(menu) {
return menu.reduce((acc, item) => {
if (item.child) {
acc.push(...this.flattenMenu(item.child));
}
acc.push(item);
return acc;
}, []);
},
onToggleCollapse(folded) { onToggleCollapse(folded) {
this.collapsed = folded; this.collapsed = folded;
localStorage.setItem("menuCollapsed", folded ? "true" : "false"); localStorage.setItem("menuCollapsed", folded ? "true" : "false");
@@ -71,7 +82,7 @@
r.class = "vsm--link_active"; r.class = "vsm--link_active";
} }
if (r.child && r.child.some(c => this.$route.path.startsWith(c.href))) { if (r.child && r.child.some(c => this.$route.path.startsWith(c.href) || c.routes?.includes(this.$route.name))) {
r.class = "vsm--link_active"; r.class = "vsm--link_active";
r.child = this.disabledCurrentRoute(r.child); r.child = this.disabledCurrentRoute(r.child);
} }
@@ -88,7 +99,7 @@
href: {name: "home"}, href: {name: "home"},
title: this.$t("homeDashboard.title"), title: this.$t("homeDashboard.title"),
icon: { icon: {
element: ViewDashboardVariantOutline, element: shallowRef(ViewDashboardVariantOutline),
class: "menu-icon", class: "menu-icon",
}, },
}, },
@@ -96,7 +107,7 @@
href: {name: "editor"}, href: {name: "editor"},
title: this.$t("editor"), title: this.$t("editor"),
icon: { icon: {
element: FolderEditOutline, element: shallowRef(FolderEditOutline),
class: "menu-icon", class: "menu-icon",
}, },
}, },
@@ -105,7 +116,7 @@
routes: this.routeStartWith("flows"), routes: this.routeStartWith("flows"),
title: this.$t("flows"), title: this.$t("flows"),
icon: { icon: {
element: FileTreeOutline, element: shallowRef(FileTreeOutline),
class: "menu-icon", class: "menu-icon",
}, },
exact: false, exact: false,
@@ -115,7 +126,7 @@
routes: this.routeStartWith("templates"), routes: this.routeStartWith("templates"),
title: this.$t("templates"), title: this.$t("templates"),
icon: { icon: {
element: ContentCopy, element: shallowRef(ContentCopy),
class: "menu-icon", class: "menu-icon",
}, },
hidden: !this.configs.isTemplateEnabled hidden: !this.configs.isTemplateEnabled
@@ -125,7 +136,7 @@
routes: this.routeStartWith("executions"), routes: this.routeStartWith("executions"),
title: this.$t("executions"), title: this.$t("executions"),
icon: { icon: {
element: TimelineClockOutline, element: shallowRef(TimelineClockOutline),
class: "menu-icon" class: "menu-icon"
}, },
}, },
@@ -134,7 +145,7 @@
routes: this.routeStartWith("taskruns"), routes: this.routeStartWith("taskruns"),
title: this.$t("taskruns"), title: this.$t("taskruns"),
icon: { icon: {
element: TimelineTextOutline, element: shallowRef(TimelineTextOutline),
class: "menu-icon" class: "menu-icon"
}, },
hidden: !this.configs.isTaskRunEnabled hidden: !this.configs.isTaskRunEnabled
@@ -144,7 +155,7 @@
routes: this.routeStartWith("logs"), routes: this.routeStartWith("logs"),
title: this.$t("logs"), title: this.$t("logs"),
icon: { icon: {
element: NotebookOutline, element: shallowRef(NotebookOutline),
class: "menu-icon" class: "menu-icon"
}, },
}, },
@@ -153,7 +164,7 @@
routes: this.routeStartWith("blueprints"), routes: this.routeStartWith("blueprints"),
title: this.$t("blueprints.title"), title: this.$t("blueprints.title"),
icon: { icon: {
element: Ballot, element: shallowRef(Ballot),
class: "menu-icon" class: "menu-icon"
}, },
}, },
@@ -161,7 +172,7 @@
title: this.$t("administration"), title: this.$t("administration"),
routes: this.routeStartWith("admin"), routes: this.routeStartWith("admin"),
icon: { icon: {
element: AccountSupervisorOutline, element: shallowRef(AccountSupervisorOutline),
class: "menu-icon" class: "menu-icon"
}, },
child: [ child: [
@@ -170,7 +181,7 @@
routes: this.routeStartWith("admin/triggers"), routes: this.routeStartWith("admin/triggers"),
title: this.$t("triggers"), title: this.$t("triggers"),
icon: { icon: {
element: TimerCogOutline, element: shallowRef(TimerCogOutline),
class: "menu-icon" class: "menu-icon"
} }
}, },
@@ -179,7 +190,7 @@
routes: this.routeStartWith("admin/workers"), routes: this.routeStartWith("admin/workers"),
title: this.$t("workers"), title: this.$t("workers"),
icon: { icon: {
element: AccountHardHatOutline, element: shallowRef(AccountHardHatOutline),
class: "menu-icon" class: "menu-icon"
}, },
} }
@@ -190,41 +201,54 @@
routes: this.routeStartWith("admin/settings"), routes: this.routeStartWith("admin/settings"),
title: this.$t("settings"), title: this.$t("settings"),
icon: { icon: {
element: CogOutline, element: shallowRef(CogOutline),
class: "menu-icon" class: "menu-icon"
} }
} }
]; ];
}, },
expandParentIfNeeded() { expandParentIfNeeded() {
document.querySelectorAll(".vsm--link_level-1.vsm--link_active:not(.vsm--link_open)[aria-haspopup]").forEach(e => e.click()); document.querySelectorAll(".vsm--link.vsm--link_level-1.vsm--link_active:not(.vsm--link_open)[aria-haspopup]").forEach(e => {
e.click()
});
} }
}, },
updated() {
// Required here because in mounted() the menu is not yet rendered
this.expandParentIfNeeded();
},
watch: { watch: {
menu: { menu: {
handler() { handler(newVal, oldVal) {
this.$el.querySelectorAll(".vsm--item span").forEach(e => { // Check if the active menu item has changed, if yes then update the menu
//empty icon name on mouseover if (JSON.stringify(this.flattenMenu(newVal).map(e => e.class?.includes("vsm--link_active") ?? false)) !==
e.setAttribute("title", "") JSON.stringify(this.flattenMenu(oldVal).map(e => e.class?.includes("vsm--link_active") ?? false))) {
}); this.localMenu = newVal;
this.expandParentIfNeeded(); this.$el.querySelectorAll(".vsm--item span").forEach(e => {
//empty icon name on mouseover
e.setAttribute("title", "")
});
}
}, },
flush: 'post' flush: "post",
} deep: true
},
}, },
data() { data() {
return { return {
collapsed: localStorage.getItem("menuCollapsed") === "true", collapsed: localStorage.getItem("menuCollapsed") === "true",
localMenu: []
}; };
}, },
computed: { computed: {
...mapState("misc", ["configs"]), ...
mapState("misc", ["configs"]),
menu() { menu() {
return this.disabledCurrentRoute(this.generateMenu()); return this.disabledCurrentRoute(this.generateMenu());
} }
}, },
mounted() { mounted() {
this.expandParentIfNeeded(); this.localMenu = this.menu;
} }
}; };
</script> </script>

View File

@@ -1,11 +1,11 @@
import {apiUrl} from "override/utils/route"; import {apiUrlWithoutTenants} from "override/utils/route";
export default { export default {
namespaced: true, namespaced: true,
actions: { actions: {
findAll(_, __) { findAll(_, __) {
return this.$http.get(`${apiUrl(this)}/workers`).then(response => { return this.$http.get(`${apiUrlWithoutTenants(this)}/workers`).then(response => {
return response.data; return response.data;
}) })
} }

View File

@@ -413,11 +413,6 @@ form.ks-horizontal {
&.is-dark { &.is-dark {
color: var(--bs-gray-100); color: var(--bs-gray-100);
html:not(.dark) & {
* {
color: var(--bs-gray-100);
}
}
background: var(--bs-gray-900); background: var(--bs-gray-900);
border: 1px solid var(--bs-border-color); border: 1px solid var(--bs-border-color);

View File

@@ -1009,12 +1009,11 @@ public class ExecutionController {
@Parameter(description = "The execution id") @PathVariable String executionId, @Parameter(description = "The execution id") @PathVariable String executionId,
@Parameter(description = "The internal storage uri") @QueryValue URI path, @Parameter(description = "The internal storage uri") @QueryValue URI path,
@Parameter(description = "The max row returns") @QueryValue @Nullable Integer maxRows, @Parameter(description = "The max row returns") @QueryValue @Nullable Integer maxRows,
@Parameter(description = "The file encoding as Java charset name. Defaults to UTF-8", example = "ISO-8859-1") @QueryValue @Nullable String encoding @Parameter(description = "The file encoding as Java charset name. Defaults to UTF-8", example = "ISO-8859-1") @QueryValue(defaultValue = "UTF-8") String encoding
) throws IOException { ) throws IOException {
this.validateFile(executionId, path, "/api/v1/executions/{executionId}/file?path=" + path); this.validateFile(executionId, path, "/api/v1/executions/{executionId}/file?path=" + path);
String extension = FilenameUtils.getExtension(path.toString()); String extension = FilenameUtils.getExtension(path.toString());
InputStream fileStream = storageInterface.get(tenantService.resolveTenant(), path);
Optional<Charset> charset; Optional<Charset> charset;
try { try {
@@ -1023,13 +1022,15 @@ public class ExecutionController {
throw new IllegalArgumentException("Unable to preview using encoding '" + encoding + "'"); throw new IllegalArgumentException("Unable to preview using encoding '" + encoding + "'");
} }
FileRender fileRender = FileRenderBuilder.of( try (InputStream fileStream = storageInterface.get(tenantService.resolveTenant(), path)){
extension, FileRender fileRender = FileRenderBuilder.of(
fileStream, extension,
charset, fileStream,
maxRows == null ? this.initialPreviewRows : (maxRows > this.maxPreviewRows ? this.maxPreviewRows : maxRows) charset,
); maxRows == null ? this.initialPreviewRows : (maxRows > this.maxPreviewRows ? this.maxPreviewRows : maxRows)
);
return HttpResponse.ok(fileRender); return HttpResponse.ok(fileRender);
}
} }
} }

View File

@@ -138,7 +138,9 @@ public class NamespaceFileController {
) throws IOException, URISyntaxException { ) throws IOException, URISyntaxException {
ensureWritableFile(path); ensureWritableFile(path);
storageInterface.put(tenantService.resolveTenant(), toNamespacedStorageUri(namespace, path), new BufferedInputStream(fileContent.getInputStream())); try(BufferedInputStream inputStream = new BufferedInputStream(fileContent.getInputStream())) {
storageInterface.put(tenantService.resolveTenant(), toNamespacedStorageUri(namespace, path), inputStream);
}
} }
@ExecuteOn(TaskExecutors.IO) @ExecuteOn(TaskExecutors.IO)