mirror of
https://github.com/kestra-io/kestra.git
synced 2025-12-26 05:00:31 -05:00
Compare commits
33 Commits
try-to-tes
...
global-sta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5a0dcc024 | ||
|
|
5c079b8b6b | ||
|
|
343d6b4eb9 | ||
|
|
d34d547412 | ||
|
|
7a542a24e2 | ||
|
|
5b1db68752 | ||
|
|
5b07b643d3 | ||
|
|
0e059772e4 | ||
|
|
f72e294e54 | ||
|
|
98dd884149 | ||
|
|
26c4f080fd | ||
|
|
01293de91c | ||
|
|
892b69f10e | ||
|
|
6f70d4d275 | ||
|
|
b41d2e456f | ||
|
|
5ec08eda8c | ||
|
|
7ed6b883ff | ||
|
|
eb166c9321 | ||
|
|
57aad1b931 | ||
|
|
60fe5b5c76 | ||
|
|
98c69b53bb | ||
|
|
d5d38559b4 | ||
|
|
4273ddc4f6 | ||
|
|
980c573a30 | ||
|
|
27109015f9 | ||
|
|
eba7d4f375 | ||
|
|
655a1172ee | ||
|
|
6e49a85acd | ||
|
|
4515bad6bd | ||
|
|
226dbd30c9 | ||
|
|
6b0c190edc | ||
|
|
c64df40a36 | ||
|
|
8af22d1bb2 |
@@ -37,7 +37,7 @@ plugins {
|
||||
id "com.vanniktech.maven.publish" version "0.34.0"
|
||||
|
||||
// OWASP dependency check
|
||||
id "org.owasp.dependencycheck" version "12.1.3" apply false
|
||||
id "org.owasp.dependencycheck" version "12.1.5" apply false
|
||||
}
|
||||
|
||||
idea {
|
||||
|
||||
@@ -2,19 +2,27 @@ package io.kestra.cli.commands.servers;
|
||||
|
||||
import io.kestra.cli.AbstractCommand;
|
||||
import io.kestra.core.contexts.KestraContext;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import picocli.CommandLine;
|
||||
|
||||
abstract public class AbstractServerCommand extends AbstractCommand implements ServerCommandInterface {
|
||||
@Slf4j
|
||||
public abstract class AbstractServerCommand extends AbstractCommand implements ServerCommandInterface {
|
||||
@CommandLine.Option(names = {"--port"}, description = "The port to bind")
|
||||
Integer serverPort;
|
||||
|
||||
@Override
|
||||
public Integer call() throws Exception {
|
||||
log.info("Machine information: {} available cpu(s), {}MB max memory, Java version {}", Runtime.getRuntime().availableProcessors(), maxMemoryInMB(), Runtime.version());
|
||||
|
||||
this.shutdownHook(true, () -> KestraContext.getContext().shutdown());
|
||||
|
||||
return super.call();
|
||||
}
|
||||
|
||||
private long maxMemoryInMB() {
|
||||
return Runtime.getRuntime().maxMemory() / 1024 / 1024;
|
||||
}
|
||||
|
||||
protected static int defaultWorkerThread() {
|
||||
return Runtime.getRuntime().availableProcessors() * 8;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import org.junitpioneer.jupiter.RetryingTest;
|
||||
|
||||
import static io.kestra.core.utils.Rethrow.throwRunnable;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -94,7 +95,7 @@ class FileChangedEventListenerTest {
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@RetryingTest(2)
|
||||
void testWithPluginDefault() throws IOException, TimeoutException {
|
||||
var tenant = TestsUtils.randomTenant(FileChangedEventListenerTest.class.getName(), "testWithPluginDefault");
|
||||
// remove the flow if it already exists
|
||||
|
||||
@@ -84,7 +84,7 @@ dependencies {
|
||||
|
||||
testImplementation "org.testcontainers:testcontainers:1.21.3"
|
||||
testImplementation "org.testcontainers:junit-jupiter:1.21.3"
|
||||
testImplementation "org.bouncycastle:bcpkix-jdk18on:1.81"
|
||||
testImplementation "org.bouncycastle:bcpkix-jdk18on"
|
||||
|
||||
testImplementation "org.wiremock:wiremock-jetty12"
|
||||
}
|
||||
|
||||
@@ -2,12 +2,13 @@ package io.kestra.core.models;
|
||||
|
||||
import io.kestra.core.utils.MapUtils;
|
||||
import jakarta.annotation.Nullable;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public record Label(@NotNull String key, @NotNull String value) {
|
||||
public record Label(@NotEmpty String key, @NotEmpty String value) {
|
||||
public static final String SYSTEM_PREFIX = "system.";
|
||||
|
||||
// system labels
|
||||
@@ -41,7 +42,7 @@ public record Label(@NotNull String key, @NotNull String value) {
|
||||
public static Map<String, String> toMap(@Nullable List<Label> labels) {
|
||||
if (labels == null || labels.isEmpty()) return Collections.emptyMap();
|
||||
return labels.stream()
|
||||
.filter(label -> label.value() != null && label.key() != null)
|
||||
.filter(label -> label.value() != null && !label.value().isEmpty() && label.key() != null && !label.key().isEmpty())
|
||||
// using an accumulator in case labels with the same key exists: the second is kept
|
||||
.collect(Collectors.toMap(Label::key, Label::value, (first, second) -> second, LinkedHashMap::new));
|
||||
}
|
||||
@@ -56,6 +57,7 @@ public record Label(@NotNull String key, @NotNull String value) {
|
||||
public static List<Label> deduplicate(@Nullable List<Label> labels) {
|
||||
if (labels == null || labels.isEmpty()) return Collections.emptyList();
|
||||
return toMap(labels).entrySet().stream()
|
||||
.filter(getEntryNotEmptyPredicate())
|
||||
.map(entry -> new Label(entry.getKey(), entry.getValue()))
|
||||
.collect(Collectors.toCollection(ArrayList::new));
|
||||
}
|
||||
@@ -70,6 +72,7 @@ public record Label(@NotNull String key, @NotNull String value) {
|
||||
if (map == null || map.isEmpty()) return List.of();
|
||||
return map.entrySet()
|
||||
.stream()
|
||||
.filter(getEntryNotEmptyPredicate())
|
||||
.map(entry -> new Label(entry.getKey(), entry.getValue()))
|
||||
.toList();
|
||||
}
|
||||
@@ -88,4 +91,14 @@ public record Label(@NotNull String key, @NotNull String value) {
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides predicate for not empty entries.
|
||||
*
|
||||
* @return The non-empty filter
|
||||
*/
|
||||
public static Predicate<Map.Entry<String, String>> getEntryNotEmptyPredicate() {
|
||||
return entry -> entry.getKey() != null && !entry.getKey().isEmpty() &&
|
||||
entry.getValue() != null && !entry.getValue().isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -865,20 +865,18 @@ public class Execution implements DeletedInterface, TenantInterface {
|
||||
* @param e the exception raise
|
||||
* @return new taskRun with updated attempt with logs
|
||||
*/
|
||||
private FailedTaskRunWithLog lastAttemptsTaskRunForFailedExecution(TaskRun taskRun,
|
||||
TaskRunAttempt lastAttempt, Exception e) {
|
||||
private FailedTaskRunWithLog lastAttemptsTaskRunForFailedExecution(TaskRun taskRun, TaskRunAttempt lastAttempt, Exception e) {
|
||||
TaskRun failed = taskRun
|
||||
.withAttempts(
|
||||
Stream
|
||||
.concat(
|
||||
taskRun.getAttempts().stream().limit(taskRun.getAttempts().size() - 1),
|
||||
Stream.of(lastAttempt.getState().isFailed() ? lastAttempt : lastAttempt.withState(State.Type.FAILED))
|
||||
)
|
||||
.toList()
|
||||
);
|
||||
return new FailedTaskRunWithLog(
|
||||
taskRun
|
||||
.withAttempts(
|
||||
Stream
|
||||
.concat(
|
||||
taskRun.getAttempts().stream().limit(taskRun.getAttempts().size() - 1),
|
||||
Stream.of(lastAttempt
|
||||
.withState(State.Type.FAILED))
|
||||
)
|
||||
.toList()
|
||||
)
|
||||
.withState(State.Type.FAILED),
|
||||
failed.getState().isFailed() ? failed : failed.withState(State.Type.FAILED),
|
||||
RunContextLogger.logEntries(loggingEventFromException(e), LogEntry.of(taskRun, kind))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ public abstract class AbstractFlow implements FlowInterface {
|
||||
@JsonSerialize(using = ListOrMapOfLabelSerializer.class)
|
||||
@JsonDeserialize(using = ListOrMapOfLabelDeserializer.class)
|
||||
@Schema(implementation = Object.class, oneOf = {List.class, Map.class})
|
||||
@Valid
|
||||
List<Label> labels;
|
||||
|
||||
@Schema(additionalProperties = Schema.AdditionalPropertiesValue.TRUE)
|
||||
|
||||
@@ -5,10 +5,7 @@ import io.kestra.core.exceptions.IllegalVariableEvaluationException;
|
||||
import io.kestra.core.exceptions.InternalException;
|
||||
import io.kestra.core.models.Label;
|
||||
import io.kestra.core.models.executions.*;
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.models.flows.FlowInterface;
|
||||
import io.kestra.core.models.flows.FlowWithException;
|
||||
import io.kestra.core.models.flows.State;
|
||||
import io.kestra.core.models.flows.*;
|
||||
import io.kestra.core.models.property.Property;
|
||||
import io.kestra.core.models.tasks.ExecutableTask;
|
||||
import io.kestra.core.models.tasks.Task;
|
||||
@@ -29,6 +26,7 @@ import org.apache.commons.lang3.stream.Streams;
|
||||
import java.time.Instant;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static io.kestra.core.trace.Tracer.throwCallable;
|
||||
import static io.kestra.core.utils.Rethrow.throwConsumer;
|
||||
@@ -153,17 +151,24 @@ public final class ExecutableUtils {
|
||||
currentFlow.getNamespace(),
|
||||
currentFlow.getId()
|
||||
)
|
||||
.orElseThrow(() -> new IllegalStateException("Unable to find flow '" + subflowNamespace + "'.'" + subflowId + "' with revision '" + subflowRevision.orElse(0) + "'"));
|
||||
.orElseThrow(() -> {
|
||||
String msg = "Unable to find flow '" + subflowNamespace + "'.'" + subflowId + "' with revision '" + subflowRevision.orElse(0) + "'";
|
||||
runContext.logger().error(msg);
|
||||
return new IllegalStateException(msg);
|
||||
});
|
||||
|
||||
if (flow.isDisabled()) {
|
||||
throw new IllegalStateException("Cannot execute a flow which is disabled");
|
||||
String msg = "Cannot execute a flow which is disabled";
|
||||
runContext.logger().error(msg);
|
||||
throw new IllegalStateException(msg);
|
||||
}
|
||||
|
||||
if (flow instanceof FlowWithException fwe) {
|
||||
throw new IllegalStateException("Cannot execute an invalid flow: " + fwe.getException());
|
||||
String msg = "Cannot execute an invalid flow: " + fwe.getException();
|
||||
runContext.logger().error(msg);
|
||||
throw new IllegalStateException(msg);
|
||||
}
|
||||
|
||||
List<Label> newLabels = inheritLabels ? new ArrayList<>(filterLabels(currentExecution.getLabels(), flow)) : new ArrayList<>(systemLabels(currentExecution));
|
||||
List<Label> newLabels = inheritLabels ? new ArrayList<>(filterLabels(currentExecution.getLabels(), flow)) : new ArrayList<>(systemLabels(currentExecution));
|
||||
if (labels != null) {
|
||||
labels.forEach(throwConsumer(label -> newLabels.add(new Label(runContext.render(label.key()), runContext.render(label.value())))));
|
||||
}
|
||||
@@ -201,7 +206,20 @@ public final class ExecutableUtils {
|
||||
.build()
|
||||
)
|
||||
.withScheduleDate(scheduleOnDate);
|
||||
|
||||
if(execution.getInputs().size()<inputs.size()) {
|
||||
Map<String,Object>resolvedInputs=execution.getInputs();
|
||||
for (var inputKey : inputs.keySet()) {
|
||||
if (!resolvedInputs.containsKey(inputKey)) {
|
||||
runContext.logger().warn(
|
||||
"Input {} was provided by parent execution {} for subflow {}.{} but isn't declared at the subflow inputs",
|
||||
inputKey,
|
||||
currentExecution.getId(),
|
||||
currentTask.subflowId().namespace(),
|
||||
currentTask.subflowId().flowId()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// inject the traceparent into the new execution
|
||||
propagator.ifPresent(pg -> pg.inject(Context.current(), execution, ExecutionTextMapSetter.INSTANCE));
|
||||
|
||||
|
||||
@@ -49,15 +49,7 @@ import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalTime;
|
||||
import java.util.AbstractMap;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.regex.Matcher;
|
||||
@@ -231,6 +223,19 @@ public class FlowInputOutput {
|
||||
return new AbstractMap.SimpleEntry<>(it.input().getId(), it.value());
|
||||
})
|
||||
.collect(HashMap::new, (m,v)-> m.put(v.getKey(), v.getValue()), HashMap::putAll);
|
||||
if (resolved.size() < data.size()) {
|
||||
RunContext runContext = runContextFactory.of(flow, execution);
|
||||
for (var inputKey : data.keySet()) {
|
||||
if (!resolved.containsKey(inputKey)) {
|
||||
runContext.logger().warn(
|
||||
"Input {} was provided for workflow {}.{} but isn't declared in the workflow inputs",
|
||||
inputKey,
|
||||
flow.getNamespace(),
|
||||
flow.getId()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return MapUtils.flattenToNestedMap(resolved);
|
||||
}
|
||||
|
||||
@@ -313,15 +318,15 @@ public class FlowInputOutput {
|
||||
});
|
||||
resolvable.setInput(input);
|
||||
|
||||
|
||||
|
||||
Object value = resolvable.get().value();
|
||||
|
||||
|
||||
// resolve default if needed
|
||||
if (value == null && input.getDefaults() != null) {
|
||||
value = resolveDefaultValue(input, runContext);
|
||||
resolvable.isDefault(true);
|
||||
}
|
||||
|
||||
|
||||
// validate and parse input value
|
||||
if (value == null) {
|
||||
if (input.getRequired()) {
|
||||
@@ -350,7 +355,7 @@ public class FlowInputOutput {
|
||||
|
||||
return resolvable.get();
|
||||
}
|
||||
|
||||
|
||||
public static Object resolveDefaultValue(Input<?> input, PropertyContext renderer) throws IllegalVariableEvaluationException {
|
||||
return switch (input.getType()) {
|
||||
case STRING, ENUM, SELECT, SECRET, EMAIL -> resolveDefaultPropertyAs(input, renderer, String.class);
|
||||
@@ -367,7 +372,7 @@ public class FlowInputOutput {
|
||||
case MULTISELECT -> resolveDefaultPropertyAsList(input, renderer, String.class);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T> Object resolveDefaultPropertyAs(Input<?> input, PropertyContext renderer, Class<T> clazz) throws IllegalVariableEvaluationException {
|
||||
return Property.as((Property<T>) input.getDefaults(), renderer, clazz);
|
||||
@@ -376,7 +381,7 @@ public class FlowInputOutput {
|
||||
private static <T> Object resolveDefaultPropertyAsList(Input<?> input, PropertyContext renderer, Class<T> clazz) throws IllegalVariableEvaluationException {
|
||||
return Property.asList((Property<List<T>>) input.getDefaults(), renderer, clazz);
|
||||
}
|
||||
|
||||
|
||||
private RunContext buildRunContextForExecutionAndInputs(final FlowInterface flow, final Execution execution, Map<String, InputAndValue> dependencies) {
|
||||
Map<String, Object> flattenInputs = MapUtils.flattenToNestedMap(dependencies.entrySet()
|
||||
.stream()
|
||||
@@ -453,7 +458,7 @@ public class FlowInputOutput {
|
||||
if (data.getType() == null) {
|
||||
return Optional.of(new AbstractMap.SimpleEntry<>(data.getId(), current));
|
||||
}
|
||||
|
||||
|
||||
final Type elementType = data instanceof ItemTypeInterface itemTypeInterface ? itemTypeInterface.getItemType() : null;
|
||||
|
||||
return Optional.of(new AbstractMap.SimpleEntry<>(
|
||||
@@ -530,17 +535,17 @@ public class FlowInputOutput {
|
||||
throw new Exception("Expected `" + type + "` but received `" + current + "` with errors:\n```\n" + e.getMessage() + "\n```");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static Map<String, Object> renderFlowOutputs(List<Output> outputs, RunContext runContext) throws IllegalVariableEvaluationException {
|
||||
if (outputs == null) return Map.of();
|
||||
|
||||
|
||||
// render required outputs
|
||||
Map<String, Object> outputsById = outputs
|
||||
.stream()
|
||||
.filter(output -> output.getRequired() == null || output.getRequired())
|
||||
.collect(HashMap::new, (map, entry) -> map.put(entry.getId(), entry.getValue()), Map::putAll);
|
||||
outputsById = runContext.render(outputsById);
|
||||
|
||||
|
||||
// render optional outputs one by one to catch, log, and skip any error.
|
||||
for (io.kestra.core.models.flows.Output output : outputs) {
|
||||
if (Boolean.FALSE.equals(output.getRequired())) {
|
||||
@@ -583,9 +588,9 @@ public class FlowInputOutput {
|
||||
}
|
||||
|
||||
public void isDefault(boolean isDefault) {
|
||||
this.input = new InputAndValue(this.input.input(), this.input.value(), this.input.enabled(), isDefault, this.input.exception());
|
||||
this.input = new InputAndValue(this.input.input(), this.input.value(), this.input.enabled(), isDefault, this.input.exception());
|
||||
}
|
||||
|
||||
|
||||
public void setInput(final Input<?> input) {
|
||||
this.input = new InputAndValue(input, this.input.value(), this.input.enabled(), this.input.isDefault(), this.input.exception());
|
||||
}
|
||||
|
||||
@@ -500,7 +500,7 @@ public class FlowableUtils {
|
||||
|
||||
ArrayList<ResolvedTask> result = new ArrayList<>();
|
||||
|
||||
int index = 0;
|
||||
int iteration = 0;
|
||||
for (Object current : distinctValue) {
|
||||
try {
|
||||
String resolvedValue = current instanceof String stringValue ? stringValue : MAPPER.writeValueAsString(current);
|
||||
@@ -508,7 +508,7 @@ public class FlowableUtils {
|
||||
result.add(ResolvedTask.builder()
|
||||
.task(task)
|
||||
.value(resolvedValue)
|
||||
.iteration(index++)
|
||||
.iteration(iteration)
|
||||
.parentId(parentTaskRun.getId())
|
||||
.build()
|
||||
);
|
||||
@@ -516,6 +516,7 @@ public class FlowableUtils {
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new IllegalVariableEvaluationException(e);
|
||||
}
|
||||
iteration++;
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -30,6 +30,6 @@ public class TimestampMicroFilter extends AbstractDate implements Filter {
|
||||
ZoneId zoneId = zoneId(timeZone);
|
||||
ZonedDateTime date = convert(input, zoneId, existingFormat);
|
||||
|
||||
return String.valueOf(TimeUnit.SECONDS.toNanos(date.toEpochSecond()) + TimeUnit.NANOSECONDS.toMicros(date.getNano()));
|
||||
return String.valueOf(TimeUnit.SECONDS.toMicros(date.toEpochSecond()) + TimeUnit.NANOSECONDS.toMicros(date.getNano()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,6 +155,7 @@ public class Labels extends Task implements ExecutionUpdatableTask {
|
||||
newLabels.putAll(labelsAsMap);
|
||||
|
||||
return execution.withLabels(newLabels.entrySet().stream()
|
||||
.filter(Label.getEntryNotEmptyPredicate())
|
||||
.map(entry -> new Label(
|
||||
entry.getKey(),
|
||||
entry.getValue()
|
||||
|
||||
@@ -1,19 +1,32 @@
|
||||
package io.kestra.core.models;
|
||||
|
||||
import io.kestra.core.junit.annotations.KestraTest;
|
||||
import io.kestra.core.models.validations.ModelValidator;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@KestraTest
|
||||
class LabelTest {
|
||||
@Inject
|
||||
private ModelValidator modelValidator;
|
||||
|
||||
@Test
|
||||
void shouldGetNestedMapGivenDistinctLabels() {
|
||||
Map<String, Object> result = Label.toNestedMap(List.of(
|
||||
new Label(Label.USERNAME, "test"),
|
||||
new Label(Label.CORRELATION_ID, "id"))
|
||||
new Label(Label.CORRELATION_ID, "id"),
|
||||
new Label("", "bar"),
|
||||
new Label(null, "bar"),
|
||||
new Label("foo", ""),
|
||||
new Label("baz", null)
|
||||
)
|
||||
);
|
||||
|
||||
assertThat(result).isEqualTo(
|
||||
@@ -34,6 +47,18 @@ class LabelTest {
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void toNestedMapShouldIgnoreEmptyOrNull() {
|
||||
Map<String, Object> result = Label.toNestedMap(List.of(
|
||||
new Label("", "bar"),
|
||||
new Label(null, "bar"),
|
||||
new Label("foo", ""),
|
||||
new Label("baz", null))
|
||||
);
|
||||
|
||||
assertThat(result).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldGetMapGivenDistinctLabels() {
|
||||
Map<String, String> result = Label.toMap(List.of(
|
||||
@@ -59,6 +84,18 @@ class LabelTest {
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void toMapShouldIgnoreEmptyOrNull() {
|
||||
Map<String, String> result = Label.toMap(List.of(
|
||||
new Label("", "bar"),
|
||||
new Label(null, "bar"),
|
||||
new Label("foo", ""),
|
||||
new Label("baz", null))
|
||||
);
|
||||
|
||||
assertThat(result).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDuplicateLabelsWithKeyOrderKept() {
|
||||
List<Label> result = Label.deduplicate(List.of(
|
||||
@@ -73,4 +110,28 @@ class LabelTest {
|
||||
new Label(Label.CORRELATION_ID, "id")
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void deduplicateShouldIgnoreEmptyAndNull() {
|
||||
List<Label> result = Label.deduplicate(List.of(
|
||||
new Label("", "bar"),
|
||||
new Label(null, "bar"),
|
||||
new Label("foo", ""),
|
||||
new Label("baz", null))
|
||||
);
|
||||
|
||||
assertThat(result).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldValidateEmpty() {
|
||||
Optional<ConstraintViolationException> validLabelResult = modelValidator.isValid(new Label("foo", "bar"));
|
||||
assertThat(validLabelResult.isPresent()).isFalse();
|
||||
|
||||
Optional<ConstraintViolationException> emptyValueLabelResult = modelValidator.isValid(new Label("foo", ""));
|
||||
assertThat(emptyValueLabelResult.isPresent()).isTrue();
|
||||
|
||||
Optional<ConstraintViolationException> emptyKeyLabelResult = modelValidator.isValid(new Label("", "bar"));
|
||||
assertThat(emptyKeyLabelResult.isPresent()).isTrue();
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,7 @@ public abstract class AbstractRunnerTest {
|
||||
private PluginDefaultsCaseTest pluginDefaultsCaseTest;
|
||||
|
||||
@Inject
|
||||
private FlowCaseTest flowCaseTest;
|
||||
protected FlowCaseTest flowCaseTest;
|
||||
|
||||
@Inject
|
||||
private WorkingDirectoryTest.Suite workingDirectoryTest;
|
||||
@@ -173,7 +173,7 @@ public abstract class AbstractRunnerTest {
|
||||
|
||||
@Test
|
||||
@LoadFlows({"flows/valids/restart_local_errors.yaml"})
|
||||
void restartFailedThenFailureWithLocalErrors() throws Exception {
|
||||
protected void restartFailedThenFailureWithLocalErrors() throws Exception {
|
||||
restartCaseTest.restartFailedThenFailureWithLocalErrors();
|
||||
}
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ public class FlowTriggerCaseTest {
|
||||
|
||||
public void triggerWithConcurrencyLimit(String tenantId) throws QueueException, TimeoutException {
|
||||
Execution execution1 = runnerUtils.runOneUntilRunning(tenantId, "io.kestra.tests.trigger.concurrency", "trigger-flow-with-concurrency-limit");
|
||||
Execution execution2 = runnerUtils.runOneUntilRunning(tenantId, "io.kestra.tests.trigger.concurrency", "trigger-flow-with-concurrency-limit");
|
||||
Execution execution2 = runnerUtils.runOne(tenantId, "io.kestra.tests.trigger.concurrency", "trigger-flow-with-concurrency-limit");
|
||||
|
||||
List<Execution> triggeredExec = runnerUtils.awaitFlowExecutionNumber(
|
||||
5,
|
||||
|
||||
@@ -6,6 +6,8 @@ import io.kestra.core.models.executions.Execution;
|
||||
import io.kestra.core.models.flows.State;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@KestraTest(startRunner = true)
|
||||
@@ -31,4 +33,15 @@ class TaskWithRunIfTest {
|
||||
assertThat(execution.findTaskRunsByTaskId("log_test").getFirst().getState().getCurrent()).isEqualTo(State.Type.SKIPPED);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ExecuteFlow("flows/valids/task-runif-executionupdating.yml")
|
||||
void executionUpdatingTask(Execution execution) {
|
||||
assertThat(execution.getState().getCurrent()).isEqualTo(State.Type.SUCCESS);
|
||||
assertThat(execution.getTaskRunList()).hasSize(5);
|
||||
assertThat(execution.findTaskRunsByTaskId("skipSetVariables").getFirst().getState().getCurrent()).isEqualTo(State.Type.SKIPPED);
|
||||
assertThat(execution.findTaskRunsByTaskId("skipUnsetVariables").getFirst().getState().getCurrent()).isEqualTo(State.Type.SKIPPED);
|
||||
assertThat(execution.findTaskRunsByTaskId("unsetVariables").getFirst().getState().getCurrent()).isEqualTo(State.Type.SUCCESS);
|
||||
assertThat(execution.findTaskRunsByTaskId("setVariables").getFirst().getState().getCurrent()).isEqualTo(State.Type.SUCCESS);
|
||||
assertThat(execution.getVariables()).containsEntry("list", List.of(42));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@ class DateFilterTest {
|
||||
)
|
||||
);
|
||||
|
||||
assertThat(render).isEqualTo("1378653552000123456");
|
||||
assertThat(render).isEqualTo("1378653552123456");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -2,12 +2,16 @@ package io.kestra.plugin.core.flow;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import io.kestra.core.exceptions.InternalException;
|
||||
import io.kestra.core.junit.annotations.ExecuteFlow;
|
||||
import io.kestra.core.junit.annotations.KestraTest;
|
||||
import io.kestra.core.models.executions.Execution;
|
||||
import io.kestra.core.models.executions.TaskRun;
|
||||
import io.kestra.core.models.flows.State;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@KestraTest(startRunner = true)
|
||||
class ForEachTest {
|
||||
|
||||
@@ -60,4 +64,13 @@ class ForEachTest {
|
||||
void nested(Execution execution) {
|
||||
assertThat(execution.getState().getCurrent()).isEqualTo(State.Type.SUCCESS);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ExecuteFlow("flows/valids/foreach-iteration.yaml")
|
||||
void iteration(Execution execution) throws InternalException {
|
||||
List<TaskRun> seconds = execution.findTaskRunsByTaskId("second");
|
||||
assertThat(seconds).hasSize(2);
|
||||
assertThat(seconds.get(0).getIteration()).isEqualTo(0);
|
||||
assertThat(seconds.get(1).getIteration()).isEqualTo(1);
|
||||
}
|
||||
}
|
||||
@@ -160,4 +160,25 @@ class RuntimeLabelsTest {
|
||||
new Label("fromListKey", "value2")
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@LoadFlows({"flows/valids/labels-update-task-empty.yml"})
|
||||
void updateIgnoresEmpty() throws TimeoutException, QueueException {
|
||||
Execution execution = runnerUtils.runOne(
|
||||
MAIN_TENANT,
|
||||
"io.kestra.tests",
|
||||
"labels-update-task-empty",
|
||||
null,
|
||||
(flow, createdExecution) -> Map.of(),
|
||||
null,
|
||||
List.of()
|
||||
);
|
||||
|
||||
assertThat(execution.getTaskRunList()).hasSize(1);
|
||||
assertThat(execution.getState().getCurrent()).isEqualTo(State.Type.FAILED);
|
||||
|
||||
assertThat(execution.getLabels()).containsExactly(
|
||||
new Label(Label.CORRELATION_ID, execution.getId())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,12 +168,12 @@ class FlowTest {
|
||||
Optional<Execution> evaluate = flowTrigger.evaluate(multipleConditionStorage, runContextFactory.of(), flow, execution);
|
||||
|
||||
assertThat(evaluate.isPresent()).isTrue();
|
||||
assertThat(evaluate.get().getLabels()).hasSize(6);
|
||||
assertThat(evaluate.get().getLabels()).hasSize(5);
|
||||
assertThat(evaluate.get().getLabels()).contains(new Label("flow-label-1", "flow-label-1"));
|
||||
assertThat(evaluate.get().getLabels()).contains(new Label("flow-label-2", "flow-label-2"));
|
||||
assertThat(evaluate.get().getLabels()).contains(new Label("trigger-label-1", "trigger-label-1"));
|
||||
assertThat(evaluate.get().getLabels()).contains(new Label("trigger-label-2", "trigger-label-2"));
|
||||
assertThat(evaluate.get().getLabels()).contains(new Label("trigger-label-3", ""));
|
||||
assertThat(evaluate.get().getLabels()).doesNotContain(new Label("trigger-label-3", ""));
|
||||
assertThat(evaluate.get().getLabels()).contains(new Label(Label.CORRELATION_ID, "correlationId"));
|
||||
assertThat(evaluate.get().getTrigger()).extracting(ExecutionTrigger::getVariables).hasFieldOrProperty("executionLabels");
|
||||
assertThat(evaluate.get().getTrigger().getVariables().get("executionLabels")).isEqualTo(Map.of("execution-label", "execution"));
|
||||
|
||||
@@ -169,7 +169,7 @@ class ScheduleTest {
|
||||
assertThat(evaluate.isPresent()).isTrue();
|
||||
assertThat(evaluate.get().getLabels()).contains(new Label("trigger-label-1", "trigger-label-1"));
|
||||
assertThat(evaluate.get().getLabels()).contains(new Label("trigger-label-2", "trigger-label-2"));
|
||||
assertThat(evaluate.get().getLabels()).contains(new Label("trigger-label-3", ""));
|
||||
assertThat(evaluate.get().getLabels()).doesNotContain(new Label("trigger-label-3", ""));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
16
core/src/test/resources/flows/valids/foreach-iteration.yaml
Normal file
16
core/src/test/resources/flows/valids/foreach-iteration.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
id: foreach-iteration
|
||||
namespace: io.kestra.tests
|
||||
|
||||
tasks:
|
||||
- id: foreach
|
||||
type: io.kestra.plugin.core.flow.ForEach
|
||||
values: [1, 2]
|
||||
tasks:
|
||||
- id: first
|
||||
type: io.kestra.plugin.core.output.OutputValues
|
||||
values:
|
||||
iteration : "{{ taskrun.iteration }}"
|
||||
- id: second
|
||||
type: io.kestra.plugin.core.output.OutputValues
|
||||
values:
|
||||
iteration : "{{ taskrun.iteration }}"
|
||||
@@ -0,0 +1,14 @@
|
||||
id: labels-update-task-empty
|
||||
namespace: io.kestra.tests
|
||||
|
||||
tasks:
|
||||
- id: from-string
|
||||
type: io.kestra.plugin.core.execution.Labels
|
||||
labels: "{ \"fromStringKey\": \"\", \"\": \"value2\" }"
|
||||
- id: from-list
|
||||
type: io.kestra.plugin.core.execution.Labels
|
||||
labels:
|
||||
- key: "fromListKey"
|
||||
value: ""
|
||||
- key: ""
|
||||
value: "value2"
|
||||
@@ -0,0 +1,35 @@
|
||||
id: task-runif-executionupdating
|
||||
namespace: io.kestra.tests
|
||||
|
||||
variables:
|
||||
list: []
|
||||
|
||||
tasks:
|
||||
- id: output
|
||||
type: io.kestra.plugin.core.output.OutputValues
|
||||
values:
|
||||
taskrun_data: 1
|
||||
|
||||
- id: unsetVariables
|
||||
type: io.kestra.plugin.core.execution.UnsetVariables
|
||||
runIf: "true"
|
||||
variables:
|
||||
- list
|
||||
|
||||
- id: setVariables
|
||||
type: io.kestra.plugin.core.execution.SetVariables
|
||||
runIf: "{{ outputs.output['values']['taskrun_data'] == 1 }}"
|
||||
variables:
|
||||
list: [42]
|
||||
|
||||
- id: skipSetVariables
|
||||
type: io.kestra.plugin.core.execution.SetVariables
|
||||
runIf: "false"
|
||||
variables:
|
||||
list: [1]
|
||||
|
||||
- id: skipUnsetVariables
|
||||
type: io.kestra.plugin.core.execution.UnsetVariables
|
||||
runIf: "{{ outputs.output['values']['taskrun_data'] == 2 }}"
|
||||
variables:
|
||||
- list
|
||||
@@ -9,6 +9,9 @@ tasks:
|
||||
- id: sleep
|
||||
type: io.kestra.plugin.core.flow.Sleep
|
||||
duration: PT0.5S
|
||||
- id: sleep_1
|
||||
- id: log
|
||||
type: io.kestra.plugin.core.log.Log
|
||||
message: "we are between sleeps"
|
||||
- id: sleep_2
|
||||
type: io.kestra.plugin.core.flow.Sleep
|
||||
duration: PT0.5S
|
||||
@@ -1072,6 +1072,17 @@ public class ExecutorService {
|
||||
var executionUpdatingTask = (ExecutionUpdatableTask) workerTask.getTask();
|
||||
|
||||
try {
|
||||
// handle runIf
|
||||
if (!TruthUtils.isTruthy(workerTask.getRunContext().render(workerTask.getTask().getRunIf()))) {
|
||||
executor.withExecution(
|
||||
executor
|
||||
.getExecution()
|
||||
.withTaskRun(workerTask.getTaskRun().withState(State.Type.SKIPPED)),
|
||||
"handleExecutionUpdatingTaskSkipped"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
executor.withExecution(
|
||||
executionUpdatingTask.update(executor.getExecution(), workerTask.getRunContext())
|
||||
.withTaskRun(workerTask.getTaskRun().withState(State.Type.RUNNING)),
|
||||
|
||||
@@ -1,7 +1,33 @@
|
||||
package io.kestra.runner.mysql;
|
||||
|
||||
import io.kestra.core.junit.annotations.LoadFlows;
|
||||
import io.kestra.jdbc.runner.JdbcRunnerTest;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class MysqlRunnerTest extends JdbcRunnerTest {
|
||||
|
||||
@Disabled("We have a bug here in the queue where no FAILED event is sent, so the state store is not cleaned")
|
||||
@Test
|
||||
@Override
|
||||
@LoadFlows({"flows/valids/restart-with-finally.yaml"})
|
||||
protected void restartFailedWithFinally() throws Exception {
|
||||
restartCaseTest.restartFailedWithFinally();
|
||||
}
|
||||
|
||||
@Disabled("Should fail the second time, but is success")
|
||||
@Test
|
||||
@Override
|
||||
@LoadFlows({"flows/valids/restart_local_errors.yaml"})
|
||||
protected void restartFailedThenFailureWithLocalErrors() throws Exception {
|
||||
restartCaseTest.restartFailedThenFailureWithLocalErrors();
|
||||
}
|
||||
|
||||
@Disabled("Is success, but is not terminated")
|
||||
@Test
|
||||
@Override
|
||||
@LoadFlows({"flows/valids/restart-with-after-execution.yaml"})
|
||||
protected void restartFailedWithAfterExecution() throws Exception {
|
||||
restartCaseTest.restartFailedWithAfterExecution();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -682,9 +682,8 @@ public class JdbcExecutor implements ExecutorInterface {
|
||||
);
|
||||
} catch (QueueException e) {
|
||||
try {
|
||||
this.executionQueue.emit(
|
||||
message.failedExecutionFromExecutor(e).getExecution().withState(State.Type.FAILED)
|
||||
);
|
||||
Execution failedExecution = fail(message, e);
|
||||
this.executionQueue.emit(failedExecution);
|
||||
} catch (QueueException ex) {
|
||||
log.error("Unable to emit the execution {}", message.getId(), ex);
|
||||
}
|
||||
@@ -701,6 +700,16 @@ public class JdbcExecutor implements ExecutorInterface {
|
||||
}
|
||||
}
|
||||
|
||||
private Execution fail(Execution message, Exception e) {
|
||||
var failedExecution = message.failedExecutionFromExecutor(e);
|
||||
try {
|
||||
logQueue.emitAsync(failedExecution.getLogs());
|
||||
} catch (QueueException ex) {
|
||||
// fail silently
|
||||
}
|
||||
return failedExecution.getExecution().getState().isFailed() ? failedExecution.getExecution() : failedExecution.getExecution().withState(State.Type.FAILED);
|
||||
}
|
||||
|
||||
private void workerTaskResultQueue(Either<WorkerTaskResult, DeserializationException> either) {
|
||||
if (either.isRight()) {
|
||||
log.error("Unable to deserialize a worker task result: {}", either.getRight().getMessage(), either.getRight());
|
||||
@@ -1178,8 +1187,9 @@ public class JdbcExecutor implements ExecutorInterface {
|
||||
// If we cannot add the new worker task result to the execution, we fail it
|
||||
executionRepository.lock(executor.getExecution().getId(), pair -> {
|
||||
Execution execution = pair.getLeft();
|
||||
Execution failedExecution = fail(execution, e);
|
||||
try {
|
||||
this.executionQueue.emit(execution.failedExecutionFromExecutor(e).getExecution().withState(State.Type.FAILED));
|
||||
this.executionQueue.emit(failedExecution);
|
||||
} catch (QueueException ex) {
|
||||
log.error("Unable to emit the execution {}", execution.getId(), ex);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ dependencies {
|
||||
// versions for libraries with multiple module but no BOM
|
||||
def slf4jVersion = "2.0.17"
|
||||
def protobufVersion = "3.25.5" // Orc still uses 3.25.5 see https://github.com/apache/orc/blob/main/java/pom.xml
|
||||
def bouncycastleVersion = "1.81"
|
||||
def bouncycastleVersion = "1.82"
|
||||
def mavenResolverVersion = "2.0.10"
|
||||
def jollydayVersion = "1.5.6"
|
||||
def jsonschemaVersion = "4.38.0"
|
||||
@@ -35,7 +35,7 @@ dependencies {
|
||||
// we define cloud bom here for GCP, Azure and AWS so they are aligned for all plugins that use them (secret, storage, oss and ee plugins)
|
||||
api platform('com.google.cloud:libraries-bom:26.68.0')
|
||||
api platform("com.azure:azure-sdk-bom:1.2.38")
|
||||
api platform('software.amazon.awssdk:bom:2.33.11')
|
||||
api platform('software.amazon.awssdk:bom:2.34.2')
|
||||
api platform("dev.langchain4j:langchain4j-bom:$langchain4jVersion")
|
||||
api platform("dev.langchain4j:langchain4j-community-bom:$langchain4jCommunityVersion")
|
||||
|
||||
@@ -65,8 +65,8 @@ dependencies {
|
||||
|
||||
// http5 client
|
||||
api("org.apache.httpcomponents.client5:httpclient5:5.5")
|
||||
api("org.apache.httpcomponents.core5:httpcore5:5.3.5")
|
||||
api("org.apache.httpcomponents.core5:httpcore5-h2:5.3.5")
|
||||
api("org.apache.httpcomponents.core5:httpcore5:5.3.6")
|
||||
api("org.apache.httpcomponents.core5:httpcore5-h2:5.3.6")
|
||||
|
||||
api("com.fasterxml.uuid:java-uuid-generator:$jugVersion")
|
||||
// issue with the Docker lib having a too old version for the k8s extension
|
||||
@@ -75,17 +75,17 @@ dependencies {
|
||||
api "org.apache.kafka:kafka-clients:$kafkaVersion"
|
||||
api "org.apache.kafka:kafka-streams:$kafkaVersion"
|
||||
// AWS CRT is not included in the AWS BOM but needed for the S3 Transfer manager
|
||||
api 'software.amazon.awssdk.crt:aws-crt:0.38.13'
|
||||
api 'software.amazon.awssdk.crt:aws-crt:0.39.0'
|
||||
|
||||
// we need at least 0.14, it could be removed when Micronaut contains a recent only version in their BOM
|
||||
api "io.micrometer:micrometer-core:1.15.4"
|
||||
// We need at least 6.17, it could be removed when Micronaut contains a recent only version in their BOM
|
||||
api "io.micronaut.openapi:micronaut-openapi-bom:6.18.0"
|
||||
api "io.micronaut.openapi:micronaut-openapi-bom:6.18.1"
|
||||
|
||||
// Other libs
|
||||
api("org.projectlombok:lombok:1.18.40")
|
||||
api("org.projectlombok:lombok:1.18.42")
|
||||
api("org.codehaus.janino:janino:3.1.12")
|
||||
api group: 'org.apache.logging.log4j', name: 'log4j-to-slf4j', version: '2.25.1'
|
||||
api group: 'org.apache.logging.log4j', name: 'log4j-to-slf4j', version: '2.25.2'
|
||||
api group: 'org.slf4j', name: 'jul-to-slf4j', version: slf4jVersion
|
||||
api group: 'org.slf4j', name: 'jcl-over-slf4j', version: slf4jVersion
|
||||
api group: 'org.fusesource.jansi', name: 'jansi', version: '2.4.2'
|
||||
@@ -101,7 +101,7 @@ dependencies {
|
||||
api group: 'org.apache.maven.resolver', name: 'maven-resolver-connector-basic', version: mavenResolverVersion
|
||||
api group: 'org.apache.maven.resolver', name: 'maven-resolver-transport-file', version: mavenResolverVersion
|
||||
api group: 'org.apache.maven.resolver', name: 'maven-resolver-transport-apache', version: mavenResolverVersion
|
||||
api 'com.github.oshi:oshi-core:6.8.3'
|
||||
api 'com.github.oshi:oshi-core:6.9.0'
|
||||
api 'io.pebbletemplates:pebble:3.2.4'
|
||||
api group: 'co.elastic.logging', name: 'logback-ecs-encoder', version: '1.7.0'
|
||||
api group: 'de.focus-shift', name: 'jollyday-core', version: jollydayVersion
|
||||
@@ -124,9 +124,9 @@ dependencies {
|
||||
api 'org.jsoup:jsoup:1.21.2'
|
||||
api "org.xhtmlrenderer:flying-saucer-core:$flyingSaucerVersion"
|
||||
api "org.xhtmlrenderer:flying-saucer-pdf:$flyingSaucerVersion"
|
||||
api group: 'jakarta.mail', name: 'jakarta.mail-api', version: '2.1.4'
|
||||
api group: 'jakarta.mail', name: 'jakarta.mail-api', version: '2.1.5'
|
||||
api group: 'jakarta.annotation', name: 'jakarta.annotation-api', version: '3.0.0'
|
||||
api group: 'org.eclipse.angus', name: 'jakarta.mail', version: '2.0.4'
|
||||
api group: 'org.eclipse.angus', name: 'jakarta.mail', version: '2.0.5'
|
||||
api group: 'com.github.ben-manes.caffeine', name: 'caffeine', version: '3.2.2'
|
||||
api group: 'de.siegmar', name: 'fastcsv', version: '4.0.0'
|
||||
// Json Diff
|
||||
|
||||
@@ -14,6 +14,7 @@ import io.kestra.core.models.conditions.ConditionContext;
|
||||
import io.kestra.core.models.executions.Execution;
|
||||
import io.kestra.core.models.executions.ExecutionKilled;
|
||||
import io.kestra.core.models.executions.ExecutionKilledTrigger;
|
||||
import io.kestra.core.models.executions.LogEntry;
|
||||
import io.kestra.core.models.flows.FlowId;
|
||||
import io.kestra.core.models.flows.FlowInterface;
|
||||
import io.kestra.core.models.flows.FlowWithException;
|
||||
@@ -37,6 +38,7 @@ import io.micronaut.inject.qualifiers.Qualifiers;
|
||||
import jakarta.annotation.Nullable;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Named;
|
||||
import jakarta.inject.Singleton;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
@@ -70,6 +72,7 @@ public abstract class AbstractScheduler implements Scheduler {
|
||||
private final QueueInterface<WorkerJob> workerJobQueue;
|
||||
private final QueueInterface<WorkerTriggerResult> workerTriggerResultQueue;
|
||||
private final QueueInterface<ExecutionKilled> executionKilledQueue;
|
||||
private final QueueInterface<LogEntry> logQueue;
|
||||
@SuppressWarnings("rawtypes")
|
||||
private final Optional<QueueInterface> clusterEventQueue;
|
||||
protected final FlowListenersInterface flowListeners;
|
||||
@@ -124,6 +127,7 @@ public abstract class AbstractScheduler implements Scheduler {
|
||||
this.executionKilledQueue = applicationContext.getBean(QueueInterface.class, Qualifiers.byName(QueueFactoryInterface.KILL_NAMED));
|
||||
this.workerTriggerResultQueue = applicationContext.getBean(QueueInterface.class, Qualifiers.byName(QueueFactoryInterface.WORKERTRIGGERRESULT_NAMED));
|
||||
this.clusterEventQueue = applicationContext.findBean(QueueInterface.class, Qualifiers.byName(QueueFactoryInterface.CLUSTER_EVENT_NAMED));
|
||||
this.logQueue = applicationContext.getBean(QueueInterface.class, Qualifiers.byName(QueueFactoryInterface.WORKERTASKLOG_NAMED));
|
||||
this.flowListeners = flowListeners;
|
||||
this.runContextFactory = applicationContext.getBean(RunContextFactory.class);
|
||||
this.runContextInitializer = applicationContext.getBean(RunContextInitializer.class);
|
||||
@@ -767,7 +771,7 @@ public abstract class AbstractScheduler implements Scheduler {
|
||||
this.executionEventPublisher.publishEvent(new CrudEvent<>(newExecution, CrudEventType.CREATE));
|
||||
} catch (QueueException e) {
|
||||
try {
|
||||
Execution failedExecution = newExecution.failedExecutionFromExecutor(e).getExecution().withState(State.Type.FAILED);
|
||||
Execution failedExecution = fail(newExecution, e);
|
||||
this.executionQueue.emit(failedExecution);
|
||||
this.executionEventPublisher.publishEvent(new CrudEvent<>(failedExecution, CrudEventType.CREATE));
|
||||
} catch (QueueException ex) {
|
||||
@@ -776,6 +780,16 @@ public abstract class AbstractScheduler implements Scheduler {
|
||||
}
|
||||
}
|
||||
|
||||
private Execution fail(Execution message, Exception e) {
|
||||
var failedExecution = message.failedExecutionFromExecutor(e);
|
||||
try {
|
||||
logQueue.emitAsync(failedExecution.getLogs());
|
||||
} catch (QueueException ex) {
|
||||
// fail silently
|
||||
}
|
||||
return failedExecution.getExecution().getState().isFailed() ? failedExecution.getExecution() : failedExecution.getExecution().withState(State.Type.FAILED);
|
||||
}
|
||||
|
||||
private void executionMonitor() {
|
||||
try {
|
||||
// Retrieve triggers with non-null execution_id from all corresponding virtual nodes
|
||||
|
||||
@@ -12,5 +12,5 @@ dependencies {
|
||||
api 'org.hamcrest:hamcrest:3.0'
|
||||
api 'org.hamcrest:hamcrest-library:3.0'
|
||||
api 'org.mockito:mockito-junit-jupiter'
|
||||
api 'org.assertj:assertj-core:3.27.4'
|
||||
api 'org.assertj:assertj-core:3.27.6'
|
||||
}
|
||||
@@ -504,7 +504,7 @@
|
||||
import YAML_CHART from "../dashboard/assets/executions_timeseries_chart.yaml?raw";
|
||||
import Utils from "../../utils/utils";
|
||||
|
||||
import {filterLabels} from "./utils"
|
||||
import {filterValidLabels} from "./utils"
|
||||
import {useExecutionsStore} from "../../stores/executions";
|
||||
import {useAuthStore} from "override/stores/auth";
|
||||
import {useFlowStore} from "../../stores/flow";
|
||||
@@ -1055,9 +1055,9 @@
|
||||
);
|
||||
},
|
||||
setLabels() {
|
||||
const filtered = filterLabels(this.executionLabels)
|
||||
const filtered = filterValidLabels(this.executionLabels)
|
||||
|
||||
if(filtered.error) {
|
||||
if (filtered.error) {
|
||||
this.$toast().error(this.$t("wrong labels"))
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
import LabelInput from "../../components/labels/LabelInput.vue";
|
||||
import {State} from "@kestra-io/ui-libs"
|
||||
|
||||
import {filterLabels} from "./utils"
|
||||
import {filterValidLabels} from "./utils"
|
||||
import permission from "../../models/permission";
|
||||
import action from "../../models/action";
|
||||
import {useAuthStore} from "override/stores/auth"
|
||||
@@ -78,10 +78,11 @@
|
||||
},
|
||||
methods: {
|
||||
setLabels() {
|
||||
let filtered = filterLabels(this.executionLabels)
|
||||
const filtered = filterValidLabels(this.executionLabels)
|
||||
|
||||
if(filtered.error) {
|
||||
filtered.labels = filtered.labels.filter(obj => !(obj.key === null && obj.value === null));
|
||||
if (filtered.error) {
|
||||
this.$toast().error(this.$t("wrong labels"))
|
||||
return;
|
||||
}
|
||||
|
||||
this.isOpen = false;
|
||||
|
||||
@@ -8,7 +8,7 @@ interface FilterResult {
|
||||
error?: boolean;
|
||||
}
|
||||
|
||||
export const filterLabels = (labels: Label[]): FilterResult => {
|
||||
const invalid = labels.some(label => label.key === null || label.value === null || label.key === "" || label.value === "");
|
||||
return invalid ? {labels, error: true} : {labels};
|
||||
export const filterValidLabels = (labels: Label[]): FilterResult => {
|
||||
const validLabels = labels.filter(label => label.key !== null && label.value !== null && label.key !== "" && label.value !== "");
|
||||
return validLabels.length === labels.length ? {labels} : {labels: validLabels, error: true};
|
||||
};
|
||||
|
||||
@@ -351,13 +351,15 @@
|
||||
const dataTableRef = useTemplateRef<typeof DataTable>("dataTable");
|
||||
|
||||
const {queryWithFilter, onPageChanged, onRowDoubleClick, onSort} = useDataTableActions({dblClickRouteName: "flows/update"});
|
||||
function selectionMapper(element: {id: string; namespace: string; disabled: boolean}): {id: string; namespace: string; enabled: boolean} {
|
||||
|
||||
function selectionMapper({id, namespace, disabled}: {id: string; namespace: string; disabled: boolean}) {
|
||||
return {
|
||||
id: element.id,
|
||||
namespace: element.namespace,
|
||||
enabled: !element.disabled,
|
||||
id,
|
||||
namespace,
|
||||
enabled: !disabled,
|
||||
};
|
||||
}
|
||||
|
||||
const {selection, queryBulkAction, handleSelectionChange, toggleAllUnselected, toggleAllSelection} = useSelectTableActions({
|
||||
dataTableRef,
|
||||
selectionMapper
|
||||
|
||||
@@ -242,7 +242,7 @@
|
||||
return this.kvs?.filter(kv =>
|
||||
!this.searchQuery ||
|
||||
kv.key.toLowerCase().includes(this.searchQuery.toLowerCase()) ||
|
||||
kv.description.toLowerCase().includes(this.searchQuery.toLowerCase())
|
||||
kv.description?.toLowerCase().includes(this.searchQuery.toLowerCase())
|
||||
);
|
||||
},
|
||||
kvModalTitle() {
|
||||
|
||||
@@ -11,10 +11,9 @@
|
||||
hideToggle
|
||||
>
|
||||
<template #header>
|
||||
<el-button @click="collapsed = onToggleCollapse(!collapsed)" class="collapseButton" :size="collapsed ? 'small':undefined">
|
||||
<ChevronRight v-if="collapsed" />
|
||||
<ChevronLeft v-else />
|
||||
</el-button>
|
||||
<SidebarToggleButton
|
||||
@toggle="collapsed = onToggleCollapse(!collapsed)"
|
||||
/>
|
||||
<div class="logo">
|
||||
<component :is="props.showLink ? 'router-link' : 'div'" :to="{name: 'home'}">
|
||||
<span class="img" />
|
||||
@@ -41,14 +40,14 @@
|
||||
|
||||
import {SidebarMenu} from "vue-sidebar-menu";
|
||||
|
||||
import ChevronLeft from "vue-material-design-icons/ChevronLeft.vue";
|
||||
import ChevronRight from "vue-material-design-icons/ChevronRight.vue";
|
||||
import StarOutline from "vue-material-design-icons/StarOutline.vue";
|
||||
|
||||
import Environment from "./Environment.vue";
|
||||
import BookmarkLinkList from "./BookmarkLinkList.vue";
|
||||
import {useBookmarksStore} from "../../stores/bookmarks";
|
||||
import type {MenuItem} from "override/components/useLeftMenu";
|
||||
import {useLayoutStore} from "../../stores/layout";
|
||||
import SidebarToggleButton from "./SidebarToggleButton.vue";
|
||||
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
@@ -63,9 +62,11 @@
|
||||
const $route = useRoute()
|
||||
const {t} = useI18n({useScope: "global"});
|
||||
|
||||
const layoutStore = useLayoutStore();
|
||||
|
||||
function onToggleCollapse(folded) {
|
||||
collapsed.value = folded;
|
||||
localStorage.setItem("menuCollapsed", folded ? "true" : "false");
|
||||
layoutStore.setSideMenuCollapsed(folded);
|
||||
$emit("menu-collapse", folded);
|
||||
|
||||
return folded;
|
||||
|
||||
43
ui/src/components/layout/SidebarToggleButton.vue
Normal file
43
ui/src/components/layout/SidebarToggleButton.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<el-button
|
||||
class="collapseButton sidebar-toggle"
|
||||
@click="$emit('toggle')"
|
||||
>
|
||||
<svg
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M11.8554 10.9932C11.8567 11.4542 11.4841 11.8289 11.0231 11.8301L1.02524 11.858C0.564312 11.8593 0.189613 11.4867 0.188327 11.0258L0.160404 1.01728C0.159118 0.556349 0.531732 0.181649 0.99266 0.180363L10.9906 0.152469C11.4515 0.151183 11.8262 0.523797 11.8275 0.984726L11.8554 10.9932ZM11.0318 11.0054L5.18316 11.0217L5.15511 0.967535L11.0037 0.951218L11.0318 11.0054ZM4.31027 11.023L0.975876 11.0323L0.947825 0.978203L4.28221 0.9689L4.31027 11.023Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineEmits<{
|
||||
(e: "toggle"): void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sidebar-toggle {
|
||||
border: none;
|
||||
color: var(--ks-text-secondary);
|
||||
|
||||
&:hover {
|
||||
color: var(--ks-content-link);
|
||||
}
|
||||
|
||||
html.dark & {
|
||||
color: var(--ks-text-secondary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,32 +1,40 @@
|
||||
<template>
|
||||
<nav data-component="FILENAME_PLACEHOLDER" class="d-flex w-100 gap-3 top-bar">
|
||||
<div class="d-flex flex-column flex-grow-1 flex-shrink-1 overflow-hidden top-title">
|
||||
<el-breadcrumb v-if="breadcrumb">
|
||||
<el-breadcrumb-item v-for="(item, x) in breadcrumb" :key="x" :class="{'pe-none': item.disabled}">
|
||||
<a v-if="item.disabled || !item.link">
|
||||
{{ item.label }}
|
||||
</a>
|
||||
<router-link v-else :to="item.link">
|
||||
{{ item.label }}
|
||||
</router-link>
|
||||
</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
<h1 class="h5 fw-semibold m-0 d-inline-flex">
|
||||
<slot name="title">
|
||||
{{ title }}
|
||||
<el-tooltip v-if="description" :content="description">
|
||||
<Information class="ms-2" />
|
||||
</el-tooltip>
|
||||
<Badge v-if="beta" label="Beta" />
|
||||
</slot>
|
||||
<el-button
|
||||
class="star-button"
|
||||
:class="{'star-active': bookmarked}"
|
||||
:icon="bookmarked ? StarIcon : StarOutlineIcon"
|
||||
circle
|
||||
@click="onStarClick"
|
||||
<div class="d-flex align-items-end gap-2">
|
||||
<SidebarToggleButton
|
||||
v-if="layoutStore.sideMenuCollapsed"
|
||||
@toggle="layoutStore.setSideMenuCollapsed(false)"
|
||||
/>
|
||||
</h1>
|
||||
<div class="d-flex flex-column gap-2">
|
||||
<el-breadcrumb v-if="breadcrumb">
|
||||
<el-breadcrumb-item v-for="(item, x) in breadcrumb" :key="x" :class="{'pe-none': item.disabled}">
|
||||
<a v-if="item.disabled || !item.link">
|
||||
{{ item.label }}
|
||||
</a>
|
||||
<router-link v-else :to="item.link">
|
||||
{{ item.label }}
|
||||
</router-link>
|
||||
</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
<h1 class="h5 fw-semibold m-0 d-inline-flex">
|
||||
<slot name="title">
|
||||
{{ title }}
|
||||
<el-tooltip v-if="description" :content="description">
|
||||
<Information class="ms-2" />
|
||||
</el-tooltip>
|
||||
<Badge v-if="beta" label="Beta" />
|
||||
</slot>
|
||||
<el-button
|
||||
class="star-button"
|
||||
:class="{'star-active': bookmarked}"
|
||||
:icon="bookmarked ? StarIcon : StarOutlineIcon"
|
||||
circle
|
||||
@click="onStarClick"
|
||||
/>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-lg-flex side gap-2 flex-shrink-0 align-items-center mycontainer">
|
||||
<div class="d-none d-lg-flex align-items-center">
|
||||
@@ -61,6 +69,8 @@
|
||||
import {useBookmarksStore} from "../../stores/bookmarks";
|
||||
import {useToast} from "../../utils/toast";
|
||||
import {useFlowStore} from "../../stores/flow";
|
||||
import {useLayoutStore} from "../../stores/layout";
|
||||
import SidebarToggleButton from "./SidebarToggleButton.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
title: string;
|
||||
@@ -73,6 +83,7 @@
|
||||
const bookmarksStore = useBookmarksStore();
|
||||
const flowStore = useFlowStore();
|
||||
const route = useRoute();
|
||||
const layoutStore = useLayoutStore();
|
||||
|
||||
|
||||
const shouldDisplayDeleteButton = computed(() => {
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const context = computed(() => details.value.title);
|
||||
const context = computed(() => ({title:details.value.title}));
|
||||
useRouteContext(context);
|
||||
|
||||
const namespace = computed(() => route.params?.id) as Ref<string>;
|
||||
|
||||
@@ -40,58 +40,50 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import {computed, onMounted} from "vue";
|
||||
import Markdown from "../layout/Markdown.vue";
|
||||
import {SchemaToHtml, TaskIcon} from "@kestra-io/ui-libs";
|
||||
import GitHub from "vue-material-design-icons/Github.vue";
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import intro from "../../assets/docs/basic.md?raw";
|
||||
import {getPluginReleaseUrl} from "../../utils/pluginUtils";
|
||||
import {mapStores} from "pinia";
|
||||
import {usePluginsStore} from "../../stores/plugins";
|
||||
import {useMiscStore} from "override/stores/misc";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
overrideIntro: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
absolute: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
fetchPluginDocumentation: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapStores(usePluginsStore, useMiscStore),
|
||||
introContent () {
|
||||
return this.overrideIntro ?? intro
|
||||
},
|
||||
pluginName() {
|
||||
const split = this.pluginsStore.editorPlugin.cls.split(".");
|
||||
return split[split.length - 1];
|
||||
},
|
||||
releaseNotesUrl() {
|
||||
return getPluginReleaseUrl(this.pluginsStore.editorPlugin.cls);
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.pluginsStore.list();
|
||||
},
|
||||
methods: {
|
||||
openReleaseNotes() {
|
||||
if (this.releaseNotesUrl) {
|
||||
window.open(this.releaseNotesUrl, "_blank");
|
||||
}
|
||||
}
|
||||
const props = withDefaults(defineProps<{
|
||||
overrideIntro?: string | null;
|
||||
absolute?: boolean;
|
||||
fetchPluginDocumentation?: boolean;
|
||||
}>(), {
|
||||
overrideIntro: null,
|
||||
absolute: false,
|
||||
fetchPluginDocumentation: true
|
||||
});
|
||||
|
||||
const pluginsStore = usePluginsStore();
|
||||
const miscStore = useMiscStore();
|
||||
|
||||
const introContent = computed(() => props.overrideIntro ?? intro);
|
||||
|
||||
const pluginName = computed(() => {
|
||||
if (!pluginsStore.editorPlugin?.cls) return "";
|
||||
const split = pluginsStore.editorPlugin.cls.split(".");
|
||||
return split[split.length - 1];
|
||||
});
|
||||
|
||||
const releaseNotesUrl = computed(() =>
|
||||
pluginsStore.editorPlugin?.cls ? getPluginReleaseUrl(pluginsStore.editorPlugin.cls) : null
|
||||
);
|
||||
|
||||
function openReleaseNotes() {
|
||||
if (releaseNotesUrl.value) {
|
||||
window.open(releaseNotesUrl.value, "_blank");
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
pluginsStore.list();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -36,7 +36,7 @@ function statsGlobalData(config: Config, uid: string): any {
|
||||
|
||||
export async function initPostHogForSetup(config: Config): Promise<void> {
|
||||
try {
|
||||
if (!config.isUiAnonymousUsageEnabled) return
|
||||
if (!config.isUiAnonymousUsageEnabled || import.meta.env.MODE === "development") return
|
||||
|
||||
const apiStore = useApiStore()
|
||||
const apiConfig = await apiStore.loadConfig()
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
import {ref, computed} from "vue"
|
||||
import {ref, computed, Ref} from "vue"
|
||||
|
||||
export function useSelectTableActions(selectTableRef: any) {
|
||||
export function useSelectTableActions({
|
||||
dataTableRef,
|
||||
selectionMapper
|
||||
}: {
|
||||
dataTableRef: Ref<any>
|
||||
selectionMapper?: (element: any) => any
|
||||
}) {
|
||||
const queryBulkAction = ref(false)
|
||||
const selection = ref<any[]>([])
|
||||
|
||||
const elTable = computed(() => selectTableRef.value?.$refs?.table)
|
||||
const elTable = computed(() => dataTableRef.value?.$refs?.table)
|
||||
|
||||
selectionMapper = selectionMapper ?? ((element: any) => element)
|
||||
|
||||
const handleSelectionChange = (value: any[]) => {
|
||||
selection.value = value.map(selectionMapper)
|
||||
@@ -22,8 +30,6 @@ export function useSelectTableActions(selectTableRef: any) {
|
||||
queryBulkAction.value = true
|
||||
}
|
||||
|
||||
const selectionMapper = (element: any) => element
|
||||
|
||||
return {
|
||||
queryBulkAction,
|
||||
selection,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<LeftMenu v-if="miscStore.configs" @menu-collapse="onMenuCollapse" />
|
||||
<LeftMenu v-if="miscStore.configs && !layoutStore.sideMenuCollapsed" @menu-collapse="onMenuCollapse" />
|
||||
<main>
|
||||
<Errors v-if="coreStore.error" :code="coreStore.error" />
|
||||
<slot v-else />
|
||||
@@ -17,20 +17,20 @@
|
||||
import Errors from "../../../components/errors/Errors.vue"
|
||||
import ContextInfoBar from "../../../components/ContextInfoBar.vue"
|
||||
import SurveyDialog from "../../../components/SurveyDialog.vue"
|
||||
import {onMounted, ref} from "vue"
|
||||
import {onMounted, ref, watch} from "vue"
|
||||
import {useSurveySkip} from "../../../composables/useSurveyData"
|
||||
import {useCoreStore} from "../../../stores/core"
|
||||
import {useMiscStore} from "override/stores/misc"
|
||||
import {useLayoutStore} from "../../../stores/layout"
|
||||
|
||||
const coreStore = useCoreStore()
|
||||
const miscStore = useMiscStore()
|
||||
const layoutStore = useLayoutStore()
|
||||
const {markSurveyDialogShown} = useSurveySkip()
|
||||
const showSurveyDialog = ref(false)
|
||||
|
||||
const onMenuCollapse = (collapse) => {
|
||||
const htmlElement = document.documentElement
|
||||
htmlElement.classList.toggle("menu-collapsed", collapse)
|
||||
htmlElement.classList.toggle("menu-not-collapsed", !collapse)
|
||||
layoutStore.setSideMenuCollapsed(collapse)
|
||||
}
|
||||
|
||||
const handleSurveyDialogClose = () => {
|
||||
@@ -49,8 +49,11 @@
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const isMenuCollapsed = localStorage.getItem("menuCollapsed") === "true"
|
||||
onMenuCollapse(isMenuCollapsed)
|
||||
onMenuCollapse(layoutStore.sideMenuCollapsed)
|
||||
checkForSurveyDialog()
|
||||
})
|
||||
|
||||
watch(() => layoutStore.sideMenuCollapsed, (val) => {
|
||||
onMenuCollapse(val)
|
||||
})
|
||||
</script>
|
||||
@@ -4,13 +4,15 @@ interface State {
|
||||
topNavbar: any | undefined;
|
||||
envName: string | undefined;
|
||||
envColor: string | undefined;
|
||||
sideMenuCollapsed: boolean;
|
||||
}
|
||||
|
||||
export const useLayoutStore = defineStore("layout", {
|
||||
state: (): State => ({
|
||||
topNavbar: undefined,
|
||||
envName: localStorage.getItem("envName") || undefined,
|
||||
envColor: localStorage.getItem("envColor") || undefined
|
||||
envColor: localStorage.getItem("envColor") || undefined,
|
||||
sideMenuCollapsed: localStorage.getItem("menuCollapsed") === "true",
|
||||
}),
|
||||
getters: {},
|
||||
actions: {
|
||||
@@ -34,6 +36,15 @@ export const useLayoutStore = defineStore("layout", {
|
||||
localStorage.removeItem("envColor");
|
||||
}
|
||||
this.envColor = value;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
setSideMenuCollapsed(value: boolean) {
|
||||
this.sideMenuCollapsed = value;
|
||||
localStorage.setItem("menuCollapsed", value ? "true" : "false");
|
||||
|
||||
const htmlElement = document.documentElement;
|
||||
htmlElement.classList.toggle("menu-collapsed", value);
|
||||
htmlElement.classList.toggle("menu-not-collapsed", !value);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -52,7 +52,7 @@ interface State {
|
||||
type?: string;
|
||||
version?: string;
|
||||
};
|
||||
forceIncludeProperties?: Record<string, any>;
|
||||
forceIncludeProperties?: string[];
|
||||
_iconsPromise: Promise<Record<string, string>> | undefined;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user