mirror of
https://github.com/kestra-io/kestra.git
synced 2025-12-25 11:12:12 -05:00
Compare commits
31 Commits
fix/remove
...
v0.13.10
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6b6c5d63f | ||
|
|
47273ddd3b | ||
|
|
af8f8e0138 | ||
|
|
70a3e46d9b | ||
|
|
c4097f5f66 | ||
|
|
5a22c218f3 | ||
|
|
0cf0af7474 | ||
|
|
83b7399171 | ||
|
|
d948e14319 | ||
|
|
7143de9ec6 | ||
|
|
33ece57996 | ||
|
|
5804ff7b26 | ||
|
|
2888d929aa | ||
|
|
8a75fd3fe5 | ||
|
|
3b1bed8bf4 | ||
|
|
01128f0927 | ||
|
|
bface3755a | ||
|
|
8fe518ceae | ||
|
|
28e39ab756 | ||
|
|
24843cfc17 | ||
|
|
2e1c3733a4 | ||
|
|
61db3ccedb | ||
|
|
6a03dc9e41 | ||
|
|
b2a107bc55 | ||
|
|
97a6afbe2e | ||
|
|
cc800b4528 | ||
|
|
da50cf4287 | ||
|
|
1ecfed1559 | ||
|
|
4a3e250019 | ||
|
|
2917c178a5 | ||
|
|
0ceaeb4a97 |
@@ -22,6 +22,7 @@ dependencies {
|
||||
implementation group: 'net.jodah', name: 'failsafe', version: '2.4.4'
|
||||
implementation 'com.github.oshi:oshi-core:6.4.6'
|
||||
implementation 'io.pebbletemplates:pebble:3.2.1'
|
||||
implementation group: 'co.elastic.logging', name: 'logback-ecs-encoder', version: '1.5.0'
|
||||
|
||||
// scheduler
|
||||
implementation group: 'com.cronutils', name: 'cron-utils', version: '9.2.1'
|
||||
|
||||
@@ -696,7 +696,7 @@ public class Execution implements DeletedInterface, TenantInterface {
|
||||
* @return List of parent {@link TaskRun}
|
||||
*/
|
||||
public List<TaskRun> findChilds(TaskRun taskRun) {
|
||||
if (taskRun.getParentTaskRunId() == null) {
|
||||
if (taskRun.getParentTaskRunId() == null || this.taskRunList == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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 lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
@@ -7,6 +9,9 @@ import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@SuperBuilder(toBuilder = true)
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@@ -15,4 +20,22 @@ import lombok.experimental.SuperBuilder;
|
||||
@EqualsAndHashCode
|
||||
public class FlowWithException extends FlowWithSource {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ public final class ExecutableUtils {
|
||||
|
||||
return WorkerTaskExecution.builder()
|
||||
.task(currentTask)
|
||||
.taskRun(currentTaskRun)
|
||||
.taskRun(currentTaskRun.withState(State.Type.RUNNING))
|
||||
.execution(execution)
|
||||
.iteration(iteration)
|
||||
.build();
|
||||
@@ -117,11 +117,11 @@ public final class ExecutableUtils {
|
||||
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();
|
||||
|
||||
int currentStateIteration = getIterationCounter(iterations, currentState, maxIterations) + 1;
|
||||
iterations.put(currentState.toString(), currentStateIteration);
|
||||
int currentStateIteration = iterations.getOrDefault(currentState.toString(), 0);
|
||||
iterations.put(currentState.toString(), currentStateIteration + 1);
|
||||
if (previousState.isPresent() && previousState.get() != currentState) {
|
||||
int previousStateIterations = getIterationCounter(iterations, previousState.get(), maxIterations) - 1;
|
||||
iterations.put(previousState.get().toString(), previousStateIterations);
|
||||
int previousStateIterations = iterations.getOrDefault(previousState.get() .toString(), maxIterations);
|
||||
iterations.put(previousState.get().toString(), previousStateIterations - 1);
|
||||
}
|
||||
|
||||
// update the state to success if current == max
|
||||
|
||||
@@ -41,7 +41,7 @@ public class Executor {
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@@ -107,7 +107,6 @@ public class NamespaceFilesService {
|
||||
private void copy(String tenantId, String namespace, Path basePath, List<URI> files) throws IOException {
|
||||
files
|
||||
.forEach(throwConsumer(f -> {
|
||||
InputStream inputStream = storageInterface.get(tenantId, uri(namespace, f));
|
||||
Path destination = Paths.get(basePath.toString(), f.getPath());
|
||||
|
||||
if (!destination.getParent().toFile().exists()) {
|
||||
@@ -115,7 +114,9 @@ public class NamespaceFilesService {
|
||||
destination.getParent().toFile().mkdirs();
|
||||
}
|
||||
|
||||
Files.copy(inputStream, destination);
|
||||
try (InputStream inputStream = storageInterface.get(tenantId, uri(namespace, f))) {
|
||||
Files.copy(inputStream, destination);
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package io.kestra.core.runners.pebble.functions;
|
||||
|
||||
import io.kestra.core.storages.StorageInterface;
|
||||
import io.kestra.core.tenant.TenantService;
|
||||
import io.kestra.core.utils.Slugify;
|
||||
import io.pebbletemplates.pebble.error.PebbleException;
|
||||
import io.pebbletemplates.pebble.extension.Function;
|
||||
@@ -11,6 +10,7 @@ import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
@@ -47,7 +47,9 @@ public class ReadFileFunction implements Function {
|
||||
private String readFromNamespaceFile(EvaluationContext context, String path) throws IOException {
|
||||
Map<String, String> flow = (Map<String, String>) context.getVariable("flow");
|
||||
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 {
|
||||
@@ -69,7 +71,9 @@ public class ReadFileFunction implements Function {
|
||||
}
|
||||
}
|
||||
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) {
|
||||
|
||||
@@ -59,7 +59,7 @@ public abstract class AbstractScheduler implements Scheduler {
|
||||
private final Map<String, ZonedDateTime> lastEvaluate = new ConcurrentHashMap<>();
|
||||
|
||||
// The triggerStateSavedLock must be used when accessing triggerStateSaved
|
||||
private final Object triggerStateSavedLock = new Object();
|
||||
protected final Object triggerStateSavedLock = new Object();
|
||||
private final Map<String, Trigger> triggerStateSaved = new ConcurrentHashMap<>();
|
||||
protected SchedulerTriggerStateInterface triggerState;
|
||||
|
||||
|
||||
@@ -4,10 +4,7 @@ import io.kestra.core.runners.RunContext;
|
||||
import io.kestra.core.storages.StorageSplitInterface;
|
||||
import io.micronaut.core.convert.format.ReadableBytesTypeConverter;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.io.*;
|
||||
import java.net.URI;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
@@ -42,8 +42,7 @@ public interface StorageInterface {
|
||||
* @return true if the uri points to a file/object that exist in the internal storage.
|
||||
*/
|
||||
default boolean exists(String tenantId, URI uri) {
|
||||
try {
|
||||
get(tenantId, uri);
|
||||
try (InputStream ignored = get(tenantId, uri)){
|
||||
return true;
|
||||
} catch (IOException ieo) {
|
||||
return false;
|
||||
|
||||
@@ -175,7 +175,10 @@ public class Flow extends Task implements ExecutableTask<Flow.Output> {
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -54,9 +54,9 @@ public abstract class AbstractState extends Task {
|
||||
|
||||
|
||||
protected Map<String, Object> get(RunContext runContext) throws IllegalVariableEvaluationException, IOException {
|
||||
InputStream taskStateFile = runContext.getTaskStateFile("tasks-states", runContext.render(this.name), this.namespace, this.taskrunValue);
|
||||
|
||||
return JacksonMapper.ofJson(false).readValue(taskStateFile, TYPE_REFERENCE);
|
||||
try (InputStream taskStateFile = runContext.getTaskStateFile("tasks-states", runContext.render(this.name), this.namespace, this.taskrunValue)) {
|
||||
return JacksonMapper.ofJson(false).readValue(taskStateFile, TYPE_REFERENCE);
|
||||
}
|
||||
}
|
||||
|
||||
protected Pair<URI, Map<String, Object>> merge(RunContext runContext, Map<String, Object> map) throws IllegalVariableEvaluationException, IOException {
|
||||
|
||||
@@ -16,6 +16,7 @@ import io.kestra.core.runners.RunContext;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
|
||||
@@ -128,7 +129,9 @@ public class Concat extends Task implements RunnableTask<Concat.Output> {
|
||||
|
||||
finalFiles.forEach(throwConsumer(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) {
|
||||
IOUtils.copy(new ByteArrayInputStream(this.separator.getBytes()), fileOutputStream);
|
||||
|
||||
@@ -165,7 +165,7 @@ public class FlowTopologyService {
|
||||
.filter(t -> t instanceof ExecutableTask)
|
||||
.map(t -> (ExecutableTask<?>) 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) {
|
||||
log.warn("Failed to detect flow task on namespace:'" + parent.getNamespace() + "', flowId:'" + parent.getId() + "'", e);
|
||||
|
||||
25
core/src/main/resources/logback/ecs.xml
Normal file
25
core/src/main/resources/logback/ecs.xml
Normal 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>
|
||||
@@ -105,8 +105,8 @@ public class ForEachItemCaseTest {
|
||||
assertThat(outputs.get("iterations"), notNullValue());
|
||||
Map<String, Integer> iterations = (Map<String, Integer>) outputs.get("iterations");
|
||||
assertThat(iterations.get("max"), is(3));
|
||||
assertThat(iterations.get("CREATED"), is(0));
|
||||
assertThat(iterations.get("RUNNING"), nullValue()); // if we didn't wait we will only observe CREATED and SUCCESS
|
||||
assertThat(iterations.get("CREATED"), nullValue());// if we didn't wait we will only observe RUNNING and SUCCESS
|
||||
assertThat(iterations.get("RUNNING"), is(0));
|
||||
assertThat(iterations.get("SUCCESS"), is(3));
|
||||
|
||||
// assert that not all subflows ran (depending on the speed of execution there can be some)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
version=0.13.0
|
||||
version=0.13.10
|
||||
|
||||
jacksonVersion=2.15.2
|
||||
micronautVersion=3.10.1
|
||||
|
||||
@@ -61,14 +61,7 @@ public abstract class AbstractJdbcFlowRepository extends AbstractJdbcRepository
|
||||
} catch (DeserializationException e) {
|
||||
try {
|
||||
JsonNode jsonNode = JdbcMapper.of().readTree(source);
|
||||
return FlowWithException.builder()
|
||||
.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();
|
||||
return FlowWithException.from(jsonNode, e).orElseThrow(() -> e);
|
||||
} catch (JsonProcessingException ex) {
|
||||
throw new DeserializationException(ex, source);
|
||||
}
|
||||
|
||||
@@ -406,7 +406,7 @@ public class JdbcExecutor implements ExecutorInterface {
|
||||
|
||||
// send a running worker task result to track running vs created status
|
||||
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 -> {
|
||||
// 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()) {
|
||||
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);
|
||||
|
||||
@@ -268,7 +268,7 @@ public abstract class JdbcRunnerTest {
|
||||
skipExecutionCaseTest.skipExecution();
|
||||
}
|
||||
|
||||
@Test
|
||||
@RetryingTest(5)
|
||||
void forEachItem() throws URISyntaxException, IOException, InterruptedException, TimeoutException {
|
||||
forEachItemCaseTest.forEachItem();
|
||||
}
|
||||
|
||||
@@ -237,7 +237,7 @@ public class MemoryExecutor implements ExecutorInterface {
|
||||
|
||||
// send a running worker task result to track running vs created status
|
||||
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 (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());
|
||||
|
||||
@@ -30,6 +30,31 @@ const extensionsToFetch = Object.entries(versionByExtensionIdToFetch).map(([exte
|
||||
}));
|
||||
|
||||
// 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 = {
|
||||
productConfiguration: {
|
||||
nameShort: "Kestra VSCode",
|
||||
@@ -77,5 +102,17 @@ window.product = {
|
||||
"workbench.colorTheme": THEME === "dark" ? "Sweet Dracula" : "Default Light Modern",
|
||||
// provide the Kestra root URL to extension
|
||||
"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
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
};
|
||||
@@ -447,7 +447,7 @@
|
||||
},
|
||||
methods: {
|
||||
onDisplayColumnsChange(event) {
|
||||
localStorage.setItem("displayExecutionsColumns", event);
|
||||
localStorage.setItem(this.storageKey, event);
|
||||
this.displayColumns = event;
|
||||
},
|
||||
displayColumn(column) {
|
||||
@@ -608,4 +608,4 @@
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -142,7 +142,7 @@
|
||||
executionId: this.executionId,
|
||||
path: this.value,
|
||||
maxRows: this.maxPreview,
|
||||
encoding: this.encoding
|
||||
encoding: this.encoding.value
|
||||
})
|
||||
.then(() => {
|
||||
this.isPreviewOpen = true;
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
<el-select
|
||||
class="d-none d-md-inline-block"
|
||||
:model-value="selectedAttemptNumberByTaskRunId[currentTaskRun.id]"
|
||||
@change="forwardEvent('swapDisplayedAttempt',(currentTaskRun.id, $event))"
|
||||
@change="forwardEvent('swapDisplayedAttempt', {taskRunId: currentTaskRun.id, attemptNumber: $event})"
|
||||
:disabled="!currentTaskRun.attempts || currentTaskRun.attempts?.length <= 1"
|
||||
>
|
||||
<el-option
|
||||
|
||||
@@ -456,15 +456,15 @@
|
||||
}
|
||||
|
||||
.bottom-right {
|
||||
bottom: var(--spacer);
|
||||
right: var(--spacer);
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
|
||||
ul {
|
||||
display: flex;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
gap: calc(var(--spacer) / 2);
|
||||
//gap: calc(var(--spacer) / 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
121
ui/src/components/inputs/EditorButtons.vue
Normal file
121
ui/src/components/inputs/EditorButtons.vue
Normal 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>
|
||||
@@ -28,6 +28,7 @@
|
||||
import {editorViewTypes} from "../../utils/constants";
|
||||
import Utils from "@kestra-io/ui-libs/src/utils/Utils";
|
||||
import {apiUrl} from "override/utils/route";
|
||||
import EditorButtons from "./EditorButtons.vue";
|
||||
|
||||
const store = useStore();
|
||||
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" />
|
||||
</template>
|
||||
<template #buttons>
|
||||
<ul>
|
||||
<li v-if="isAllowedEdit || canDelete">
|
||||
<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="!props.isCreating && canDelete"
|
||||
:icon="Delete"
|
||||
size="large"
|
||||
@click="deleteFlow"
|
||||
>
|
||||
{{ $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>
|
||||
<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;"
|
||||
/>
|
||||
</template>
|
||||
</editor>
|
||||
<div class="slider" @mousedown="dragEditor" v-if="combinedEditor" />
|
||||
@@ -859,6 +807,22 @@
|
||||
class="to-topology-button"
|
||||
@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>
|
||||
</template>
|
||||
|
||||
@@ -879,6 +843,13 @@
|
||||
right: 45px;
|
||||
}
|
||||
|
||||
.to-action-button {
|
||||
position: absolute;
|
||||
bottom: 30px;
|
||||
right: 45px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.editor-combined {
|
||||
height: 100%;
|
||||
width: 50%;
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
:level="level"
|
||||
:log="item"
|
||||
:exclude-metas="excludeMetas"
|
||||
v-if="filter === '' || item.message.toLowerCase().includes(filter)"
|
||||
v-if="filter === '' || item.message?.toLowerCase().includes(filter)"
|
||||
/>
|
||||
<task-run-details
|
||||
v-if="!taskRunId && isSubflow(currentTaskRun) && currentTaskRun.outputs?.executionId"
|
||||
@@ -53,6 +53,8 @@
|
||||
:allow-auto-expand-subflows="false"
|
||||
:target-execution-id="currentTaskRun.outputs.executionId"
|
||||
:class="$el.classList.contains('even') ? '' : 'even'"
|
||||
:show-progress-bar="showProgressBar"
|
||||
:show-logs="showLogs"
|
||||
/>
|
||||
</DynamicScrollerItem>
|
||||
</template>
|
||||
@@ -264,7 +266,7 @@
|
||||
},
|
||||
logsWithIndexByAttemptUid() {
|
||||
const indexedLogs = this.logs
|
||||
.filter(logLine => this.filter === "" || logLine.message.toLowerCase().includes(this.filter) || this.isSubflow(this.taskRunById[logLine.taskRunId]))
|
||||
.filter(logLine => this.filter === "" || logLine?.message.toLowerCase().includes(this.filter) || this.isSubflow(this.taskRunById[logLine.taskRunId]))
|
||||
.map((logLine, index) => ({...logLine, index}));
|
||||
|
||||
return _groupBy(indexedLogs, indexedLog => this.attemptUid(indexedLog.taskRunId, indexedLog.attemptNumber));
|
||||
@@ -315,6 +317,16 @@
|
||||
this.logsWithIndexByAttemptUid[this.attemptUid(taskRun.id, this.selectedAttemptNumberByTaskRunId[taskRun.id])])) &&
|
||||
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) {
|
||||
this.$store
|
||||
.dispatch("execution/followLogs", {id: executionId})
|
||||
@@ -418,11 +430,14 @@
|
||||
toggleShowAttempt(attemptUid) {
|
||||
this.shownAttemptsUid = _xor(this.shownAttemptsUid, [attemptUid])
|
||||
},
|
||||
swapDisplayedAttempt(taskRunId, newDisplayedAttemptUid) {
|
||||
swapDisplayedAttempt(event) {
|
||||
const {taskRunId, attemptNumber: newDisplayedAttemptNumber} = event;
|
||||
this.shownAttemptsUid = this.shownAttemptsUid.map(attemptUid => attemptUid.startsWith(`${taskRunId}-`)
|
||||
? this.attemptUid(taskRunId, newDisplayedAttemptUid)
|
||||
? this.attemptUid(taskRunId, newDisplayedAttemptNumber)
|
||||
: attemptUid
|
||||
);
|
||||
|
||||
this.selectedAttemptNumberByTaskRunId[taskRunId] = newDisplayedAttemptNumber;
|
||||
},
|
||||
taskType(taskRun) {
|
||||
const task = FlowUtils.findTaskById(this.flow, taskRun.taskId);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<sidebar-menu
|
||||
id="side-menu"
|
||||
:menu="menu"
|
||||
:menu="localMenu"
|
||||
@update:collapsed="onToggleCollapse"
|
||||
width="268px"
|
||||
:collapsed="collapsed"
|
||||
@@ -44,6 +44,7 @@
|
||||
import TimerCogOutline from "vue-material-design-icons/TimerCogOutline.vue";
|
||||
import {mapState} from "vuex";
|
||||
import AccountHardHatOutline from "vue-material-design-icons/AccountHardHatOutline.vue";
|
||||
import {shallowRef} from "vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -54,6 +55,16 @@
|
||||
},
|
||||
emits: ["menu-collapse"],
|
||||
methods: {
|
||||
flattenMenu(menu) {
|
||||
return menu.reduce((acc, item) => {
|
||||
if (item.child) {
|
||||
acc.push(...this.flattenMenu(item.child));
|
||||
}
|
||||
|
||||
acc.push(item);
|
||||
return acc;
|
||||
}, []);
|
||||
},
|
||||
onToggleCollapse(folded) {
|
||||
this.collapsed = folded;
|
||||
localStorage.setItem("menuCollapsed", folded ? "true" : "false");
|
||||
@@ -71,7 +82,7 @@
|
||||
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.child = this.disabledCurrentRoute(r.child);
|
||||
}
|
||||
@@ -88,7 +99,7 @@
|
||||
href: {name: "home"},
|
||||
title: this.$t("homeDashboard.title"),
|
||||
icon: {
|
||||
element: ViewDashboardVariantOutline,
|
||||
element: shallowRef(ViewDashboardVariantOutline),
|
||||
class: "menu-icon",
|
||||
},
|
||||
},
|
||||
@@ -96,7 +107,7 @@
|
||||
href: {name: "editor"},
|
||||
title: this.$t("editor"),
|
||||
icon: {
|
||||
element: FolderEditOutline,
|
||||
element: shallowRef(FolderEditOutline),
|
||||
class: "menu-icon",
|
||||
},
|
||||
},
|
||||
@@ -105,7 +116,7 @@
|
||||
routes: this.routeStartWith("flows"),
|
||||
title: this.$t("flows"),
|
||||
icon: {
|
||||
element: FileTreeOutline,
|
||||
element: shallowRef(FileTreeOutline),
|
||||
class: "menu-icon",
|
||||
},
|
||||
exact: false,
|
||||
@@ -115,7 +126,7 @@
|
||||
routes: this.routeStartWith("templates"),
|
||||
title: this.$t("templates"),
|
||||
icon: {
|
||||
element: ContentCopy,
|
||||
element: shallowRef(ContentCopy),
|
||||
class: "menu-icon",
|
||||
},
|
||||
hidden: !this.configs.isTemplateEnabled
|
||||
@@ -125,7 +136,7 @@
|
||||
routes: this.routeStartWith("executions"),
|
||||
title: this.$t("executions"),
|
||||
icon: {
|
||||
element: TimelineClockOutline,
|
||||
element: shallowRef(TimelineClockOutline),
|
||||
class: "menu-icon"
|
||||
},
|
||||
},
|
||||
@@ -134,7 +145,7 @@
|
||||
routes: this.routeStartWith("taskruns"),
|
||||
title: this.$t("taskruns"),
|
||||
icon: {
|
||||
element: TimelineTextOutline,
|
||||
element: shallowRef(TimelineTextOutline),
|
||||
class: "menu-icon"
|
||||
},
|
||||
hidden: !this.configs.isTaskRunEnabled
|
||||
@@ -144,7 +155,7 @@
|
||||
routes: this.routeStartWith("logs"),
|
||||
title: this.$t("logs"),
|
||||
icon: {
|
||||
element: NotebookOutline,
|
||||
element: shallowRef(NotebookOutline),
|
||||
class: "menu-icon"
|
||||
},
|
||||
},
|
||||
@@ -153,7 +164,7 @@
|
||||
routes: this.routeStartWith("blueprints"),
|
||||
title: this.$t("blueprints.title"),
|
||||
icon: {
|
||||
element: Ballot,
|
||||
element: shallowRef(Ballot),
|
||||
class: "menu-icon"
|
||||
},
|
||||
},
|
||||
@@ -161,7 +172,7 @@
|
||||
title: this.$t("administration"),
|
||||
routes: this.routeStartWith("admin"),
|
||||
icon: {
|
||||
element: AccountSupervisorOutline,
|
||||
element: shallowRef(AccountSupervisorOutline),
|
||||
class: "menu-icon"
|
||||
},
|
||||
child: [
|
||||
@@ -170,7 +181,7 @@
|
||||
routes: this.routeStartWith("admin/triggers"),
|
||||
title: this.$t("triggers"),
|
||||
icon: {
|
||||
element: TimerCogOutline,
|
||||
element: shallowRef(TimerCogOutline),
|
||||
class: "menu-icon"
|
||||
}
|
||||
},
|
||||
@@ -179,7 +190,7 @@
|
||||
routes: this.routeStartWith("admin/workers"),
|
||||
title: this.$t("workers"),
|
||||
icon: {
|
||||
element: AccountHardHatOutline,
|
||||
element: shallowRef(AccountHardHatOutline),
|
||||
class: "menu-icon"
|
||||
},
|
||||
}
|
||||
@@ -190,41 +201,54 @@
|
||||
routes: this.routeStartWith("admin/settings"),
|
||||
title: this.$t("settings"),
|
||||
icon: {
|
||||
element: CogOutline,
|
||||
element: shallowRef(CogOutline),
|
||||
class: "menu-icon"
|
||||
}
|
||||
}
|
||||
];
|
||||
},
|
||||
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: {
|
||||
menu: {
|
||||
handler() {
|
||||
this.$el.querySelectorAll(".vsm--item span").forEach(e => {
|
||||
//empty icon name on mouseover
|
||||
e.setAttribute("title", "")
|
||||
});
|
||||
this.expandParentIfNeeded();
|
||||
handler(newVal, oldVal) {
|
||||
// Check if the active menu item has changed, if yes then update the menu
|
||||
if (JSON.stringify(this.flattenMenu(newVal).map(e => e.class?.includes("vsm--link_active") ?? false)) !==
|
||||
JSON.stringify(this.flattenMenu(oldVal).map(e => e.class?.includes("vsm--link_active") ?? false))) {
|
||||
this.localMenu = newVal;
|
||||
this.$el.querySelectorAll(".vsm--item span").forEach(e => {
|
||||
//empty icon name on mouseover
|
||||
e.setAttribute("title", "")
|
||||
});
|
||||
}
|
||||
},
|
||||
flush: 'post'
|
||||
}
|
||||
flush: "post",
|
||||
deep: true
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
collapsed: localStorage.getItem("menuCollapsed") === "true",
|
||||
localMenu: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState("misc", ["configs"]),
|
||||
...
|
||||
mapState("misc", ["configs"]),
|
||||
menu() {
|
||||
return this.disabledCurrentRoute(this.generateMenu());
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.expandParentIfNeeded();
|
||||
this.localMenu = this.menu;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import {apiUrl} from "override/utils/route";
|
||||
import {apiUrlWithoutTenants} from "override/utils/route";
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
|
||||
actions: {
|
||||
findAll(_, __) {
|
||||
return this.$http.get(`${apiUrl(this)}/workers`).then(response => {
|
||||
return this.$http.get(`${apiUrlWithoutTenants(this)}/workers`).then(response => {
|
||||
return response.data;
|
||||
})
|
||||
}
|
||||
|
||||
@@ -413,11 +413,6 @@ form.ks-horizontal {
|
||||
|
||||
&.is-dark {
|
||||
color: var(--bs-gray-100);
|
||||
html:not(.dark) & {
|
||||
* {
|
||||
color: var(--bs-gray-100);
|
||||
}
|
||||
}
|
||||
|
||||
background: var(--bs-gray-900);
|
||||
border: 1px solid var(--bs-border-color);
|
||||
|
||||
@@ -495,6 +495,11 @@ export default class YamlUtils {
|
||||
static updateMetadata(source, metadata) {
|
||||
// TODO: check how to keep comments
|
||||
const yamlDoc = yaml.parseDocument(source);
|
||||
|
||||
if (!yamlDoc.contents.items) {
|
||||
return source;
|
||||
}
|
||||
|
||||
for (const property in metadata) {
|
||||
if (yamlDoc.contents.items.find(item => item.key.value === property)) {
|
||||
yamlDoc.contents.items.find(item => item.key.value === property).value = metadata[property];
|
||||
@@ -508,7 +513,12 @@ export default class YamlUtils {
|
||||
static cleanMetadata(source) {
|
||||
// Reorder and remove empty metadata
|
||||
const yamlDoc = yaml.parseDocument(source);
|
||||
const order = ["id", "namespace", "description", "labels", "inputs", "variables", "tasks", "triggers", "errors", "taskDefaults"];
|
||||
|
||||
if (!yamlDoc.contents.items) {
|
||||
return source;
|
||||
}
|
||||
|
||||
const order = ["id", "namespace", "description", "labels", "inputs", "variables", "tasks", "triggers", "errors", "taskDefaults", "concurrency"];
|
||||
const updatedItems = [];
|
||||
for (const prop of order) {
|
||||
const item = yamlDoc.contents.items.find(e => e.key.value === prop);
|
||||
@@ -532,12 +542,23 @@ export default class YamlUtils {
|
||||
}
|
||||
|
||||
static flowHaveTasks(source) {
|
||||
const tasks = yaml.parseDocument(source).contents.items.find(item => item.key.value === "tasks");
|
||||
return tasks && tasks.value.items && tasks.value.items.length >= 1;
|
||||
const yamlDoc = yaml.parseDocument(source);
|
||||
|
||||
if (!yamlDoc.contents.items) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const tasks = yamlDoc.contents.items.find(item => item.key.value === "tasks");
|
||||
return tasks?.value?.items?.length >= 1;
|
||||
}
|
||||
|
||||
static deleteMetadata(source, metadata) {
|
||||
const yamlDoc = yaml.parseDocument(source);
|
||||
|
||||
if (!yamlDoc.contents.items) {
|
||||
return source;
|
||||
}
|
||||
|
||||
const item = yamlDoc.contents.items.find(e => e.key.value === metadata);
|
||||
if (item) {
|
||||
yamlDoc.contents.items.splice(yamlDoc.contents.items.indexOf(item), 1);
|
||||
|
||||
@@ -1009,12 +1009,11 @@ public class ExecutionController {
|
||||
@Parameter(description = "The execution id") @PathVariable String executionId,
|
||||
@Parameter(description = "The internal storage uri") @QueryValue URI path,
|
||||
@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 {
|
||||
this.validateFile(executionId, path, "/api/v1/executions/{executionId}/file?path=" + path);
|
||||
|
||||
String extension = FilenameUtils.getExtension(path.toString());
|
||||
InputStream fileStream = storageInterface.get(tenantService.resolveTenant(), path);
|
||||
Optional<Charset> charset;
|
||||
|
||||
try {
|
||||
@@ -1023,13 +1022,15 @@ public class ExecutionController {
|
||||
throw new IllegalArgumentException("Unable to preview using encoding '" + encoding + "'");
|
||||
}
|
||||
|
||||
FileRender fileRender = FileRenderBuilder.of(
|
||||
extension,
|
||||
fileStream,
|
||||
charset,
|
||||
maxRows == null ? this.initialPreviewRows : (maxRows > this.maxPreviewRows ? this.maxPreviewRows : maxRows)
|
||||
);
|
||||
try (InputStream fileStream = storageInterface.get(tenantService.resolveTenant(), path)){
|
||||
FileRender fileRender = FileRenderBuilder.of(
|
||||
extension,
|
||||
fileStream,
|
||||
charset,
|
||||
maxRows == null ? this.initialPreviewRows : (maxRows > this.maxPreviewRows ? this.maxPreviewRows : maxRows)
|
||||
);
|
||||
|
||||
return HttpResponse.ok(fileRender);
|
||||
return HttpResponse.ok(fileRender);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,7 +138,9 @@ public class NamespaceFileController {
|
||||
) throws IOException, URISyntaxException {
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user