mirror of
https://github.com/kestra-io/kestra.git
synced 2025-12-25 11:12:12 -05:00
Compare commits
46 Commits
plugin/tem
...
v0.22.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5f2901f7f | ||
|
|
ea2bb3f6bd | ||
|
|
ed58b7b5b8 | ||
|
|
e6e0ffcdb7 | ||
|
|
ca84a0fbfd | ||
|
|
08579cf555 | ||
|
|
2b5d08c9f2 | ||
|
|
822a3b438a | ||
|
|
f6db013142 | ||
|
|
a031bfc129 | ||
|
|
1613dee76b | ||
|
|
a884708862 | ||
|
|
91bf3207f4 | ||
|
|
95b1f8dfcc | ||
|
|
4e7c6e87be | ||
|
|
8ac089de1d | ||
|
|
3d7c891b95 | ||
|
|
54eccac637 | ||
|
|
b3799cc039 | ||
|
|
143ebc061f | ||
|
|
224026c399 | ||
|
|
a093198004 | ||
|
|
380e329e97 | ||
|
|
6ee206f5f3 | ||
|
|
6c43f9c7c3 | ||
|
|
ec067e1a06 | ||
|
|
8ebc3fbba7 | ||
|
|
7e087c696c | ||
|
|
04b84df6ea | ||
|
|
b8e8333f62 | ||
|
|
54aa935702 | ||
|
|
8be17827c7 | ||
|
|
9d83d9b6eb | ||
|
|
ccd47f14ae | ||
|
|
8f4ce5fc18 | ||
|
|
acb305dfdb | ||
|
|
4c93a2b0e9 | ||
|
|
dea66ca259 | ||
|
|
c965f2f64c | ||
|
|
6516f7fc60 | ||
|
|
2dd61fc194 | ||
|
|
771e841d78 | ||
|
|
4448203031 | ||
|
|
e083163583 | ||
|
|
8617eb0c7b | ||
|
|
2a002e9531 |
@@ -24,15 +24,9 @@ on:
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
build-artifacts:
|
||||
name: Build - Artifacts
|
||||
uses: ./.github/workflows/workflow-build-artifacts.yml
|
||||
with:
|
||||
plugin-version: ${{ github.event.inputs.plugin-version != null && github.event.inputs.plugin-version || 'LATEST' }}
|
||||
|
||||
publish:
|
||||
name: Publish - Docker
|
||||
needs: build-artifacts
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
|
||||
10
.github/workflows/workflow-release.yml
vendored
10
.github/workflows/workflow-release.yml
vendored
@@ -38,9 +38,18 @@ on:
|
||||
description: "The Sonatype GPG file."
|
||||
required: true
|
||||
jobs:
|
||||
build-artifacts:
|
||||
name: Build - Artifacts
|
||||
uses: ./.github/workflows/workflow-build-artifacts.yml
|
||||
with:
|
||||
plugin-version: ${{ github.event.inputs.plugin-version != null && github.event.inputs.plugin-version || 'LATEST' }}
|
||||
|
||||
Docker:
|
||||
name: Publish Docker
|
||||
needs: build-artifacts
|
||||
uses: ./.github/workflows/workflow-publish-docker.yml
|
||||
with:
|
||||
plugin-version: ${{ github.event.inputs.plugin-version != null && github.event.inputs.plugin-version || 'LATEST' }}
|
||||
secrets:
|
||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
@@ -57,6 +66,7 @@ jobs:
|
||||
|
||||
Github:
|
||||
name: Github Release
|
||||
needs: build-artifacts
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: ./.github/workflows/workflow-github-release.yml
|
||||
secrets:
|
||||
|
||||
@@ -18,6 +18,8 @@ import picocli.CommandLine;
|
||||
FlowNamespaceCommand.class,
|
||||
FlowDotCommand.class,
|
||||
FlowExportCommand.class,
|
||||
FlowUpdateCommand.class,
|
||||
FlowUpdatesCommand.class
|
||||
}
|
||||
)
|
||||
@Slf4j
|
||||
|
||||
@@ -12,6 +12,7 @@ import picocli.CommandLine.Command;
|
||||
mixinStandardHelpOptions = true,
|
||||
subcommands = {
|
||||
PluginInstallCommand.class,
|
||||
PluginUninstallCommand.class,
|
||||
PluginListCommand.class,
|
||||
PluginDocCommand.class,
|
||||
PluginSearchCommand.class
|
||||
|
||||
@@ -17,10 +17,10 @@ import java.util.List;
|
||||
|
||||
@CommandLine.Command(
|
||||
name = "uninstall",
|
||||
description = "uninstall a plugin"
|
||||
description = "Uninstall plugins"
|
||||
)
|
||||
public class PluginUninstallCommand extends AbstractCommand {
|
||||
@Parameters(index = "0..*", description = "the plugins to uninstall")
|
||||
@Parameters(index = "0..*", description = "The plugins to uninstall. Represented as Maven artifact coordinates (i.e., <groupId>:<artifactId>:(<version>|LATEST)")
|
||||
List<String> dependencies = new ArrayList<>();
|
||||
|
||||
@Spec
|
||||
|
||||
@@ -2,6 +2,7 @@ package io.kestra.cli.commands.servers;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import io.kestra.cli.services.FileChangedEventListener;
|
||||
import io.kestra.core.contexts.KestraContext;
|
||||
import io.kestra.core.models.ServerType;
|
||||
import io.kestra.core.repositories.LocalFlowRepositoryLoader;
|
||||
import io.kestra.core.runners.StandAloneRunner;
|
||||
@@ -88,9 +89,10 @@ public class StandAloneCommand extends AbstractServerCommand {
|
||||
this.skipExecutionService.setSkipFlows(skipFlows);
|
||||
this.skipExecutionService.setSkipNamespaces(skipNamespaces);
|
||||
this.skipExecutionService.setSkipTenants(skipTenants);
|
||||
|
||||
this.startExecutorService.applyOptions(startExecutors, notStartExecutors);
|
||||
|
||||
KestraContext.getContext().injectWorkerConfigs(workerThread, null);
|
||||
|
||||
super.call();
|
||||
|
||||
if (flowPath != null) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package io.kestra.cli.commands.servers;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import io.kestra.core.contexts.KestraContext;
|
||||
import io.kestra.core.models.ServerType;
|
||||
import io.kestra.core.runners.Worker;
|
||||
import io.kestra.core.utils.Await;
|
||||
@@ -36,7 +37,11 @@ public class WorkerCommand extends AbstractServerCommand {
|
||||
|
||||
@Override
|
||||
public Integer call() throws Exception {
|
||||
|
||||
KestraContext.getContext().injectWorkerConfigs(thread, workerGroupKey);
|
||||
|
||||
super.call();
|
||||
|
||||
if (this.workerGroupKey != null && !this.workerGroupKey.matches("[a-zA-Z0-9_-]+")) {
|
||||
throw new IllegalArgumentException("The --worker-group option must match the [a-zA-Z0-9_-]+ pattern");
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ import io.micronaut.context.env.Environment;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
@@ -27,6 +29,10 @@ public abstract class KestraContext {
|
||||
// Properties
|
||||
public static final String KESTRA_SERVER_TYPE = "kestra.server-type";
|
||||
|
||||
// Those properties are injected bases on the CLI args.
|
||||
private static final String KESTRA_WORKER_MAX_NUM_THREADS = "kestra.worker.max-num-threads";
|
||||
private static final String KESTRA_WORKER_GROUP_KEY = "kestra.worker.group-key";
|
||||
|
||||
/**
|
||||
* Gets the current {@link KestraContext}.
|
||||
*
|
||||
@@ -54,6 +60,12 @@ public abstract class KestraContext {
|
||||
*/
|
||||
public abstract ServerType getServerType();
|
||||
|
||||
public abstract Optional<Integer> getWorkerMaxNumThreads();
|
||||
|
||||
public abstract Optional<String> getWorkerGroupKey();
|
||||
|
||||
public abstract void injectWorkerConfigs(Integer maxNumThreads, String workerGroupKey);
|
||||
|
||||
/**
|
||||
* Returns the Kestra Version.
|
||||
*
|
||||
@@ -110,6 +122,34 @@ public abstract class KestraContext {
|
||||
.orElse(ServerType.STANDALONE);
|
||||
}
|
||||
|
||||
/** {@inheritDoc} **/
|
||||
@Override
|
||||
public Optional<Integer> getWorkerMaxNumThreads() {
|
||||
return Optional.ofNullable(environment)
|
||||
.flatMap(env -> env.getProperty(KESTRA_WORKER_MAX_NUM_THREADS, Integer.class));
|
||||
}
|
||||
|
||||
/** {@inheritDoc} **/
|
||||
@Override
|
||||
public Optional<String> getWorkerGroupKey() {
|
||||
return Optional.ofNullable(environment)
|
||||
.flatMap(env -> env.getProperty(KESTRA_WORKER_GROUP_KEY, String.class));
|
||||
}
|
||||
/** {@inheritDoc} **/
|
||||
@Override
|
||||
public void injectWorkerConfigs(Integer maxNumThreads, String workerGroupKey) {
|
||||
final Map<String, Object> configs = new HashMap<>();
|
||||
Optional.ofNullable(maxNumThreads)
|
||||
.ifPresent(val -> configs.put(KESTRA_WORKER_MAX_NUM_THREADS, val));
|
||||
|
||||
Optional.ofNullable(workerGroupKey)
|
||||
.ifPresent(val -> configs.put(KESTRA_WORKER_GROUP_KEY, val));
|
||||
|
||||
if (!configs.isEmpty()) {
|
||||
environment.addPropertySource("kestra-runtime", configs);
|
||||
}
|
||||
}
|
||||
|
||||
/** {@inheritDoc} **/
|
||||
@Override
|
||||
public void shutdown() {
|
||||
|
||||
@@ -81,7 +81,7 @@ public class JsonSchemaGenerator {
|
||||
objectNode.put("type", "array");
|
||||
}
|
||||
replaceAnyOfWithOneOf(objectNode);
|
||||
pullOfDefaultFromOneOf(objectNode);
|
||||
pullDocumentationAndDefaultFromOneOf(objectNode);
|
||||
removeRequiredOnPropsWithDefaults(objectNode);
|
||||
|
||||
return JacksonMapper.toMap(objectNode);
|
||||
@@ -122,22 +122,35 @@ public class JsonSchemaGenerator {
|
||||
// This hack exists because for Property we generate a oneOf for properties that are not strings.
|
||||
// By default, the 'default' is in each oneOf which Monaco editor didn't take into account.
|
||||
// So, we pull off the 'default' from any of the oneOf to the parent.
|
||||
private void pullOfDefaultFromOneOf(ObjectNode objectNode) {
|
||||
// same thing for documentation fields: 'title', 'description', '$deprecated'
|
||||
private void pullDocumentationAndDefaultFromOneOf(ObjectNode objectNode) {
|
||||
objectNode.findParents("oneOf").forEach(jsonNode -> {
|
||||
if (jsonNode instanceof ObjectNode oNode) {
|
||||
JsonNode oneOf = oNode.get("oneOf");
|
||||
if (oneOf instanceof ArrayNode arrayNode) {
|
||||
Iterator<JsonNode> it = arrayNode.elements();
|
||||
JsonNode defaultNode = null;
|
||||
while (it.hasNext() && defaultNode == null) {
|
||||
var nodesToPullUp = new HashMap<String, Optional<JsonNode>>(Map.ofEntries(
|
||||
Map.entry("default", Optional.empty()),
|
||||
Map.entry("title", Optional.empty()),
|
||||
Map.entry("description", Optional.empty()),
|
||||
Map.entry("$deprecated", Optional.empty())
|
||||
));
|
||||
// find nodes to pull up
|
||||
while (it.hasNext() && nodesToPullUp.containsValue(Optional.<JsonNode>empty())) {
|
||||
JsonNode next = it.next();
|
||||
if (next instanceof ObjectNode nextAsObj) {
|
||||
defaultNode = nextAsObj.get("default");
|
||||
nodesToPullUp.entrySet().stream()
|
||||
.filter(node -> node.getValue().isEmpty())
|
||||
.forEach(node -> node
|
||||
.setValue(Optional.ofNullable(
|
||||
nextAsObj.get(node.getKey())
|
||||
)));
|
||||
}
|
||||
}
|
||||
if (defaultNode != null) {
|
||||
oNode.set("default", defaultNode);
|
||||
}
|
||||
// create nodes on parent
|
||||
nodesToPullUp.entrySet().stream()
|
||||
.filter(node -> node.getValue().isPresent())
|
||||
.forEach(node -> oNode.set(node.getKey(), node.getValue().get()));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -629,7 +642,7 @@ public class JsonSchemaGenerator {
|
||||
try {
|
||||
ObjectNode objectNode = generator.generateSchema(cls);
|
||||
replaceAnyOfWithOneOf(objectNode);
|
||||
pullOfDefaultFromOneOf(objectNode);
|
||||
pullDocumentationAndDefaultFromOneOf(objectNode);
|
||||
removeRequiredOnPropsWithDefaults(objectNode);
|
||||
|
||||
return JacksonMapper.toMap(extractMainRef(objectNode));
|
||||
|
||||
@@ -8,6 +8,7 @@ public enum CrudEventType {
|
||||
LOGIN,
|
||||
LOGOUT,
|
||||
IMPERSONATE,
|
||||
LOGIN_FAILURE
|
||||
LOGIN_FAILURE,
|
||||
ACCOUNT_LOCKED
|
||||
}
|
||||
|
||||
|
||||
@@ -23,9 +23,9 @@ public class RunContextResponseInterceptor implements HttpResponseInterceptor {
|
||||
response instanceof BasicClassicHttpResponse httpResponse
|
||||
) {
|
||||
try {
|
||||
// FIXME temporary fix for https://github.com/kestra-io/kestra/issues/8092
|
||||
runContext.logger().debug(
|
||||
"Request '{}' from '{}' with the response code '{}'",
|
||||
httpClientContext.getRequest().getUri(),
|
||||
"Request " + httpClientContext.getRequest().getUri() + " from '{}' with the response code '{}'",
|
||||
httpClientContext.getEndpointDetails().getRemoteAddress(),
|
||||
response.getCode()
|
||||
);
|
||||
|
||||
@@ -19,6 +19,7 @@ public record Label(@NotNull String key, @NotNull String value) {
|
||||
public static final String RESTARTED = SYSTEM_PREFIX + "restarted";
|
||||
public static final String REPLAY = SYSTEM_PREFIX + "replay";
|
||||
public static final String REPLAYED = SYSTEM_PREFIX + "replayed";
|
||||
public static final String SIMULATED_EXECUTION = SYSTEM_PREFIX + "simulatedExecution";
|
||||
|
||||
/**
|
||||
* Static helper method for converting a list of labels to a nested map.
|
||||
|
||||
@@ -10,7 +10,7 @@ import jakarta.validation.constraints.Pattern;
|
||||
*/
|
||||
public interface PluginVersioning {
|
||||
|
||||
@Pattern(regexp="\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9]+)?|([a-zA-Z0-9]+)")
|
||||
@Pattern(regexp="\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9-]+)?|([a-zA-Z0-9]+)")
|
||||
@Schema(title = "The version of the plugin to use.")
|
||||
String getVersion();
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ public record QueryFilter(
|
||||
NAMESPACE("namespace") {
|
||||
@Override
|
||||
public List<Op> supportedOp() {
|
||||
return List.of(Op.EQUALS, Op.NOT_EQUALS, Op.CONTAINS, Op.STARTS_WITH, Op.ENDS_WITH, Op.REGEX);
|
||||
return List.of(Op.EQUALS, Op.NOT_EQUALS, Op.CONTAINS, Op.STARTS_WITH, Op.ENDS_WITH, Op.REGEX, Op.IN);
|
||||
}
|
||||
},
|
||||
LABELS("labels") {
|
||||
|
||||
@@ -38,7 +38,7 @@ public record PluginArtifact(
|
||||
"([^: ]+):([^: ]+)(:([^: ]*)(:([^: ]+))?)?:([^: ]+)"
|
||||
);
|
||||
private static final Pattern FILENAME_PATTERN = Pattern.compile(
|
||||
"^(?<groupId>[\\w_]+)__(?<artifactId>[\\w-_]+)(?:__(?<classifier>[\\w-_]+))?__(?<version>\\d+_\\d+_\\d+(-[a-zA-Z0-9]+)?|([a-zA-Z0-9]+))\\.jar$"
|
||||
"^(?<groupId>[\\w_]+)__(?<artifactId>[\\w-_]+)(?:__(?<classifier>[\\w-_]+))?__(?<version>\\d+_\\d+_\\d+(-[a-zA-Z0-9-]+)?|([a-zA-Z0-9]+))\\.jar$"
|
||||
);
|
||||
|
||||
public static final String JAR_EXTENSION = "jar";
|
||||
|
||||
@@ -94,6 +94,7 @@ public interface ExecutionRepositoryInterface extends SaveRepositoryInterface<Ex
|
||||
boolean allowDeleted
|
||||
);
|
||||
|
||||
Flux<Execution> findAllAsync(@Nullable String tenantId);
|
||||
|
||||
ArrayListTotal<TaskRun> findTaskRun(
|
||||
Pageable pageable,
|
||||
|
||||
@@ -88,6 +88,8 @@ public interface LogRepositoryInterface extends SaveRepositoryInterface<LogEntry
|
||||
ZonedDateTime startDate
|
||||
);
|
||||
|
||||
Flux<LogEntry> findAllAsync(@Nullable String tenantId);
|
||||
|
||||
List<LogStatistics> statistics(
|
||||
@Nullable String query,
|
||||
@Nullable String tenantId,
|
||||
|
||||
@@ -6,6 +6,7 @@ import io.kestra.core.models.executions.metrics.MetricAggregations;
|
||||
import io.kestra.plugin.core.dashboard.data.Metrics;
|
||||
import io.micronaut.core.annotation.Nullable;
|
||||
import io.micronaut.data.model.Pageable;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.List;
|
||||
@@ -28,6 +29,8 @@ public interface MetricRepositoryInterface extends SaveRepositoryInterface<Metri
|
||||
|
||||
Integer purge(Execution execution);
|
||||
|
||||
Flux<MetricEntry> findAllAsync(@Nullable String tenantId);
|
||||
|
||||
default Function<String, String> sortMapping() throws IllegalArgumentException {
|
||||
return s -> s;
|
||||
}
|
||||
|
||||
@@ -201,7 +201,7 @@ public final class FileSerde {
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> SequenceWriter createSequenceWriter(ObjectMapper objectMapper, Writer writer, TypeReference<T> type) throws IOException {
|
||||
public static <T> SequenceWriter createSequenceWriter(ObjectMapper objectMapper, Writer writer, TypeReference<T> type) throws IOException {
|
||||
return objectMapper.writerFor(type).writeValues(writer);
|
||||
}
|
||||
|
||||
|
||||
@@ -132,6 +132,15 @@ public class FlowService {
|
||||
}
|
||||
|
||||
List<String> warnings = new ArrayList<>(checkValidSubflows(flow, tenantId));
|
||||
List<io.kestra.plugin.core.trigger.Flow> flowTriggers = ListUtils.emptyOnNull(flow.getTriggers()).stream()
|
||||
.filter(io.kestra.plugin.core.trigger.Flow.class::isInstance)
|
||||
.map(io.kestra.plugin.core.trigger.Flow.class::cast)
|
||||
.toList();
|
||||
flowTriggers.forEach(flowTrigger -> {
|
||||
if (ListUtils.emptyOnNull(flowTrigger.getConditions()).isEmpty() && flowTrigger.getPreconditions() == null) {
|
||||
warnings.add("This flow will be triggered for EVERY execution of EVERY flow on your instance. We recommend adding the preconditions property to the Flow trigger '" + flowTrigger.getId() + "'.");
|
||||
}
|
||||
});
|
||||
|
||||
return warnings;
|
||||
}
|
||||
@@ -140,7 +149,11 @@ public class FlowService {
|
||||
try {
|
||||
Map<String, Class<?>> aliases = pluginRegistry.plugins().stream()
|
||||
.flatMap(plugin -> plugin.getAliases().values().stream())
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
.collect(Collectors.toMap(
|
||||
Map.Entry::getKey,
|
||||
Map.Entry::getValue,
|
||||
(existing, duplicate) -> existing
|
||||
));
|
||||
Map<String, Object> stringObjectMap = JacksonMapper.ofYaml().readValue(flowSource, JacksonMapper.MAP_TYPE_REFERENCE);
|
||||
return relocations(aliases, stringObjectMap);
|
||||
} catch (JsonProcessingException e) {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package io.kestra.core.services;
|
||||
|
||||
import io.kestra.core.repositories.FlowRepositoryInterface;
|
||||
import io.kestra.core.utils.NamespaceUtils;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
@@ -19,7 +21,7 @@ public class NamespaceService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a given namespace exists.
|
||||
* Checks whether a given namespace exists. A namespace is considered existing if at least one Flow is within the namespace or a parent namespace
|
||||
*
|
||||
* @param tenant The tenant ID
|
||||
* @param namespace The namespace - cannot be null.
|
||||
@@ -29,7 +31,10 @@ public class NamespaceService {
|
||||
Objects.requireNonNull(namespace, "namespace cannot be null");
|
||||
|
||||
if (flowRepository.isPresent()) {
|
||||
List<String> namespaces = flowRepository.get().findDistinctNamespace(tenant);
|
||||
List<String> namespaces = flowRepository.get().findDistinctNamespace(tenant).stream()
|
||||
.map(NamespaceUtils::asTree)
|
||||
.flatMap(Collection::stream)
|
||||
.toList();
|
||||
return namespaces.stream().anyMatch(ns -> ns.equals(namespace) || ns.startsWith(namespace));
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -29,7 +29,7 @@ import java.util.stream.Stream;
|
||||
@Slf4j
|
||||
@Singleton
|
||||
public class FlowTopologyService {
|
||||
public static final Label SIMULATED_EXECUTION = new Label(Label.SYSTEM_PREFIX + "simulatedExecution", "true");
|
||||
public static final Label SIMULATED_EXECUTION = new Label(Label.SIMULATED_EXECUTION, "true");
|
||||
|
||||
@Inject
|
||||
protected ConditionService conditionService;
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
package io.kestra.core.utils;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
@Slf4j
|
||||
public class MapUtils {
|
||||
private static final String CONFLICT_AT_KEY_MSG = "Conflict at key: '{}', ignoring it. Map keys are: {}";
|
||||
|
||||
public static Map<String, Object> merge(Map<String, Object> a, Map<String, Object> b) {
|
||||
if (a == null && b == null) {
|
||||
return null;
|
||||
@@ -136,9 +140,9 @@ public class MapUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method nested a flatten map.
|
||||
* Utility method nested a flattened map.
|
||||
*
|
||||
* @param flatMap the flatten map.
|
||||
* @param flatMap the flattened map.
|
||||
* @return the nested map.
|
||||
*
|
||||
* @throws IllegalArgumentException if the given map contains conflicting keys.
|
||||
@@ -156,13 +160,15 @@ public class MapUtils {
|
||||
currentMap.put(key, new HashMap<>());
|
||||
} else if (!(currentMap.get(key) instanceof Map)) {
|
||||
var invalidKey = String.join(",", Arrays.copyOfRange(keys, 0, i));
|
||||
throw new IllegalArgumentException("Conflict at key: '" + invalidKey + "'. Map keys are: " + flatMap.keySet());
|
||||
log.warn(CONFLICT_AT_KEY_MSG, invalidKey, flatMap.keySet());
|
||||
continue;
|
||||
}
|
||||
currentMap = (Map<String, Object>) currentMap.get(key);
|
||||
}
|
||||
String lastKey = keys[keys.length - 1];
|
||||
if (currentMap.containsKey(lastKey)) {
|
||||
throw new IllegalArgumentException("Conflict at key: '" + lastKey + "', Map keys are: " + flatMap.keySet());
|
||||
log.warn("Conflict at key: '{}', ignoring it. Map keys are: {}", lastKey, flatMap.keySet());
|
||||
continue;
|
||||
}
|
||||
currentMap.put(lastKey, entry.getValue());
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
package io.kestra.core.validations;
|
||||
|
||||
import io.kestra.core.validations.validator.ConditionValidator;
|
||||
import jakarta.validation.Constraint;
|
||||
import jakarta.validation.Payload;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Constraint(validatedBy = ConditionValidator.class)
|
||||
public @interface ConditionValidation {
|
||||
String message() default "one condition must be set";
|
||||
Class<?>[] groups() default {};
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package io.kestra.core.validations.validator;
|
||||
|
||||
import io.kestra.core.validations.ConditionValidation;
|
||||
import io.kestra.plugin.core.trigger.Flow;
|
||||
import io.micronaut.core.annotation.AnnotationValue;
|
||||
import io.micronaut.core.annotation.NonNull;
|
||||
import io.micronaut.core.annotation.Nullable;
|
||||
import io.micronaut.validation.validator.constraints.ConstraintValidator;
|
||||
import io.micronaut.validation.validator.constraints.ConstraintValidatorContext;
|
||||
import jakarta.inject.Singleton;
|
||||
|
||||
@Singleton
|
||||
public class ConditionValidator implements ConstraintValidator<ConditionValidation, Flow> {
|
||||
@Override
|
||||
public boolean isValid(@Nullable Flow value, @NonNull AnnotationValue<ConditionValidation> annotationMetadata, @NonNull ConstraintValidatorContext context) {
|
||||
if (value == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return value.getConditions() != null || value.getPreconditions() != null;
|
||||
}
|
||||
}
|
||||
@@ -4,47 +4,51 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import io.kestra.core.exceptions.IllegalVariableEvaluationException;
|
||||
import io.kestra.core.exceptions.InternalException;
|
||||
import io.kestra.core.models.Label;
|
||||
import io.kestra.core.models.annotations.Example;
|
||||
import io.kestra.core.models.annotations.Plugin;
|
||||
import io.kestra.core.models.annotations.PluginProperty;
|
||||
import io.kestra.core.models.conditions.Condition;
|
||||
import io.kestra.core.models.conditions.ConditionContext;
|
||||
import io.kestra.core.models.executions.Execution;
|
||||
import io.kestra.core.models.executions.ExecutionTrigger;
|
||||
import io.kestra.core.models.flows.State;
|
||||
import io.kestra.core.models.triggers.AbstractTrigger;
|
||||
import io.kestra.core.models.triggers.TimeWindow;
|
||||
import io.kestra.core.models.triggers.TriggerOutput;
|
||||
import io.kestra.core.models.triggers.multipleflows.MultipleCondition;
|
||||
import io.kestra.core.models.triggers.multipleflows.MultipleConditionStorageInterface;
|
||||
import io.kestra.core.models.triggers.multipleflows.MultipleConditionWindow;
|
||||
import io.kestra.core.runners.RunContext;
|
||||
import io.kestra.core.services.LabelService;
|
||||
import io.kestra.core.utils.IdUtils;
|
||||
import io.kestra.core.utils.ListUtils;
|
||||
import io.kestra.core.utils.MapUtils;
|
||||
import io.kestra.core.utils.TruthUtils;
|
||||
import io.kestra.core.validations.ConditionValidation;
|
||||
import io.kestra.core.validations.PreconditionFilterValidation;
|
||||
import io.micronaut.core.annotation.Nullable;
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import lombok.*;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import io.kestra.core.models.annotations.Example;
|
||||
import io.kestra.core.models.annotations.Plugin;
|
||||
import io.kestra.core.models.executions.Execution;
|
||||
import io.kestra.core.models.executions.ExecutionTrigger;
|
||||
import io.kestra.core.models.flows.State;
|
||||
import io.kestra.core.models.triggers.AbstractTrigger;
|
||||
import io.kestra.core.models.triggers.TriggerOutput;
|
||||
import io.kestra.core.runners.RunContext;
|
||||
import io.kestra.core.utils.IdUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.stream.Streams;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import io.micronaut.core.annotation.Nullable;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import static io.kestra.core.topologies.FlowTopologyService.SIMULATED_EXECUTION;
|
||||
import static io.kestra.core.utils.Rethrow.throwPredicate;
|
||||
|
||||
@@ -194,7 +198,6 @@ import static io.kestra.core.utils.Rethrow.throwPredicate;
|
||||
aliases = "io.kestra.core.models.triggers.types.Flow"
|
||||
)
|
||||
@Slf4j
|
||||
@ConditionValidation
|
||||
public class Flow extends AbstractTrigger implements TriggerOutput<Flow.Output> {
|
||||
private static final String TRIGGER_VAR = "trigger";
|
||||
private static final String OUTPUTS_VAR = "outputs";
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package io.kestra.core.contexts;
|
||||
|
||||
import io.kestra.core.junit.annotations.KestraTest;
|
||||
import io.micronaut.context.ApplicationContext;
|
||||
import jakarta.inject.Inject;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
|
||||
@KestraTest
|
||||
class KestraContextTest {
|
||||
|
||||
@Inject
|
||||
KestraContext context;
|
||||
|
||||
@Test
|
||||
void shouldGetWorkerMaxNumThreads() {
|
||||
// When
|
||||
context.injectWorkerConfigs(16, null);
|
||||
|
||||
// Then
|
||||
assertThat(KestraContext.getContext().getWorkerMaxNumThreads(), is(Optional.of(16)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldGetWorkerGroupKey() {
|
||||
// When
|
||||
context.injectWorkerConfigs(null, "my-key");
|
||||
|
||||
// Then
|
||||
assertThat(KestraContext.getContext().getWorkerGroupKey(), is(Optional.of("my-key")));
|
||||
}
|
||||
}
|
||||
@@ -249,6 +249,30 @@ class JsonSchemaGeneratorTest {
|
||||
assertThat((List<String>) generate.get("required"), containsInAnyOrder("requiredWithNoDefault"));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
void testDocumentation() {
|
||||
Map<String, Object> generate = jsonSchemaGenerator.properties(Task.class, TaskWithDynamicDocumentedFields.class);
|
||||
assertThat(generate, is(not(nullValue())));
|
||||
assertThat(((Map<String, Map<String, Object>>) generate.get("properties")).get("stringProperty").get("title"), is("stringProperty title"));
|
||||
assertThat(((Map<String, Map<String, Object>>) generate.get("properties")).get("stringProperty").get("description"), is("stringProperty description"));
|
||||
assertThat(((Map<String, Map<String, Object>>) generate.get("properties")).get("stringProperty").get("$deprecated"), is(true));
|
||||
|
||||
assertThat(((Map<String, Map<String, Object>>) generate.get("properties")).get("integerProperty").get("title"), is("integerProperty title"));
|
||||
assertThat(((Map<String, Map<String, Object>>) generate.get("properties")).get("integerProperty").get("description"), is("integerProperty description"));
|
||||
assertThat(((Map<String, Map<String, Object>>) generate.get("properties")).get("integerProperty").get("$deprecated"), is(true));
|
||||
|
||||
assertThat(((Map<String, Map<String, Object>>) generate.get("properties")).get("stringPropertyWithDefault").get("title"), is("stringPropertyWithDefault title"));
|
||||
assertThat(((Map<String, Map<String, Object>>) generate.get("properties")).get("stringPropertyWithDefault").get("description"), is("stringPropertyWithDefault description"));
|
||||
assertThat(((Map<String, Map<String, Object>>) generate.get("properties")).get("stringPropertyWithDefault").get("$deprecated"), is(true));
|
||||
assertThat(((Map<String, Map<String, Object>>) generate.get("properties")).get("stringPropertyWithDefault").get("default"), is("my string"));
|
||||
|
||||
assertThat(((Map<String, Map<String, Object>>) generate.get("properties")).get("integerPropertyWithDefault").get("title"), is("integerPropertyWithDefault title"));
|
||||
assertThat(((Map<String, Map<String, Object>>) generate.get("properties")).get("integerPropertyWithDefault").get("description"), is("integerPropertyWithDefault description"));
|
||||
assertThat(((Map<String, Map<String, Object>>) generate.get("properties")).get("integerPropertyWithDefault").get("$deprecated"), is(true));
|
||||
assertThat(((Map<String, Map<String, Object>>) generate.get("properties")).get("integerPropertyWithDefault").get("default"), is("10000"));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
void dashboard() throws URISyntaxException {
|
||||
@@ -314,10 +338,11 @@ class JsonSchemaGeneratorTest {
|
||||
private TestEnum testEnum;
|
||||
|
||||
@PluginProperty
|
||||
@Schema(title = "Title from the attribute")
|
||||
@Schema(title = "Title from the attribute", description = "Description from the attribute")
|
||||
private TestClass testClass;
|
||||
|
||||
@PluginProperty(internalStorageURI = true)
|
||||
@Schema(title = "Title from the attribute", description = "Description from the attribute")
|
||||
private String uri;
|
||||
|
||||
@PluginProperty
|
||||
@@ -392,4 +417,49 @@ class JsonSchemaGeneratorTest {
|
||||
@NotNull
|
||||
private Property<TaskWithEnum.TestClass> requiredWithNoDefault;
|
||||
}
|
||||
|
||||
@SuperBuilder
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
public static class TaskWithDynamicDocumentedFields extends Task implements RunnableTask<VoidOutput> {
|
||||
|
||||
@Deprecated(since="deprecation_version_1", forRemoval=true)
|
||||
@Schema(
|
||||
title = "integerPropertyWithDefault title",
|
||||
description = "integerPropertyWithDefault description"
|
||||
)
|
||||
@Builder.Default
|
||||
protected Property<Integer> integerPropertyWithDefault = Property.of(10000);
|
||||
|
||||
@Deprecated(since="deprecation_version_1", forRemoval=true)
|
||||
@Schema(
|
||||
title = "stringPropertyWithDefault title",
|
||||
description = "stringPropertyWithDefault description"
|
||||
)
|
||||
@Builder.Default
|
||||
protected Property<String> stringPropertyWithDefault = Property.of("my string");
|
||||
|
||||
|
||||
@Deprecated(since="deprecation_version_1", forRemoval=true)
|
||||
@Schema(
|
||||
title = "stringProperty title",
|
||||
description = "stringProperty description"
|
||||
)
|
||||
protected Property<String> stringProperty;
|
||||
|
||||
@Deprecated(since="deprecation_version_1", forRemoval=true)
|
||||
@Schema(
|
||||
title = "integerProperty title",
|
||||
description = "integerProperty description"
|
||||
)
|
||||
protected Property<Integer> integerProperty;
|
||||
|
||||
@Override
|
||||
public VoidOutput run(RunContext runContext) throws Exception {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -33,6 +33,32 @@ class PluginArtifactTest {
|
||||
assertNull(artifact.uri());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldParseGivenValidFilenameWithQualifier() {
|
||||
String fileName = "io_kestra_plugin__plugin-serdes__custom-classifier__0_20_0-SNAPSHOT.jar";
|
||||
PluginArtifact artifact = PluginArtifact.fromFileName(fileName);
|
||||
|
||||
assertEquals("io.kestra.plugin", artifact.groupId());
|
||||
assertEquals("plugin-serdes", artifact.artifactId());
|
||||
assertEquals("jar", artifact.extension());
|
||||
assertEquals("custom-classifier", artifact.classifier());
|
||||
assertEquals("0.20.0-SNAPSHOT", artifact.version());
|
||||
assertNull(artifact.uri());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldParseGivenValidFilenameWithNonStandardQualifier() {
|
||||
String fileName = "io_kestra_plugin__plugin-serdes__custom-classifier__0_20_0-RC1-SNAPSHOT.jar";
|
||||
PluginArtifact artifact = PluginArtifact.fromFileName(fileName);
|
||||
|
||||
assertEquals("io.kestra.plugin", artifact.groupId());
|
||||
assertEquals("plugin-serdes", artifact.artifactId());
|
||||
assertEquals("jar", artifact.extension());
|
||||
assertEquals("custom-classifier", artifact.classifier());
|
||||
assertEquals("0.20.0-RC1-SNAPSHOT", artifact.version());
|
||||
assertNull(artifact.uri());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowIllegalArgumentExceptionGivenInvalidFilenameMissingVersion() {
|
||||
String fileName = "io_kestra_plugin__plugin-serdes__custom-classifier.jar";
|
||||
|
||||
@@ -784,4 +784,12 @@ public abstract class AbstractExecutionRepositoryTest {
|
||||
.taskRunList(List.of())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
protected void findAllAsync() {
|
||||
inject();
|
||||
|
||||
List<Execution> executions = executionRepository.findAllAsync(null).collectList().block();
|
||||
assertThat(executions, hasSize(28));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import java.util.List;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
|
||||
@KestraTest
|
||||
@@ -205,7 +206,7 @@ public abstract class AbstractLogRepositoryTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void findAsych() {
|
||||
void findAsync() {
|
||||
logRepository.save(logEntry(Level.INFO).build());
|
||||
logRepository.save(logEntry(Level.ERROR).build());
|
||||
logRepository.save(logEntry(Level.WARN).build());
|
||||
@@ -214,18 +215,29 @@ public abstract class AbstractLogRepositoryTest {
|
||||
|
||||
Flux<LogEntry> find = logRepository.findAsync(null, "io.kestra.unittest", Level.INFO, startDate);
|
||||
List<LogEntry> logEntries = find.collectList().block();
|
||||
assertThat(logEntries.size(), is(3));
|
||||
assertThat(logEntries, hasSize(3));
|
||||
|
||||
find = logRepository.findAsync(null, null, Level.ERROR, startDate);
|
||||
logEntries = find.collectList().block();
|
||||
assertThat(logEntries.size(), is(1));
|
||||
assertThat(logEntries, hasSize(1));
|
||||
|
||||
find = logRepository.findAsync(null, "io.kestra.unused", Level.INFO, startDate);
|
||||
logEntries = find.collectList().block();
|
||||
assertThat(logEntries.size(), is(0));
|
||||
assertThat(logEntries, hasSize(0));
|
||||
|
||||
find = logRepository.findAsync(null, null, Level.INFO, startDate.plusSeconds(2));
|
||||
logEntries = find.collectList().block();
|
||||
assertThat(logEntries.size(), is(0));
|
||||
assertThat(logEntries, hasSize(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void findAllAsync() {
|
||||
logRepository.save(logEntry(Level.INFO).build());
|
||||
logRepository.save(logEntry(Level.ERROR).build());
|
||||
logRepository.save(logEntry(Level.WARN).build());
|
||||
|
||||
Flux<LogEntry> find = logRepository.findAllAsync(null);
|
||||
List<LogEntry> logEntries = find.collectList().block();
|
||||
assertThat(logEntries, hasSize(3));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import io.kestra.core.models.executions.metrics.Timer;
|
||||
import io.micronaut.data.model.Pageable;
|
||||
import io.kestra.core.junit.annotations.KestraTest;
|
||||
import jakarta.inject.Inject;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.Duration;
|
||||
@@ -17,6 +16,7 @@ import java.time.ZonedDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
|
||||
@KestraTest
|
||||
@@ -95,6 +95,20 @@ public abstract class AbstractMetricRepositoryTest {
|
||||
assertThat(tasksWithMetrics.size(), is(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
void findAllAsync() {
|
||||
String executionId = FriendlyId.createFriendlyId();
|
||||
TaskRun taskRun1 = taskRun(executionId, "task");
|
||||
MetricEntry counter = MetricEntry.of(taskRun1, counter("counter"));
|
||||
TaskRun taskRun2 = taskRun(executionId, "task");
|
||||
MetricEntry timer = MetricEntry.of(taskRun2, timer());
|
||||
metricRepository.save(counter);
|
||||
metricRepository.save(timer);
|
||||
|
||||
List<MetricEntry> results = metricRepository.findAllAsync(null).collectList().block();
|
||||
assertThat(results, hasSize(2));
|
||||
}
|
||||
|
||||
private Counter counter(String metricName) {
|
||||
return Counter.of(metricName, 1);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package io.kestra.core.runners;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import io.kestra.core.junit.annotations.ExecuteFlow;
|
||||
import io.kestra.core.junit.annotations.KestraTest;
|
||||
@@ -10,11 +9,9 @@ import io.kestra.core.junit.annotations.LoadFlows;
|
||||
import io.kestra.core.models.executions.Execution;
|
||||
import io.kestra.core.models.executions.LogEntry;
|
||||
import io.kestra.core.models.flows.State;
|
||||
import io.kestra.core.queues.MessageTooBigException;
|
||||
import io.kestra.core.queues.QueueException;
|
||||
import io.kestra.core.queues.QueueFactoryInterface;
|
||||
import io.kestra.core.queues.QueueInterface;
|
||||
import io.kestra.core.utils.TestsUtils;
|
||||
import io.kestra.plugin.core.flow.EachSequentialTest;
|
||||
import io.kestra.plugin.core.flow.FlowCaseTest;
|
||||
import io.kestra.plugin.core.flow.ForEachItemCaseTest;
|
||||
@@ -23,20 +20,12 @@ import io.kestra.plugin.core.flow.WaitForCaseTest;
|
||||
import io.kestra.plugin.core.flow.WorkingDirectoryTest;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Named;
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestInstance;
|
||||
import org.junitpioneer.jupiter.RetryingTest;
|
||||
import org.slf4j.event.Level;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
@KestraTest(startRunner = true)
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
@@ -370,7 +359,7 @@ public abstract class AbstractRunnerTest {
|
||||
forEachItemCaseTest.forEachItemWithSubflowOutputs();
|
||||
}
|
||||
|
||||
@RetryingTest(5) // Flaky on CI but never locally even with 100 repetitions
|
||||
@Test
|
||||
@LoadFlows({"flows/valids/restart-for-each-item.yaml", "flows/valids/restart-child.yaml"})
|
||||
void restartForEachItem() throws Exception {
|
||||
forEachItemCaseTest.restartForEachItem();
|
||||
|
||||
@@ -21,6 +21,7 @@ import io.kestra.core.runners.FlowListeners;
|
||||
import io.kestra.core.utils.Await;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Named;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
@@ -525,6 +526,7 @@ public class SchedulerScheduleTest extends AbstractSchedulerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled("too flaky on CI")
|
||||
void recoverLASTLongRunningExecution() throws Exception {
|
||||
// mock flow listeners
|
||||
FlowListeners flowListenersServiceSpy = spy(this.flowListenersService);
|
||||
@@ -596,6 +598,7 @@ public class SchedulerScheduleTest extends AbstractSchedulerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled("too flaky on CI")
|
||||
void recoverNONELongRunningExecution() throws Exception {
|
||||
// mock flow listeners
|
||||
FlowListeners flowListenersServiceSpy = spy(this.flowListenersService);
|
||||
|
||||
@@ -205,6 +205,26 @@ class FlowServiceTest {
|
||||
assertThat(collect.stream().filter(flow -> flow.getId().equals("test3")).findFirst().orElseThrow().getRevision(), is(3));
|
||||
}
|
||||
|
||||
@Test
|
||||
void warnings() {
|
||||
Flow flow = create("test", "test", 1).toBuilder()
|
||||
.namespace("system")
|
||||
.triggers(List.of(
|
||||
io.kestra.plugin.core.trigger.Flow.builder()
|
||||
.id("flow-trigger")
|
||||
.type(io.kestra.plugin.core.trigger.Flow.class.getName())
|
||||
.build()
|
||||
))
|
||||
.build();
|
||||
|
||||
List<String> warnings = flowService.warnings(flow, null);
|
||||
|
||||
assertThat(warnings.size(), is(1));
|
||||
assertThat(warnings, containsInAnyOrder(
|
||||
"This flow will be triggered for EVERY execution of EVERY flow on your instance. We recommend adding the preconditions property to the Flow trigger 'flow-trigger'."
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
void aliases() {
|
||||
List<FlowService.Relocation> warnings = flowService.relocations("""
|
||||
|
||||
@@ -125,12 +125,13 @@ class MapUtilsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowExceptionWhenNestingMapGivenFlattenMapWithConflicts() {
|
||||
Assertions.assertThrows(IllegalArgumentException.class, () -> {
|
||||
MapUtils.flattenToNestedMap(Map.of(
|
||||
"k1.k2", "v1",
|
||||
"k1.k2.k3", "v2"
|
||||
));
|
||||
});
|
||||
void shouldReturnMapAndIgnoreConflicts() {
|
||||
Map<String, Object> results = MapUtils.flattenToNestedMap(Map.of(
|
||||
"k1.k2", "v1",
|
||||
"k1.k2.k3", "v2"
|
||||
));
|
||||
|
||||
assertThat(results, aMapWithSize(1));
|
||||
// due to ordering change on each JVM restart, the result map would be different as different entries will be skipped
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,7 @@ import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import static io.kestra.core.models.flows.State.Type.FAILED;
|
||||
import static io.kestra.core.utils.Rethrow.throwRunnable;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.greaterThan;
|
||||
@@ -136,7 +137,6 @@ public class ForEachItemCaseTest {
|
||||
Flux<Execution> receive = TestsUtils.receive(executionQueue, either -> {
|
||||
Execution execution = either.getLeft();
|
||||
if (execution.getFlowId().equals("for-each-item-subflow")) {
|
||||
log.info("Received sub-execution " + execution.getId() + " with status " + execution.getState().getCurrent());
|
||||
if (execution.getState().getCurrent().isTerminated()) {
|
||||
triggered.set(execution);
|
||||
countDownLatch.countDown();
|
||||
@@ -204,8 +204,9 @@ public class ForEachItemCaseTest {
|
||||
// assert on the main flow execution
|
||||
assertThat(execution.getTaskRunList(), hasSize(3));
|
||||
assertThat(execution.getTaskRunList().get(2).getAttempts(), hasSize(1));
|
||||
assertThat(execution.getTaskRunList().get(2).getAttempts().getFirst().getState().getCurrent(), is(State.Type.FAILED));
|
||||
assertThat(execution.getState().getCurrent(), is(State.Type.FAILED));
|
||||
assertThat(execution.getTaskRunList().get(2).getAttempts().getFirst().getState().getCurrent(), is(
|
||||
FAILED));
|
||||
assertThat(execution.getState().getCurrent(), is(FAILED));
|
||||
Map<String, Object> outputs = execution.getTaskRunList().get(2).getOutputs();
|
||||
assertThat(outputs.get("numberOfBatches"), is(26));
|
||||
assertThat(outputs.get("iterations"), notNullValue());
|
||||
@@ -215,7 +216,7 @@ public class ForEachItemCaseTest {
|
||||
assertThat(iterations.get("FAILED"), is(26));
|
||||
|
||||
// assert on the last subflow execution
|
||||
assertThat(triggered.get().getState().getCurrent(), is(State.Type.FAILED));
|
||||
assertThat(triggered.get().getState().getCurrent(), is(FAILED));
|
||||
assertThat(triggered.get().getFlowId(), is("for-each-item-subflow-failed"));
|
||||
assertThat((String) triggered.get().getInputs().get("items"), matchesRegex("kestra:///io/kestra/tests/for-each-item-failed/executions/.*/tasks/each-split/.*\\.txt"));
|
||||
assertThat(triggered.get().getTaskRunList(), hasSize(1));
|
||||
@@ -290,7 +291,7 @@ public class ForEachItemCaseTest {
|
||||
(flow, execution1) -> flowIO.readExecutionInputs(flow, execution1, inputs),
|
||||
Duration.ofSeconds(30));
|
||||
assertThat(execution.getTaskRunList(), hasSize(3));
|
||||
assertThat(execution.getState().getCurrent(), is(State.Type.FAILED));
|
||||
assertThat(execution.getState().getCurrent(), is(FAILED));
|
||||
|
||||
// here we must have 1 failed subflows
|
||||
assertTrue(countDownLatch.await(1, TimeUnit.MINUTES));
|
||||
@@ -303,11 +304,15 @@ public class ForEachItemCaseTest {
|
||||
successLatch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
//Wait before restarting until the failed execution tasks are persisted.
|
||||
Thread.sleep(1000L);
|
||||
|
||||
Execution restarted = executionService.restart(execution, null);
|
||||
execution = runnerUtils.awaitExecution(
|
||||
e -> e.getState().getCurrent() == State.Type.SUCCESS && e.getFlowId().equals("restart-for-each-item"),
|
||||
throwRunnable(() -> executionQueue.emit(restarted)),
|
||||
Duration.ofSeconds(10)
|
||||
Duration.ofSeconds(20)
|
||||
);
|
||||
assertThat(execution.getTaskRunList(), hasSize(4));
|
||||
assertTrue(successLatch.await(1, TimeUnit.MINUTES));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
version=0.22.0-SNAPSHOT
|
||||
version=0.22.0
|
||||
|
||||
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||
org.gradle.parallel=true
|
||||
org.gradle.caching=true
|
||||
org.gradle.priority=low
|
||||
org.gradle.priority=low
|
||||
|
||||
@@ -48,6 +48,7 @@ import java.util.*;
|
||||
import java.util.Comparator;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public abstract class AbstractJdbcExecutionRepository extends AbstractJdbcRepository implements ExecutionRepositoryInterface, JdbcQueueIndexerInterface<Execution> {
|
||||
private static final int FETCH_SIZE = 100;
|
||||
@@ -343,6 +344,28 @@ public abstract class AbstractJdbcExecutionRepository extends AbstractJdbcReposi
|
||||
return select;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<Execution> findAllAsync(@Nullable String tenantId) {
|
||||
return Flux.create(emitter -> this.jdbcRepository
|
||||
.getDslContextWrapper()
|
||||
.transaction(configuration -> {
|
||||
DSLContext context = DSL.using(configuration);
|
||||
|
||||
SelectConditionStep<Record1<Object>> select = context
|
||||
.select(field("value"))
|
||||
.hint(context.configuration().dialect().supports(SQLDialect.MYSQL) ? "SQL_CALC_FOUND_ROWS" : null)
|
||||
.from(this.jdbcRepository.getTable())
|
||||
.where(this.defaultFilter(tenantId));
|
||||
|
||||
try (Stream<Record1<Object>> stream = select.fetchSize(FETCH_SIZE).stream()){
|
||||
stream.map((Record record) -> jdbcRepository.map(record))
|
||||
.forEach(emitter::next);
|
||||
} finally {
|
||||
emitter.complete();
|
||||
}
|
||||
}), FluxSink.OverflowStrategy.BUFFER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ArrayListTotal<Execution> findByFlowId(String tenantId, String namespace, String id, Pageable pageable) {
|
||||
return this.jdbcRepository
|
||||
|
||||
@@ -183,6 +183,28 @@ public abstract class AbstractJdbcLogRepository extends AbstractJdbcRepository i
|
||||
}), FluxSink.OverflowStrategy.BUFFER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<LogEntry> findAllAsync(@Nullable String tenantId) {
|
||||
return Flux.create(emitter -> this.jdbcRepository
|
||||
.getDslContextWrapper()
|
||||
.transaction(configuration -> {
|
||||
DSLContext context = DSL.using(configuration);
|
||||
|
||||
SelectConditionStep<Record1<Object>> select = context
|
||||
.select(field("value"))
|
||||
.hint(context.configuration().dialect().supports(SQLDialect.MYSQL) ? "SQL_CALC_FOUND_ROWS" : null)
|
||||
.from(this.jdbcRepository.getTable())
|
||||
.where(this.defaultFilter(tenantId));
|
||||
|
||||
try (Stream<Record1<Object>> stream = select.fetchSize(FETCH_SIZE).stream()){
|
||||
stream.map((Record record) -> jdbcRepository.map(record))
|
||||
.forEach(emitter::next);
|
||||
} finally {
|
||||
emitter.complete();
|
||||
}
|
||||
}), FluxSink.OverflowStrategy.BUFFER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<LogStatistics> statistics(
|
||||
@Nullable String query,
|
||||
|
||||
@@ -18,6 +18,8 @@ import lombok.Getter;
|
||||
import org.jooq.Record;
|
||||
import org.jooq.*;
|
||||
import org.jooq.impl.DSL;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.FluxSink;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.ZoneId;
|
||||
@@ -27,6 +29,7 @@ import java.time.temporal.ChronoUnit;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public abstract class AbstractJdbcMetricRepository extends AbstractJdbcRepository implements MetricRepositoryInterface {
|
||||
protected io.kestra.jdbc.AbstractJdbcRepository<MetricEntry> jdbcRepository;
|
||||
@@ -92,6 +95,28 @@ public abstract class AbstractJdbcMetricRepository extends AbstractJdbcRepositor
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<MetricEntry> findAllAsync(@io.micronaut.core.annotation.Nullable String tenantId) {
|
||||
return Flux.create(emitter -> this.jdbcRepository
|
||||
.getDslContextWrapper()
|
||||
.transaction(configuration -> {
|
||||
DSLContext context = DSL.using(configuration);
|
||||
|
||||
SelectConditionStep<Record1<Object>> select = context
|
||||
.select(field("value"))
|
||||
.hint(context.configuration().dialect().supports(SQLDialect.MYSQL) ? "SQL_CALC_FOUND_ROWS" : null)
|
||||
.from(this.jdbcRepository.getTable())
|
||||
.where(this.defaultFilter(tenantId));
|
||||
|
||||
try (Stream<Record1<Object>> stream = select.fetchSize(FETCH_SIZE).stream()){
|
||||
stream.map((Record record) -> jdbcRepository.map(record))
|
||||
.forEach(emitter::next);
|
||||
} finally {
|
||||
emitter.complete();
|
||||
}
|
||||
}), FluxSink.OverflowStrategy.BUFFER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> flowMetrics(
|
||||
String tenantId,
|
||||
|
||||
@@ -49,6 +49,7 @@ dependencies {
|
||||
// ugly hack on crypto plugin
|
||||
api("org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion")
|
||||
api("org.bouncycastle:bcpg-jdk18on:$bouncycastleVersion")
|
||||
api("org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion")
|
||||
// ugly hack for jackson: as enforcing platform didn't work (it didn't enforce everywhere, not in plugins), we had to force all jackson libs individually.
|
||||
api("com.fasterxml.jackson.core:jackson-core:$jacksonVersion")
|
||||
api("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion")
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 27.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 439 309" style="enable-background:new 0 0 439 309;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#1D63ED;}
|
||||
</style>
|
||||
<path class="st0" d="M379.6,111.7c-2.3-16.7-11.5-31.2-28.1-44.3l-9.6-6.5l-6.4,9.7c-8.2,12.5-12.3,29.9-11,46.6
|
||||
c0.6,5.8,2.5,16.4,8.4,25.5c-5.9,3.3-17.6,7.7-33.2,7.4H1.7l-0.6,3.5c-2.8,16.7-2.8,69,30.7,109.1c25.5,30.5,63.6,46,113.4,46
|
||||
c108,0,187.8-50.3,225.3-141.9c14.7,0.3,46.4,0.1,62.7-31.4c0.4-0.7,1.4-2.6,4.2-8.6l1.6-3.3l-9.1-6.2
|
||||
C419.9,110.8,397.2,108.3,379.6,111.7L379.6,111.7z M240,0h-45.3v41.7H240V0z M240,50.1h-45.3v41.7H240V50.1z M186.4,50.1h-45.3
|
||||
v41.7h45.3V50.1z M132.9,50.1H87.6v41.7h45.3V50.1z M79.3,100.2H34v41.7h45.3V100.2z M132.9,100.2H87.6v41.7h45.3V100.2z
|
||||
M186.4,100.2h-45.3v41.7h45.3V100.2z M240,100.2h-45.3v41.7H240V100.2z M293.6,100.2h-45.3v41.7h45.3V100.2z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -16,9 +16,6 @@
|
||||
import Utils from "../utils/utils";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<span v-if="required" class="me-1 text-danger">*</span>
|
||||
<span v-if="label" class="label">{{ label }}</span>
|
||||
<label v-if="label" class="label" :for="uid">{{ label }}</label>
|
||||
<div class="mt-1 mb-2 wrapper" :class="props.class">
|
||||
<el-input
|
||||
v-model="input"
|
||||
@input="handleInput"
|
||||
:id="uid"
|
||||
:placeholder
|
||||
:disabled
|
||||
type="textarea"
|
||||
@@ -14,10 +14,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, watch} from "vue";
|
||||
import {useId, computed} from "vue";
|
||||
|
||||
defineOptions({inheritAttrs: false});
|
||||
|
||||
const uid = useId();
|
||||
|
||||
const emits = defineEmits(["update:modelValue"]);
|
||||
const props = defineProps({
|
||||
modelValue: {type: [String, Number, Boolean], default: undefined},
|
||||
@@ -28,20 +30,12 @@
|
||||
class: {type: String, default: undefined},
|
||||
});
|
||||
|
||||
const input = ref(props.modelValue);
|
||||
|
||||
const handleInput = (value: string) => {
|
||||
emits("update:modelValue", value);
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
if (newValue !== input.value) {
|
||||
input.value = newValue;
|
||||
}
|
||||
const input = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => {
|
||||
emits("update:modelValue", value);
|
||||
},
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -112,11 +112,12 @@
|
||||
>
|
||||
<PluginDocumentation
|
||||
v-if="currentView === views.DOC"
|
||||
class="plugin-doc combined-right-view enhance-readability"
|
||||
class="combined-right-view enhance-readability"
|
||||
:override-intro="intro"
|
||||
absolute
|
||||
/>
|
||||
<div
|
||||
class="d-flex justify-content-center align-items-center w-100 p-5"
|
||||
class="d-flex justify-content-center align-items-center w-100 p-3"
|
||||
v-else-if="currentView === views.CHART"
|
||||
>
|
||||
<div v-if="selectedChart" class="w-100">
|
||||
@@ -129,7 +130,7 @@
|
||||
<small>{{ selectedChart.chartOptions.description }}</small>
|
||||
</p>
|
||||
|
||||
<div class="w-100">
|
||||
<div :style="`position: relative; width:calc(${100}% - 10px)`">
|
||||
<component
|
||||
:key="selectedChart.id"
|
||||
:is="types[selectedChart.type]"
|
||||
@@ -171,6 +172,8 @@
|
||||
defineEmits(["save"])
|
||||
</script>
|
||||
<script>
|
||||
import {shallowRef} from "vue";
|
||||
|
||||
import Editor from "../../inputs/Editor.vue";
|
||||
import yaml from "yaml";
|
||||
import ContentSave from "vue-material-design-icons/ContentSave.vue";
|
||||
@@ -312,11 +315,11 @@
|
||||
charts: [],
|
||||
chartError: null,
|
||||
types: {
|
||||
"io.kestra.plugin.core.dashboard.chart.TimeSeries": TimeSeries,
|
||||
"io.kestra.plugin.core.dashboard.chart.Bar": Bar,
|
||||
"io.kestra.plugin.core.dashboard.chart.Markdown": Markdown,
|
||||
"io.kestra.plugin.core.dashboard.chart.Table": Table,
|
||||
"io.kestra.plugin.core.dashboard.chart.Pie": Pie,
|
||||
"io.kestra.plugin.core.dashboard.chart.TimeSeries": shallowRef(TimeSeries),
|
||||
"io.kestra.plugin.core.dashboard.chart.Bar": shallowRef(Bar),
|
||||
"io.kestra.plugin.core.dashboard.chart.Markdown": shallowRef(Markdown),
|
||||
"io.kestra.plugin.core.dashboard.chart.Table": shallowRef(Table),
|
||||
"io.kestra.plugin.core.dashboard.chart.Pie": shallowRef(Pie),
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
import Pencil from "vue-material-design-icons/Pencil.vue";
|
||||
import Plus from "vue-material-design-icons/Plus.vue";
|
||||
import ViewDashboardEdit from "vue-material-design-icons/ViewDashboardEdit.vue";
|
||||
import useRouteContext from "../../../mixins/useRouteContext.js";
|
||||
|
||||
const store = useStore();
|
||||
const {t} = useI18n({useScope: "global"});
|
||||
@@ -72,4 +73,6 @@
|
||||
const routeInfo = computed(() => ({
|
||||
title: props.title ?? t("homeDashboard.title"),
|
||||
}));
|
||||
|
||||
useRouteContext(routeInfo);
|
||||
</script>
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
|
||||
defineOptions({inheritAttrs: false});
|
||||
const props = defineProps({
|
||||
identifier: {type: Number, required: true},
|
||||
identifier: {type: [Number, String], required: true},
|
||||
chart: {type: Object, required: true},
|
||||
isPreview: {type: Boolean, required: false, default: false}
|
||||
});
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
|
||||
defineOptions({inheritAttrs: false});
|
||||
const props = defineProps({
|
||||
identifier: {type: Number, required: true},
|
||||
identifier: {type: [Number, String], required: true},
|
||||
chart: {type: Object, required: true},
|
||||
isPreview: {type: Boolean, required: false, default: false}
|
||||
});
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
|
||||
defineOptions({inheritAttrs: false});
|
||||
const props = defineProps({
|
||||
identifier: {type: Number, required: true},
|
||||
identifier: {type: [Number, String], required: true},
|
||||
chart: {type: Object, required: true},
|
||||
isPreview: {type: Boolean, required: false, default: false}
|
||||
});
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
|
||||
defineOptions({inheritAttrs: false});
|
||||
const props = defineProps({
|
||||
identifier: {type: Number, required: true},
|
||||
identifier: {type: [Number, String], required: true},
|
||||
chart: {type: Object, required: true},
|
||||
isPreview: {type: Boolean, required: false, default: false}
|
||||
});
|
||||
|
||||
34
ui/src/components/demo/DemoButtons.vue
Normal file
34
ui/src/components/demo/DemoButtons.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div>
|
||||
<a class="el-button el-button--large el-button--primary" target="_blank" :href="getADemoUrl.href">
|
||||
{{ $t("demos.get_a_demo_button") }}
|
||||
</a>
|
||||
<el-button size="large" @click="store.commit('misc/setContextInfoBarOpenTab', 'docs')">
|
||||
Learn More
|
||||
<el-icon class="el-icon--right">
|
||||
<ArrowRightIcon />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ArrowRightIcon from "vue-material-design-icons/ArrowRight.vue";
|
||||
import {useRoute} from "vue-router";
|
||||
import {useStore} from "vuex";
|
||||
import {computed} from "vue";
|
||||
|
||||
const store = useStore();
|
||||
const route = useRoute();
|
||||
|
||||
const getADemoUrl = computed(() => {
|
||||
const demoUrl = new URL("https://kestra.io/demo");
|
||||
// set all utm params from the route query
|
||||
for (const [key, value] of Object.entries(route.query)) {
|
||||
if (key.startsWith("utm_")) {
|
||||
demoUrl.searchParams.set(key, value as string);
|
||||
}
|
||||
}
|
||||
return demoUrl;
|
||||
});
|
||||
</script>
|
||||
@@ -8,39 +8,14 @@
|
||||
</div>
|
||||
<h2>{{ title }}</h2>
|
||||
<p><slot name="message" /></p>
|
||||
<a class="el-button el-button--large el-button--primary" target="_blank" :href="getADemoUrl.href">
|
||||
{{ $t("demos.get_a_demo_button") }}
|
||||
</a>
|
||||
<el-button size="large" @click="store.commit('misc/setContextInfoBarOpenTab', 'docs')">
|
||||
Learn More
|
||||
<el-icon class="el-icon--right">
|
||||
<ArrowRightIcon />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<DemoButtons />
|
||||
</div>
|
||||
</EmptyTemplate>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {computed} from "vue";
|
||||
import {useStore} from "vuex";
|
||||
import {useRoute} from "vue-router";
|
||||
import ArrowRightIcon from "vue-material-design-icons/ArrowRight.vue";
|
||||
import EmptyTemplate from "../layout/EmptyTemplate.vue";
|
||||
|
||||
const store = useStore();
|
||||
const route = useRoute();
|
||||
|
||||
const getADemoUrl = computed(() => {
|
||||
const demoUrl = new URL("https://kestra.io/demo");
|
||||
// set all utm params from the route query
|
||||
for (const [key, value] of Object.entries(route.query)) {
|
||||
if (key.startsWith("utm_")) {
|
||||
demoUrl.searchParams.set(key, value as string);
|
||||
}
|
||||
}
|
||||
return demoUrl;
|
||||
});
|
||||
import DemoButtons from "./DemoButtons.vue";
|
||||
|
||||
defineProps<{
|
||||
title: string;
|
||||
|
||||
@@ -76,6 +76,7 @@
|
||||
<editor
|
||||
ref="debugEditor"
|
||||
:full-height="false"
|
||||
:custom-height="20"
|
||||
:input="true"
|
||||
:navbar="false"
|
||||
:model-value="computedDebugValue"
|
||||
@@ -100,6 +101,7 @@
|
||||
:read-only="true"
|
||||
:input="true"
|
||||
:full-height="false"
|
||||
:custom-height="20"
|
||||
:navbar="false"
|
||||
:model-value="debugExpression"
|
||||
:lang="isJSON ? 'json' : ''"
|
||||
|
||||
@@ -201,7 +201,7 @@
|
||||
|
||||
import {useI18n} from "vue-i18n";
|
||||
import {useStore} from "vuex";
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
import {useRoute, useRouter, LocationQueryRaw} from "vue-router";
|
||||
import {useFilters} from "./composables/useFilters";
|
||||
import action from "../../models/action";
|
||||
import permission from "../../models/permission";
|
||||
@@ -632,21 +632,9 @@
|
||||
|
||||
watch(
|
||||
() => route.query,
|
||||
(q: any) => {
|
||||
// Handling change of label filters from direct click events
|
||||
if (
|
||||
Object.keys(q).length === 0 ||
|
||||
Object.keys(q).some((key) => key.startsWith("filters[labels]"))
|
||||
) {
|
||||
const routeFilters = decodeParams(
|
||||
route.name,
|
||||
q,
|
||||
props.include,
|
||||
OPTIONS,
|
||||
props.isDefaultDashboard
|
||||
);
|
||||
currentFilters.value = routeFilters;
|
||||
}
|
||||
(q: LocationQueryRaw) => {
|
||||
const routeFilters = decodeParams(route.name, q, props.include, OPTIONS, props.isDefaultDashboard) as CurrentItem[];
|
||||
currentFilters.value = routeFilters;
|
||||
},
|
||||
{immediate: true},
|
||||
);
|
||||
@@ -736,7 +724,7 @@
|
||||
|
||||
const handleClickedItems = (value) => {
|
||||
if (value) currentFilters.value = value;
|
||||
select.value?.focus();
|
||||
triggerSearch();
|
||||
};
|
||||
|
||||
const triggerSearch = () => {
|
||||
|
||||
@@ -41,14 +41,20 @@
|
||||
class="me-2"
|
||||
>
|
||||
<span class="small">
|
||||
<Label :option="value" />
|
||||
<Label :option="value" :prefix="props.prefix" />
|
||||
</span>
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-auto">
|
||||
<DeleteOutline @click.stop="remove(index)" />
|
||||
<KestraIcon
|
||||
@click.stop="remove(index)"
|
||||
:tooltip="$t('filters.save.remove')"
|
||||
placement="right"
|
||||
>
|
||||
<DeleteOutline />
|
||||
</KestraIcon>
|
||||
</div>
|
||||
</div>
|
||||
</el-dropdown-item>
|
||||
@@ -96,10 +102,15 @@
|
||||
@import "../styles/filter";
|
||||
|
||||
.dropdown {
|
||||
width: 400px;
|
||||
width: 800px;
|
||||
|
||||
&:hover {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.items {
|
||||
background-color: var(--el-bg-color-overlay);
|
||||
max-height: 170px !important; // 5 visible items
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -15,8 +15,9 @@ $filters-font-xs: var(--el-font-size-extra-small);
|
||||
|
||||
.items {
|
||||
.el-tag {
|
||||
background: $filters-border-color;
|
||||
color: $filters-gray-900;
|
||||
padding: 0;
|
||||
color: var(--ks-tag-content);
|
||||
background: var(--ks-tag-background-active);
|
||||
}
|
||||
|
||||
.small {
|
||||
|
||||
@@ -121,22 +121,22 @@ export const encodeSearchParams = (filters, OPTIONS) => {
|
||||
};
|
||||
|
||||
return filters.reduce((query, filter) => {
|
||||
if(filter.field === "labels" && filter.operation) {
|
||||
Object.assign(query, encode(filter.value, "labels", "EQUALS"));
|
||||
}
|
||||
if(filter.operation) {
|
||||
Object.assign(query, encode(filter.value, filter.field, filter.operation));
|
||||
} else {
|
||||
const match = OPTIONS.find((o) => o.value.label === filter.label);
|
||||
const key = match ? match.key : filter.label === "text" ? "q" : null;
|
||||
const operation = filter.comparator?.value || match?.comparators?.find(c => c.value === filter.operation)?.value || "EQUALS";
|
||||
|
||||
const match = OPTIONS.find((o) => o.value.label === filter.label);
|
||||
const key = match ? match.key : filter.label === "text" ? "q" : null;
|
||||
const operation = filter.comparator?.value || match?.comparators?.find(c => c.value === filter.operation)?.value || "EQUALS";
|
||||
|
||||
if (key) {
|
||||
if (key !== "date") {
|
||||
Object.assign(query, encode(filter.value, key, operation));
|
||||
} else if (filter.value?.length > 0) {
|
||||
const {startDate, endDate} = filter.value[0];
|
||||
if(startDate && endDate) {
|
||||
query["filters[startDate][GREATER_THAN_OR_EQUAL_TO]"] = startDate;
|
||||
query["filters[endDate][LESS_THAN_OR_EQUAL_TO]"] = endDate;
|
||||
if (key) {
|
||||
if (key !== "date") {
|
||||
Object.assign(query, encode(filter.value, key, operation));
|
||||
} else if (filter.value?.length > 0) {
|
||||
const {startDate, endDate} = filter.value[0];
|
||||
if(startDate && endDate) {
|
||||
query["filters[startDate][GREATER_THAN_OR_EQUAL_TO]"] = startDate;
|
||||
query["filters[endDate][LESS_THAN_OR_EQUAL_TO]"] = endDate;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
</el-card>
|
||||
<template v-if="blueprint.description">
|
||||
<h4>{{ $t('about_this_blueprint') }}</h4>
|
||||
<div v-if="!system" class="tags text-uppercase">
|
||||
<div class="tags text-uppercase">
|
||||
<div v-for="(tag, index) in blueprint.tags" :key="index" class="tag-box">
|
||||
<el-tag type="info" size="small">
|
||||
{{ tag }}
|
||||
@@ -130,6 +130,10 @@
|
||||
type: String,
|
||||
default: "flow",
|
||||
},
|
||||
combinedView: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
goBack() {
|
||||
@@ -153,7 +157,11 @@
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
this.$store.dispatch("blueprints/getBlueprint", {type: this.$route.params.tab, kind: this.blueprintKind, id: this.blueprintId})
|
||||
this.$store.dispatch("blueprints/getBlueprint", {
|
||||
type: this.combinedView ? this.blueprintType : this.$route.params.tab,
|
||||
kind: this.blueprintKind,
|
||||
id: this.blueprintId
|
||||
})
|
||||
.then(data => {
|
||||
this.blueprint = data;
|
||||
if (this.kind === "flow") {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</el-col>
|
||||
<el-col :span="20">
|
||||
<InputText
|
||||
:model-value="YamUtils.stringify(element)"
|
||||
:model-value="element"
|
||||
@update:model-value="(v) => handleInput(v, index)"
|
||||
:placeholder="$t('value')"
|
||||
class="w-100"
|
||||
@@ -35,7 +35,11 @@
|
||||
defineOptions({inheritAttrs: false});
|
||||
|
||||
const emits = defineEmits(["update:modelValue"]);
|
||||
const props = defineProps({modelValue: {type: Array, default: undefined}});
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue?: (string | number | boolean | undefined)[]
|
||||
}>(), {
|
||||
modelValue: undefined
|
||||
});
|
||||
|
||||
const items = ref(
|
||||
!Array.isArray(props.modelValue) ? [props.modelValue] : props.modelValue,
|
||||
|
||||
@@ -37,11 +37,14 @@
|
||||
import {computed, getCurrentInstance} from "vue";
|
||||
import {useStore} from "vuex"
|
||||
import {useRouter, useRoute} from "vue-router";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import EditorButtons from "./EditorButtons.vue";
|
||||
import ValidationError from "../flows/ValidationError.vue";
|
||||
|
||||
import localUtils from "../../utils/utils";
|
||||
|
||||
const {t} = useI18n();
|
||||
|
||||
const exportYaml = () => {
|
||||
const blob = new Blob([store.getters["flow/flowYaml"]], {type: "text/yaml"});
|
||||
localUtils.downloadUrl(window.URL.createObjectURL(blob), "flow.yaml");
|
||||
@@ -57,12 +60,33 @@
|
||||
const isAllowedEdit = computed(() => store.getters["flow/isAllowedEdit"])
|
||||
const flowHaveTasks = computed(() => store.getters["flow/flowHaveTasks"])
|
||||
const flowErrors = computed(() => store.getters["flow/flowErrors"])
|
||||
const flowWarnings = computed(() => store.getters["flow/flowWarnings"])
|
||||
const flowInfos = computed(() => store.getters["flow/flowInfos"])
|
||||
const flowParsed = computed(() => store.getters["flow/flow"])
|
||||
const tabs = computed<{dirty:boolean}[]>(() => store.state.editor.tabs)
|
||||
const metadata = computed(() => store.state.flow.metadata);
|
||||
const toast = getCurrentInstance().appContext.config.globalProperties.$toast();
|
||||
const toast = getCurrentInstance()?.appContext.config.globalProperties.$toast();
|
||||
const flowWarnings = computed(() => {
|
||||
|
||||
const outdatedWarning =
|
||||
store.state.flow.flowValidation?.outdated && !store.state.flow.isCreating
|
||||
? [store.getters["flow/outdatedMessage"]]
|
||||
: [];
|
||||
|
||||
const deprecationWarnings =
|
||||
store.state.flow.flowValidation?.deprecationPaths?.map(
|
||||
(f: string) => `${f} ${t("is deprecated")}.`
|
||||
) ?? [];
|
||||
|
||||
const otherWarnings = store.state.flow.flowValidation?.warnings ?? [];
|
||||
|
||||
const warnings = [
|
||||
...outdatedWarning,
|
||||
...deprecationWarnings,
|
||||
...otherWarnings,
|
||||
];
|
||||
|
||||
return warnings.length === 0 ? undefined : warnings;
|
||||
});
|
||||
|
||||
async function save(){
|
||||
await store.dispatch("flow/saveAll")
|
||||
|
||||
@@ -1148,7 +1148,7 @@
|
||||
height: 30px;
|
||||
padding: 16px;
|
||||
font-size: var(--el-font-size-small);
|
||||
color: $gray-900;
|
||||
color: var(--ks-content-primary);
|
||||
|
||||
&:hover {
|
||||
color: var(--ks-content-secondary);
|
||||
|
||||
@@ -250,7 +250,7 @@
|
||||
v-if="viewType === editorViewTypes.SOURCE_BLUEPRINTS"
|
||||
class="combined-right-view enhance-readability"
|
||||
>
|
||||
<Blueprints @loaded="blueprintsLoaded = true" embed kind="flow" />
|
||||
<Blueprints @loaded="blueprintsLoaded = true" embed kind="flow" combined-view />
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -560,7 +560,31 @@
|
||||
|
||||
const baseOutdatedTranslationKey = computed(() => store.getters["flow/baseOutdatedTranslationKey"]);
|
||||
const flowErrors = computed(() => store.getters["flow/flowErrors"]);
|
||||
const flowWarnings = computed(() => store.getters["flow/flowWarnings"]);
|
||||
const flowWarnings = computed(() => {
|
||||
if (isFlow.value) {
|
||||
const outdatedWarning =
|
||||
store.state.flow.flowValidation?.outdated && !store.state.flow.isCreating
|
||||
? [store.getters["flow/outdatedMessage"]]
|
||||
: [];
|
||||
|
||||
const deprecationWarnings =
|
||||
store.state.flow.flowValidation?.deprecationPaths?.map(
|
||||
(f) => `${f} ${t("is deprecated")}.`
|
||||
) ?? [];
|
||||
|
||||
const otherWarnings = store.state.flow.flowValidation?.warnings ?? [];
|
||||
|
||||
const warnings = [
|
||||
...outdatedWarning,
|
||||
...deprecationWarnings,
|
||||
...otherWarnings,
|
||||
];
|
||||
|
||||
return warnings.length === 0 ? undefined : warnings;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
});
|
||||
const flowInfos = computed(() => store.getters["flow/flowInfos"]);
|
||||
const flowHaveTasks = computed(() => store.getters["flow/flowHaveTasks"]);
|
||||
|
||||
@@ -862,11 +886,11 @@
|
||||
|
||||
const onUpdateMetadata = (event, shouldSave) => {
|
||||
if(shouldSave) {
|
||||
metadata.value = {...metadata.value, ...(event.concurrency?.limit === 0 ? {concurrency: null} : event)};
|
||||
onSaveMetadata();
|
||||
validateFlow(flowYaml.value)
|
||||
store.commit("flow/setMetadata", {...metadata.value, ...(event.concurrency?.limit === 0 ? {concurrency: null} : event)});
|
||||
store.dispatch("flow/onSaveMetadata");
|
||||
store.dispatch("flow/validateFlow", {flow: flowYaml.value});
|
||||
} else {
|
||||
metadata.value = event.concurrency?.limit === 0 ? {concurrency: null} : event;
|
||||
store.commit("flow/setMetadata", event.concurrency?.limit === 0 ? {concurrency: null} : event);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -875,22 +899,6 @@
|
||||
isEditMetadataOpen.value = false;
|
||||
};
|
||||
|
||||
const validateFlow = (flow) => {
|
||||
if(!flow) return;
|
||||
|
||||
return store
|
||||
.dispatch("flow/validateFlow", {flow})
|
||||
.then((value) => {
|
||||
if (validationDomElement.value && editorDomElement.value) {
|
||||
validationDomElement.value.onResize(
|
||||
editorDomElement.value.$el.offsetWidth
|
||||
);
|
||||
}
|
||||
|
||||
return value;
|
||||
});
|
||||
};
|
||||
|
||||
const handleReorder = (yaml) => {
|
||||
store.commit("flow/setFlowYaml", yaml);
|
||||
store.commit("flow/setHaveChange", true)
|
||||
@@ -953,7 +961,7 @@
|
||||
|
||||
const save = async () => {
|
||||
const result = await store.dispatch("flow/save", {
|
||||
content: editorDomElement.value.$refs.monacoEditor.value,
|
||||
content: editorDomElement.value?.$refs.monacoEditor.value ?? flowYaml.value,
|
||||
})
|
||||
if(result === "redirect_to_update"){
|
||||
await router.push({
|
||||
|
||||
@@ -209,7 +209,7 @@
|
||||
this.initMonaco(monaco)
|
||||
})
|
||||
|
||||
if (!this.monacoYamlConfigured && (this.creating || this.current?.flow)) {
|
||||
if (!this.monacoYamlConfigured && this.language === "yaml") {
|
||||
this.$store.commit("core/setMonacoYamlConfigured", true);
|
||||
configureMonacoYaml(monaco, {
|
||||
enableSchemaRequest: true,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<KestraFilter :placeholder="$t('search')" :search-callback="(input) => search = input" :decode="false" />
|
||||
<section class="d-inline-flex mb-3 filters">
|
||||
<el-input v-model="search" :placeholder="$t('search')" />
|
||||
</section>
|
||||
|
||||
<select-table
|
||||
:data="filteredKvs"
|
||||
@@ -190,7 +192,6 @@
|
||||
import ContentSave from "vue-material-design-icons/ContentSave.vue";
|
||||
import TimeSelect from "../executions/date-select/TimeSelect.vue";
|
||||
import Check from "vue-material-design-icons/Check.vue";
|
||||
import KestraFilter from "../filter/KestraFilter.vue";
|
||||
import NamespaceSelect from "../namespace/NamespaceSelect.vue";
|
||||
|
||||
import Utils from "../../utils/utils";
|
||||
@@ -258,6 +259,13 @@
|
||||
if (this.$refs.form) {
|
||||
this.$refs.form.clearValidate("value");
|
||||
}
|
||||
},
|
||||
search(newValue) {
|
||||
if (newValue !== undefined) {
|
||||
this.$router.push({query: {
|
||||
q: newValue
|
||||
}})
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -272,7 +280,7 @@
|
||||
},
|
||||
kvs: undefined,
|
||||
namespaceIterator: undefined,
|
||||
search: "",
|
||||
search: this.$route.query?.q ?? "",
|
||||
rules: {
|
||||
key: [
|
||||
{required: true, trigger: "change"},
|
||||
@@ -411,13 +419,15 @@
|
||||
});
|
||||
});
|
||||
},
|
||||
reloadKvs() {
|
||||
async reloadKvs() {
|
||||
this.namespaceIterator = undefined;
|
||||
|
||||
const previousLength = this.secrets?.length ?? 0;
|
||||
await this.$refs.selectTable.resetInfiniteScroll();
|
||||
this.kvs = [];
|
||||
this.$refs.selectTable.resetInfiniteScroll();
|
||||
|
||||
// If we are in the global KV view we let the infinite scroll handling the fetch
|
||||
if (this.namespace !== undefined) {
|
||||
if (this.namespace !== undefined || previousLength === 0) {
|
||||
this.fetchKvs();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -24,9 +24,11 @@
|
||||
import Navbar from "../layout/TopNavBar.vue";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import {computed, ref} from "vue";
|
||||
import useRouteContext from "../../mixins/useRouteContext.js";
|
||||
|
||||
const addKvModalVisible = ref(false);
|
||||
|
||||
const {t} = useI18n({useScope: "global"});
|
||||
const routeInfo = computed(() => ({title: t("kv.name")}));
|
||||
useRouteContext(routeInfo);
|
||||
</script>
|
||||
@@ -25,12 +25,9 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {default as vElTableInfiniteScroll} from "el-table-infinite-scroll";
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import NoData from "./NoData.vue";
|
||||
import elTableInfiniteScroll from "el-table-infinite-scroll";
|
||||
|
||||
export default {
|
||||
components: {NoData},
|
||||
@@ -61,9 +58,13 @@
|
||||
return this.infiniteScrollDisabled === false;
|
||||
}
|
||||
},
|
||||
directives: {
|
||||
elTableInfiniteScroll
|
||||
},
|
||||
methods: {
|
||||
async resetInfiniteScroll() {
|
||||
this.infiniteScrollDisabled = false;
|
||||
this.tableHeight = await this.computeTableHeight();
|
||||
},
|
||||
async waitTableRender() {
|
||||
if (this.tableView === undefined) {
|
||||
@@ -82,19 +83,9 @@
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(this.tableView.querySelector(".el-table__body > tbody"), {childList: true,});
|
||||
observer.observe(this.tableView.querySelector(".el-table__body > tbody"), {childList: true});
|
||||
});
|
||||
},
|
||||
async hasScrollbar() {
|
||||
const scrollEl = this.scrollWrapper;
|
||||
if (scrollEl === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await this.waitTableRender();
|
||||
|
||||
return scrollEl.clientHeight < scrollEl.scrollHeight;
|
||||
},
|
||||
selectionChanged(selection) {
|
||||
this.hasSelection = selection.length > 0;
|
||||
this.$emit("selection-change", selection);
|
||||
@@ -114,6 +105,10 @@
|
||||
return "auto";
|
||||
}
|
||||
|
||||
if (!this.stillHaveDataToFetch && this.data.length === 0) {
|
||||
return "calc(var(--table-header-height) + 60px)";
|
||||
}
|
||||
|
||||
return this.stillHaveDataToFetch || this.tableView === undefined ? "100%" : `min(${this.tableView.scrollHeight}px, 100%)`;
|
||||
},
|
||||
async infiniteScrollLoadWithDisableHandling() {
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
import {useRoute} from "vue-router";
|
||||
import useNamespaces, {Namespace} from "../../composables/useNamespaces";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import useRouteContext from "../../mixins/useRouteContext.ts";
|
||||
|
||||
const {t} = useI18n({useScope: "global"});
|
||||
|
||||
@@ -74,6 +75,8 @@
|
||||
}
|
||||
|
||||
const routeInfo = computed(() => ({title: t("namespaces")}));
|
||||
useRouteContext(routeInfo);
|
||||
|
||||
const user = computed(() => store.state.auth.user);
|
||||
const isUserEmpty = computed(() => Object.keys(user.value).length === 0);
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
</schema-to-html>
|
||||
</Suspense>
|
||||
</template>
|
||||
<markdown v-else :source="introContent" />
|
||||
<markdown v-else :source="introContent" :class="{'position-absolute': absolute}" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
overrideIntro: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
absolute: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
||||
@@ -10,23 +10,57 @@
|
||||
</ul>
|
||||
</template>
|
||||
</Navbar>
|
||||
<section data-component="FILENAME_PLACEHOLDER" class="d-flex flex-column fill-height container padding-bottom" :class="configs?.secretsEnabled === undefined ? 'min-w-auto ms-auto me-auto' : ''">
|
||||
<template v-if="configs?.secretsEnabled === undefined">
|
||||
<Layout
|
||||
:title="$t('demos.secrets.title')"
|
||||
:image="{source: sourceImg, alt: $t('demos.secrets.title')}"
|
||||
>
|
||||
<template #message>
|
||||
{{ $t('demos.secrets.message') }}
|
||||
</template>
|
||||
</Layout>
|
||||
<el-divider />
|
||||
<p>Here are secret-type environment variables identified at instance start-time:</p>
|
||||
</template>
|
||||
<section
|
||||
data-component="FILENAME_PLACEHOLDER"
|
||||
class="d-flex flex-column fill-height padding-bottom"
|
||||
:class="configs?.secretsEnabled === undefined ? 'mt-0 p-0' : 'container'"
|
||||
>
|
||||
<EmptyTemplate v-if="configs?.secretsEnabled === undefined" class="d-flex flex-column text-start m-0 p-0 mw-100">
|
||||
<div class="no-secret-manager-block d-flex flex-column gap-6">
|
||||
<div class="header-block d-flex align-items-center">
|
||||
<div class="d-flex flex-column">
|
||||
<h5 class="mb-3">
|
||||
{{ $t('demos.secrets.title') }}
|
||||
</h5>
|
||||
<p>{{ $t('demos.secrets.message') }}</p>
|
||||
<DemoButtons />
|
||||
</div>
|
||||
<div class="img-wrapper">
|
||||
<img :src="sourceImg" :alt="$t('demos.secrets.title')">
|
||||
</div>
|
||||
</div>
|
||||
<p class="mb-0">
|
||||
{{ $t('demos.secrets.detected_env') }}
|
||||
</p>
|
||||
<div v-if="hasData === false">
|
||||
<p class="text-tertiary mb-4">
|
||||
{{ $t('demos.secrets.empty_env') }}
|
||||
</p>
|
||||
<div class="text-secondary">
|
||||
<p class="bold mb-0">
|
||||
{{ $t('demos.secrets.add_env.intro') }}
|
||||
</p>
|
||||
<ul>
|
||||
<li v-html="$t('demos.secrets.add_env.first')" />
|
||||
<li v-html="$t('demos.secrets.add_env.second')" />
|
||||
<li v-html="$t('demos.secrets.add_env.third')" />
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<SecretsTable
|
||||
v-show="hasData === true"
|
||||
:filterable="false"
|
||||
key-only
|
||||
:namespace="configs?.systemNamespace ?? 'system'"
|
||||
:add-secret-modal-visible="addSecretModalVisible"
|
||||
@update:add-secret-modal-visible="addSecretModalVisible = $event"
|
||||
@has-data="hasData = $event"
|
||||
/>
|
||||
</div>
|
||||
</EmptyTemplate>
|
||||
<SecretsTable
|
||||
:filterable="configs?.secretsEnabled !== undefined"
|
||||
:key-only="configs?.secretsEnabled === undefined"
|
||||
:namespace="configs?.secretsEnabled === true ? undefined : (configs?.systemNamespace ?? 'system')"
|
||||
v-else
|
||||
filterable
|
||||
:add-secret-modal-visible="addSecretModalVisible"
|
||||
@update:add-secret-modal-visible="addSecretModalVisible = $event"
|
||||
/>
|
||||
@@ -40,21 +74,50 @@
|
||||
import {useI18n} from "vue-i18n";
|
||||
import {computed, ref} from "vue";
|
||||
import {useStore} from "vuex";
|
||||
import useRouteContext from "../../mixins/useRouteContext.js";
|
||||
import sourceImg from "../../assets/demo/secrets.png";
|
||||
import Layout from "../demo/Layout.vue";
|
||||
import DemoButtons from "../demo/DemoButtons.vue";
|
||||
import EmptyTemplate from "../layout/EmptyTemplate.vue";
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const configs = computed(() => store.getters["misc/configs"]);
|
||||
|
||||
const addSecretModalVisible = ref(false);
|
||||
const hasData = ref(undefined);
|
||||
|
||||
const {t} = useI18n({useScope: "global"});
|
||||
const routeInfo = computed(() => ({title: t("secret.names")}));
|
||||
|
||||
useRouteContext(routeInfo);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.message-block) {
|
||||
width: 100%;
|
||||
.no-secret-manager-block {
|
||||
padding: 0 10.75rem;
|
||||
|
||||
*[style*="display: none"] { display: none !important }
|
||||
|
||||
.header-block {
|
||||
border-bottom: 1px solid var(--ks-border-primary);
|
||||
|
||||
p {
|
||||
font-size: .875rem;
|
||||
}
|
||||
|
||||
.img-wrapper {
|
||||
width: 350px;
|
||||
overflow: visible;
|
||||
direction: rtl;
|
||||
}
|
||||
}
|
||||
|
||||
.text-secondary {
|
||||
color: var(--ks-content-secondary) !important;
|
||||
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,148 +1,155 @@
|
||||
<template>
|
||||
<KestraFilter v-if="filterable" :placeholder="$t('search')" :decode="false" />
|
||||
|
||||
<select-table
|
||||
:data="secrets"
|
||||
v-bind="$attrs"
|
||||
ref="selectTable"
|
||||
:default-sort="{prop: 'key', order: 'ascending'}"
|
||||
table-layout="auto"
|
||||
fixed
|
||||
:selectable="false"
|
||||
@sort-change="handleSort"
|
||||
:infinite-scroll-load="namespace === undefined ? fetchSecrets : undefined"
|
||||
class="fill-height"
|
||||
>
|
||||
<el-table-column
|
||||
v-if="namespace === undefined"
|
||||
prop="namespace"
|
||||
sortable="custom"
|
||||
:sort-orders="['ascending', 'descending']"
|
||||
:label="$t('namespace')"
|
||||
/>
|
||||
<el-table-column prop="key" sortable="custom" :sort-orders="['ascending', 'descending']" :label="keyOnly ? $t('secret.names') : $t('key')">
|
||||
<template #default="scope">
|
||||
<id v-if="scope.row.key !== undefined" :value="scope.row.key" :shrink="false" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column v-if="!keyOnly" prop="description" :label="$t('description')">
|
||||
<template #default="scope">
|
||||
{{ scope.row.description }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column v-if="!keyOnly" prop="tags" :label="$t('tags')">
|
||||
<template #default="scope">
|
||||
<labels v-if="scope.row.tags !== undefined" :labels="scope.row.tags" read-only />
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column column-key="locked" class-name="row-action">
|
||||
<template #default="scope">
|
||||
<el-tooltip v-if="scope.row.namespace !== undefined && areNamespaceSecretsReadOnly?.[scope.row.namespace]" transition="" :hide-after="0" :persistent="false" effect="light">
|
||||
<template #content>
|
||||
<span v-html="$t('secret.isReadOnly')" />
|
||||
</template>
|
||||
<Lock />
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column column-key="copy" class-name="row-action">
|
||||
<template #default="scope">
|
||||
<el-tooltip :content="$t('copy_to_clipboard')">
|
||||
<el-button :icon="ContentCopy" link @click="Utils.copy(`\{\{ secret('${scope.row.key}') \}\}`)" />
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column column-key="update" class-name="row-action">
|
||||
<template #default="scope">
|
||||
<el-button v-if="canUpdate(scope.row)" :icon="FileDocumentEdit" link @click="updateSecretModal(scope.row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column column-key="delete" class-name="row-action">
|
||||
<template #default="scope">
|
||||
<el-button v-if="canDelete(scope.row)" :icon="Delete" link @click="removeSecret(scope.row.key)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</select-table>
|
||||
|
||||
<drawer
|
||||
v-if="addSecretDrawerVisible"
|
||||
v-model="addSecretDrawerVisible"
|
||||
:title="secretModalTitle"
|
||||
>
|
||||
<el-form class="ks-horizontal" :model="secret" :rules="rules" ref="form">
|
||||
<el-form-item v-if="namespace === undefined" :label="$t('namespace')" prop="namespace" required>
|
||||
<!-- TODO ADD FILTER ON NAMESPACES WITH READ-ONLY SECRETS, FOR NOW IT WOULD BE TOO COSTFUL AS IT REQUIRES 1 CALL PER NAMESPACE -->
|
||||
<namespace-select
|
||||
v-model="secret.namespace"
|
||||
:readonly="secret.update"
|
||||
data-type="flow"
|
||||
:include-system-namespace="true"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('secret.key')" prop="key">
|
||||
<el-input v-model="secret.key" :readonly="secret.update" required />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="!secret.update" :label="$t('secret.name')" prop="value">
|
||||
<el-input v-model="secret.value" :placeholder="secretModalTitle" autosize type="textarea" required />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="secret.update" :label="$t('secret.name')" prop="value">
|
||||
<el-col :span="20">
|
||||
<el-input
|
||||
v-model="secret.value"
|
||||
:placeholder="secretModalTitle"
|
||||
autosize
|
||||
type="textarea"
|
||||
required
|
||||
:disabled="!secret.updateValue"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col class="px-2" :span="4">
|
||||
<el-switch
|
||||
size="large"
|
||||
inline-prompt
|
||||
v-model="secret.updateValue"
|
||||
:active-icon="PencilOutline"
|
||||
:inactive-icon="PencilOff"
|
||||
/>
|
||||
</el-col>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('secret.description')" prop="description">
|
||||
<el-input v-model="secret.description" :placeholder="$t('secret.descriptionPlaceholder')" required />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('secret.tags')" prop="tags">
|
||||
<el-row :gutter="20" v-for="(tag, index) in secret.tags" :key="index">
|
||||
<el-col :span="8">
|
||||
<el-input required v-model="tag.key" :placeholder="$t('key')" />
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-input required v-model="tag.value" :placeholder="$t('value')" />
|
||||
</el-col>
|
||||
<el-button-group class="d-flex flex-nowrap">
|
||||
<el-button
|
||||
:icon="Delete"
|
||||
@click="removeSecretTag(index)"
|
||||
:disabled="secret.tags.length === 1"
|
||||
/>
|
||||
</el-button-group>
|
||||
</el-row>
|
||||
<el-button :icon="Plus" @click="addSecretTag" type="primary">
|
||||
{{ $t('secret.addTag') }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button :icon="ContentSave" @click="saveSecret($refs.form)" type="primary">
|
||||
{{ $t('save') }}
|
||||
</el-button>
|
||||
<div class="d-flex flex-column fill-height">
|
||||
<template v-if="filterable">
|
||||
<KestraFilter v-if="namespace" :placeholder="$t('search')" :decode="false" />
|
||||
<section v-else class="d-inline-flex mb-3 filters">
|
||||
<el-input v-model="search" :placeholder="$t('search')" />
|
||||
</section>
|
||||
</template>
|
||||
</drawer>
|
||||
|
||||
<select-table
|
||||
:data="filteredSecrets"
|
||||
ref="selectTable"
|
||||
:default-sort="{prop: 'key', order: 'ascending'}"
|
||||
table-layout="auto"
|
||||
fixed
|
||||
:selectable="false"
|
||||
@sort-change="handleSort"
|
||||
:infinite-scroll-load="namespace === undefined ? fetchSecrets : undefined"
|
||||
class="fill-height"
|
||||
>
|
||||
<el-table-column
|
||||
v-if="namespace === undefined"
|
||||
prop="namespace"
|
||||
sortable="custom"
|
||||
:sort-orders="['ascending', 'descending']"
|
||||
:label="$t('namespace')"
|
||||
/>
|
||||
<el-table-column prop="key" sortable="custom" :sort-orders="['ascending', 'descending']" :label="keyOnly ? $t('secret.names') : $t('key')">
|
||||
<template #default="scope">
|
||||
<id v-if="scope.row.key !== undefined" :value="scope.row.key" :shrink="false" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column v-if="!keyOnly" prop="description" :label="$t('description')">
|
||||
<template #default="scope">
|
||||
{{ scope.row.description }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column v-if="!keyOnly" prop="tags" :label="$t('tags')">
|
||||
<template #default="scope">
|
||||
<labels v-if="scope.row.tags !== undefined" :labels="scope.row.tags" read-only />
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column column-key="locked" class-name="row-action">
|
||||
<template #default="scope">
|
||||
<el-tooltip v-if="scope.row.namespace !== undefined && areNamespaceSecretsReadOnly?.[scope.row.namespace]" transition="" :hide-after="0" :persistent="false" effect="light">
|
||||
<template #content>
|
||||
<span v-html="$t('secret.isReadOnly')" />
|
||||
</template>
|
||||
<el-icon class="d-flex justify-content-center text-base">
|
||||
<Lock />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column column-key="copy" class-name="row-action">
|
||||
<template #default="scope">
|
||||
<el-tooltip :content="$t('copy_to_clipboard')">
|
||||
<el-button :icon="ContentCopy" link @click="Utils.copy(`\{\{ secret('${scope.row.key}') \}\}`)" />
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column v-if="!keyOnly" column-key="update" class-name="row-action">
|
||||
<template #default="scope">
|
||||
<el-button v-if="canUpdate(scope.row)" :icon="FileDocumentEdit" link @click="updateSecretModal(scope.row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column v-if="!keyOnly" column-key="delete" class-name="row-action">
|
||||
<template #default="scope">
|
||||
<el-button v-if="canDelete(scope.row)" :icon="Delete" link @click="removeSecret(scope.row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</select-table>
|
||||
|
||||
<drawer
|
||||
v-if="addSecretDrawerVisible"
|
||||
v-model="addSecretDrawerVisible"
|
||||
:title="secretModalTitle"
|
||||
>
|
||||
<el-form class="ks-horizontal" :model="secret" :rules="rules" ref="form">
|
||||
<el-form-item v-if="namespace === undefined" :label="$t('namespace')" prop="namespace" required>
|
||||
<namespace-select
|
||||
v-model="secret.namespace"
|
||||
:readonly="secret.update"
|
||||
data-type="flow"
|
||||
:include-system-namespace="true"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('secret.key')" prop="key">
|
||||
<el-input v-model="secret.key" :readonly="secret.update" required />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="!secret.update" :label="$t('secret.name')" prop="value">
|
||||
<el-input v-model="secret.value" :placeholder="secretModalTitle" autosize type="textarea" required />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="secret.update" :label="$t('secret.name')" prop="value">
|
||||
<el-col :span="20">
|
||||
<el-input
|
||||
v-model="secret.value"
|
||||
:placeholder="secretModalTitle"
|
||||
autosize
|
||||
type="textarea"
|
||||
required
|
||||
:disabled="!secret.updateValue"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col class="px-2" :span="4">
|
||||
<el-switch
|
||||
size="large"
|
||||
inline-prompt
|
||||
v-model="secret.updateValue"
|
||||
:active-icon="PencilOutline"
|
||||
:inactive-icon="PencilOff"
|
||||
/>
|
||||
</el-col>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('secret.description')" prop="description">
|
||||
<el-input v-model="secret.description" :placeholder="$t('secret.descriptionPlaceholder')" required />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('secret.tags')" prop="tags">
|
||||
<el-row :gutter="20" v-for="(tag, index) in secret.tags" :key="index">
|
||||
<el-col :span="8">
|
||||
<el-input required v-model="tag.key" :placeholder="$t('key')" />
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-input required v-model="tag.value" :placeholder="$t('value')" />
|
||||
</el-col>
|
||||
<el-button-group class="d-flex flex-nowrap">
|
||||
<el-button
|
||||
:icon="Delete"
|
||||
@click="removeSecretTag(index)"
|
||||
:disabled="secret.tags.length === 1"
|
||||
/>
|
||||
</el-button-group>
|
||||
</el-row>
|
||||
<el-button :icon="Plus" @click="addSecretTag" type="primary">
|
||||
{{ $t('secret.addTag') }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button :icon="ContentSave" @click="saveSecret($refs.form)" type="primary">
|
||||
{{ $t('save') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -176,9 +183,13 @@
|
||||
Id,
|
||||
Drawer
|
||||
},
|
||||
inheritAttrs: false,
|
||||
computed: {
|
||||
...mapState("auth", ["user"]),
|
||||
filteredSecrets() {
|
||||
return this.namespace === undefined
|
||||
? this.secrets?.filter((secret: {key: string}) => !this.search || secret.key.toLowerCase().includes(this.search.toLowerCase()))
|
||||
: this.secrets;
|
||||
},
|
||||
secretModalTitle() {
|
||||
return this.secret?.update ? this.$t("secret.update", {name: this.secret.key}) : this.$t("secret.add");
|
||||
},
|
||||
@@ -219,13 +230,35 @@
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
"update:addSecretModalVisible"
|
||||
"update:addSecretModalVisible",
|
||||
"update:isSecretReadOnly",
|
||||
"hasData"
|
||||
],
|
||||
watch: {
|
||||
addSecretModalVisible(newValue) {
|
||||
if (!newValue) {
|
||||
this.resetForm();
|
||||
}
|
||||
},
|
||||
hasData(newValue, oldValue) {
|
||||
if (oldValue === undefined) {
|
||||
this.$emit("hasData", newValue);
|
||||
}
|
||||
},
|
||||
search(newValue) {
|
||||
if (newValue !== undefined) {
|
||||
this.$router.push({query: {
|
||||
q: newValue
|
||||
}})
|
||||
}
|
||||
},
|
||||
"$route.query.q"(newValue, oldValue) {
|
||||
if (newValue !== undefined && newValue !== oldValue) {
|
||||
if (this.namespace === undefined && this.search !== newValue) {
|
||||
this.search = newValue;
|
||||
}
|
||||
this.reloadSecrets();
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -263,7 +296,9 @@
|
||||
required: false,
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
hasData: undefined,
|
||||
search: this.$route.query?.q ?? ""
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
@@ -275,7 +310,14 @@
|
||||
},
|
||||
async fetchSecrets() {
|
||||
if (this.secretsIterator === undefined) {
|
||||
this.secretsIterator = this.namespace === undefined ? useAllSecrets(this.$store, 20) : useNamespaceSecrets(this.$store, this.namespace, 20);
|
||||
this.secretsIterator = this.namespace === undefined ? useAllSecrets(this.$store, 20) : useNamespaceSecrets(this.$store, this.namespace, 20, {
|
||||
sort: this.$route.query.sort || "key:asc",
|
||||
...(this.$route.query.q === undefined ? {} : {filters: {
|
||||
q: {
|
||||
STARTS_WITH: this.$route.query.q[0]
|
||||
}
|
||||
}})
|
||||
});
|
||||
}
|
||||
|
||||
let emitReadOnly = false;
|
||||
@@ -288,9 +330,11 @@
|
||||
}
|
||||
|
||||
if (fetch.length === 0) {
|
||||
this.hasData = false;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this.hasData = true;
|
||||
this.secrets = [...(this.secrets || []), ...fetch];
|
||||
|
||||
return fetch;
|
||||
@@ -338,26 +382,26 @@
|
||||
removeSecretTag(index) {
|
||||
this.secret.tags.splice(index, 1);
|
||||
},
|
||||
reloadSecrets() {
|
||||
async reloadSecrets() {
|
||||
this.secretsIterator = undefined;
|
||||
|
||||
const previousLength = this.secrets?.length ?? 0;
|
||||
await this.$refs.selectTable.resetInfiniteScroll();
|
||||
this.secrets = [];
|
||||
this.$refs.selectTable.resetInfiniteScroll();
|
||||
|
||||
// If we are in the global Secrets view we let the infinite scroll handling the fetch
|
||||
if (this.namespace !== undefined) {
|
||||
if (this.namespace !== undefined || previousLength === 0) {
|
||||
this.fetchSecrets();
|
||||
}
|
||||
},
|
||||
removeSecret(key) {
|
||||
removeSecret({key, namespace}) {
|
||||
this.$toast().confirm(this.$t("delete confirm", {name: key}), () => {
|
||||
return this.$store
|
||||
.dispatch("namespace/deleteSecrets", {namespace: this.$route.params.id, key: key})
|
||||
.dispatch("namespace/deleteSecrets", {namespace: namespace, key})
|
||||
.then(() => {
|
||||
this.$toast().deleted(key);
|
||||
})
|
||||
.then(() => {
|
||||
this.reloadSecrets();
|
||||
})
|
||||
.then(() => this.reloadSecrets())
|
||||
});
|
||||
},
|
||||
isSecretValueUpdated() {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
export type FetchResult<T> = { total: number, results: T[] };
|
||||
|
||||
export abstract class EntityIterator<T> {
|
||||
private readonly fetchSize: number;
|
||||
private total: number | undefined;
|
||||
readonly fetchSize: number;
|
||||
private _total: number | undefined;
|
||||
private page = 0;
|
||||
private alreadyFetched: T[] = [];
|
||||
private buffered: T[] = [];
|
||||
private readonly options: any;
|
||||
readonly options: any;
|
||||
|
||||
protected constructor(fetchSize: number, options?: any) {
|
||||
if (fetchSize <= 0) {
|
||||
@@ -14,6 +16,10 @@ export abstract class EntityIterator<T> {
|
||||
this.options = options ?? {};
|
||||
}
|
||||
|
||||
get total(): number | undefined {
|
||||
return this._total;
|
||||
}
|
||||
|
||||
fetchOptions() {
|
||||
return {
|
||||
commit: false,
|
||||
@@ -24,13 +30,17 @@ export abstract class EntityIterator<T> {
|
||||
}
|
||||
}
|
||||
|
||||
abstract fetchCall(options: any): Promise<{total: number, results: T[]}>;
|
||||
abstract fetchCall(): Promise<FetchResult<T>>;
|
||||
|
||||
stopCondition() {
|
||||
return this.total === this.alreadyFetched.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* If no buffer is available, fetches the next entity and returns the first entity while buffering the rest
|
||||
*/
|
||||
async single(): Promise<T | undefined> {
|
||||
if (this.total === this.alreadyFetched.length && this.buffered.length === 0) {
|
||||
if (this.stopCondition() && this.buffered.length === 0) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
@@ -45,12 +55,12 @@ export abstract class EntityIterator<T> {
|
||||
* Fetches the next batch of entities
|
||||
*/
|
||||
async next(): Promise<T[]> {
|
||||
if (this.total === this.alreadyFetched.length) {
|
||||
if (this.stopCondition()) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
const entityFetch = await this.fetchCall();
|
||||
this.total = entityFetch.total;
|
||||
this._total = entityFetch.total;
|
||||
this.alreadyFetched = [...this.alreadyFetched, ...entityFetch.results];
|
||||
return entityFetch.results;
|
||||
}
|
||||
@@ -64,9 +74,9 @@ export abstract class EntityIterator<T> {
|
||||
}
|
||||
|
||||
await this.next();
|
||||
const entitiesFetchPromises: Promise<T[]> = [];
|
||||
const entitiesFetchPromises: Promise<T[]>[] = [];
|
||||
|
||||
for (let i = this.page; i < this.total / this.fetchSize; i++) {
|
||||
for (let i = this.page; i < this.total! / this.fetchSize; i++) {
|
||||
entitiesFetchPromises.push(this.next());
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {Store} from "vuex";
|
||||
import {EntityIterator} from "./entityIterator.ts";
|
||||
import {EntityIterator, FetchResult} from "./entityIterator.ts";
|
||||
import {NamespaceIterator} from "./useNamespaces.ts";
|
||||
import {Me} from "../stores/auth";
|
||||
import permissions from "../models/permission";
|
||||
@@ -14,10 +14,12 @@ export interface NamespaceSecret {
|
||||
|
||||
export type SecretIterator = NamespaceSecretIterator | AllSecretIterator;
|
||||
|
||||
type NamespaceSecretFetchResult = FetchResult<NamespaceSecret> & { readOnly: boolean };
|
||||
|
||||
export class NamespaceSecretIterator extends EntityIterator<NamespaceSecret>{
|
||||
private readonly store: Store<any>;
|
||||
readonly namespace: string;
|
||||
areNamespaceSecretsReadOnly: boolean | undefined = ref(undefined);
|
||||
areNamespaceSecretsReadOnly = ref(undefined) as unknown as boolean | undefined;
|
||||
|
||||
constructor(store: Store<any>, namespace: string, fetchSize: number, options?: any) {
|
||||
super(fetchSize, options);
|
||||
@@ -34,12 +36,12 @@ export class NamespaceSecretIterator extends EntityIterator<NamespaceSecret>{
|
||||
};
|
||||
}
|
||||
|
||||
fetchCall(): Promise<{ total: number; results: NamespaceSecret[], readOnly: boolean }> {
|
||||
fetchCall(): Promise<NamespaceSecretFetchResult> {
|
||||
return this.doFetch();
|
||||
}
|
||||
|
||||
private async doFetch(): Promise<{ total: number; results: NamespaceSecret[], readOnly: boolean }> {
|
||||
const fetch = await this.store.dispatch("namespace/listSecrets", this.fetchOptions());
|
||||
private async doFetch(): Promise<NamespaceSecretFetchResult> {
|
||||
const fetch = (await this.store.dispatch("namespace/listSecrets", this.fetchOptions())) as NamespaceSecretFetchResult;
|
||||
this.areNamespaceSecretsReadOnly = fetch.readOnly ?? true;
|
||||
|
||||
return {
|
||||
@@ -52,9 +54,9 @@ export class NamespaceSecretIterator extends EntityIterator<NamespaceSecret>{
|
||||
export class AllSecretIterator extends EntityIterator<NamespaceSecret>{
|
||||
private readonly store: Store<any>;
|
||||
private readonly user: Me;
|
||||
private namespaceIterator: NamespaceIterator;
|
||||
private namespaceSecretIterator: NamespaceSecretIterator;
|
||||
private areNamespaceSecretsReadOnly: {[namespace: string]: boolean} = ref({});
|
||||
private namespaceIterator: NamespaceIterator | undefined;
|
||||
private namespaceSecretIterator: NamespaceSecretIterator | undefined;
|
||||
private areNamespaceSecretsReadOnly = ref({}) as unknown as {[namespace: string]: boolean};
|
||||
|
||||
constructor(store: Store<any>, fetchSize: number, options?: any) {
|
||||
super(fetchSize, options);
|
||||
@@ -62,7 +64,11 @@ export class AllSecretIterator extends EntityIterator<NamespaceSecret>{
|
||||
this.user = this.store.state?.["auth"]?.user;
|
||||
}
|
||||
|
||||
async fetchCall(): Promise<{ total: number; results: NamespaceSecret[], readOnly: boolean }> {
|
||||
stopCondition(): boolean {
|
||||
return this.total === 0;
|
||||
}
|
||||
|
||||
async fetchCall(): Promise<FetchResult<NamespaceSecret>> {
|
||||
if (this.namespaceIterator === undefined) {
|
||||
this.namespaceIterator = new NamespaceIterator(this.store, 20, {
|
||||
commit: false,
|
||||
@@ -83,16 +89,25 @@ export class AllSecretIterator extends EntityIterator<NamespaceSecret>{
|
||||
|
||||
this.namespaceSecretIterator = new NamespaceSecretIterator(this.store, namespace.id, this.fetchSize, this.options);
|
||||
}
|
||||
const fetch = await this.namespaceSecretIterator.fetchCall();
|
||||
if (fetch.results.length > 0) {
|
||||
this.areNamespaceSecretsReadOnly[this.namespaceSecretIterator.namespace] = fetch.readOnly;
|
||||
return {
|
||||
...fetch,
|
||||
results: fetch.results.map(secret => ({...secret, namespace: this.namespaceSecretIterator.namespace}))
|
||||
};
|
||||
|
||||
const fetch = {
|
||||
results: await this.namespaceSecretIterator.next(),
|
||||
namespace: this.namespaceSecretIterator.namespace,
|
||||
areNamespaceSecretsReadOnly: this.namespaceSecretIterator.areNamespaceSecretsReadOnly,
|
||||
total: this.namespaceSecretIterator.total
|
||||
};
|
||||
|
||||
if (this.namespaceSecretIterator.stopCondition()) {
|
||||
this.namespaceSecretIterator = undefined;
|
||||
}
|
||||
|
||||
this.namespaceSecretIterator = undefined;
|
||||
if (fetch.results.length > 0) {
|
||||
this.areNamespaceSecretsReadOnly[fetch.namespace] = fetch.areNamespaceSecretsReadOnly!;
|
||||
return {
|
||||
total: fetch.total!,
|
||||
results: fetch.results.map(secret => ({...secret, namespace: fetch.namespace}))
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,6 +116,6 @@ export function useNamespaceSecrets(store: Store<any>, namespace: string, secret
|
||||
return new NamespaceSecretIterator(store, namespace, secretsFetchSize, options);
|
||||
}
|
||||
|
||||
export function useAllSecrets(store: Store<any>, secretsFetchSize: number, options?: any): NamespaceSecretIterator {
|
||||
export function useAllSecrets(store: Store<any>, secretsFetchSize: number, options?: any): AllSecretIterator {
|
||||
return new AllSecretIterator(store, secretsFetchSize, options);
|
||||
}
|
||||
@@ -14,6 +14,7 @@
|
||||
:blueprint-id="selectedBlueprintId"
|
||||
blueprint-type="community"
|
||||
@back="selectedBlueprintId = undefined"
|
||||
:combined-view
|
||||
/>
|
||||
<blueprints-browser
|
||||
@loaded="$emit('loaded', $event)"
|
||||
@@ -75,7 +76,11 @@
|
||||
tab: {
|
||||
type: String,
|
||||
default: "community"
|
||||
}
|
||||
},
|
||||
combinedView: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
<div class="left">
|
||||
<div class="blueprint">
|
||||
<div class="ps-0 title">
|
||||
{{ blueprint.title }}
|
||||
{{ blueprint.title ?? blueprint.id }}
|
||||
</div>
|
||||
<div v-if="!system" class="tags text-uppercase">
|
||||
<div v-for="(tag, index) in blueprint.tags" :key="index" class="tag-box">
|
||||
|
||||
@@ -138,6 +138,9 @@ export function useLeftMenu() {
|
||||
icon: {
|
||||
element: shallowRef(ShieldKeyOutline),
|
||||
class: "menu-icon"
|
||||
},
|
||||
attributes: {
|
||||
locked: true
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -233,7 +236,7 @@ export function useLeftMenu() {
|
||||
{
|
||||
href: {name: "admin/tenants/list"},
|
||||
routes: routeStartWith("admin/tenants"),
|
||||
title: t("tenants"),
|
||||
title: t("tenant.names"),
|
||||
icon: {
|
||||
element: shallowRef(ShieldLockOutline),
|
||||
class: "menu-icon"
|
||||
|
||||
@@ -768,31 +768,6 @@ export default {
|
||||
|
||||
return undefined;
|
||||
},
|
||||
flowWarnings(state, getters){
|
||||
if (getters.isFlow) {
|
||||
const outdatedWarning =
|
||||
state.flowValidation?.outdated && !state.isCreating
|
||||
? [getters.outdatedMessage]
|
||||
: [];
|
||||
|
||||
const deprecationWarnings =
|
||||
state.flowValidation?.deprecationPaths?.map(
|
||||
(f) => `${f} ${this.$i18n.t("is deprecated")}.`
|
||||
) ?? [];
|
||||
|
||||
const otherWarnings = state.flowValidation?.warnings ?? [];
|
||||
|
||||
const warnings = [
|
||||
...outdatedWarning,
|
||||
...deprecationWarnings,
|
||||
...otherWarnings,
|
||||
];
|
||||
|
||||
return warnings.length === 0 ? undefined : warnings;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
flowInfos(state, getters){
|
||||
if (getters.isFlow) {
|
||||
const infos = state.flowValidation?.infos ?? [];
|
||||
|
||||
@@ -303,6 +303,14 @@
|
||||
}
|
||||
},
|
||||
"secrets": {
|
||||
"add_env": {
|
||||
"first": "Kodieren Sie den <strong><a href=\"https://kestra.io/docs/how-to-guides/secrets#using-secrets-in-kestra\" target=\"_blank\">geheimen Wert in Base64</a></strong>",
|
||||
"intro": "Um ein neues Secret zu erstellen:",
|
||||
"second": "Fügen Sie eine Umgebungsvariable mit dem Namen '<strong>SECRET_{'{MY_SECRET_KEY}'}</strong>' mit dem obigen Wert hinzu",
|
||||
"third": "Starte deine Kestra-Instanz neu"
|
||||
},
|
||||
"detected_env": "Hier sind Umgebungsvariablen vom Typ \"secret\", die beim Start der Instanz identifiziert wurden:",
|
||||
"empty_env": "Sie haben noch keine Secrets in Ihrer Umgebung.",
|
||||
"message": "Die Enterprise Edition (EE) ermöglicht es Ihnen, Geheimnisse direkt in der UI hinzuzufügen, zu bearbeiten oder zu löschen – ohne Neustart der Instanz. Organisieren Sie Geheimnisse nach namespace mit detaillierten RBAC-Berechtigungen und erben Sie sie von übergeordneten zu untergeordneten namespaces. EE integriert sich mit Geheimnis-Managern wie HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, Google Secret Manager und Elasticsearch. Richten Sie dedizierte Backends pro namespace, Mandant oder Instanz ein, um Geheimnisse zwischen Teams zu isolieren, oder aktivieren Sie schreibgeschützte Geheimnisse, die in externen Tresoren gespeichert sind. Geheimnisse bleiben sowohl im Ruhezustand als auch während der Übertragung verschlüsselt. Optionales Caching reduziert API-Aufrufe, und Prüfpfade protokollieren alle Zugriffe.",
|
||||
"title": "Aktualisieren Sie Ihr Secrets Management"
|
||||
},
|
||||
@@ -547,6 +555,7 @@
|
||||
},
|
||||
"empty": "Sie haben noch keine Suche gespeichert.",
|
||||
"label": "Gespeicherte Suchen",
|
||||
"remove": "Entfernen",
|
||||
"tooltip": "Sie können keine leeren Suchkriterien speichern."
|
||||
},
|
||||
"settings": {
|
||||
@@ -1152,8 +1161,10 @@
|
||||
"templates deleted": "<code>{count}</code> Vorlage(n) gelöscht",
|
||||
"templates deprecated": "Vorlagen sind veraltet. Bitte verwenden Sie stattdessen Subflows. Siehe den <a href=\"https://kestra.io/docs/migration-guide/0.11.0/templates\" target=\"_blank\">Migrationsabschnitt</a>, der erklärt, wie Sie von Vorlagen zu Subflows migrieren können.",
|
||||
"templates exported": "Vorlagen exportiert",
|
||||
"tenant": "Tenant",
|
||||
"tenants": "Tenants",
|
||||
"tenant": {
|
||||
"name": "Mandant",
|
||||
"names": "Mandanten"
|
||||
},
|
||||
"theme": "Modus",
|
||||
"this_task_has": "Diese Task hat",
|
||||
"timezone": "Zeitzone",
|
||||
|
||||
@@ -807,7 +807,15 @@
|
||||
"get_a_demo_button": "Get a Demo",
|
||||
"secrets": {
|
||||
"title": "Upgrade your Secrets Management",
|
||||
"message": "The Enterprise Edition (EE) lets you add, edit, or delete secrets directly in the UI—no instance restarts needed. Organize secrets by namespace with granular RBAC permissions, and inherit them from parent to child namespaces. EE integrates with secrets managers like HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, Google Secret Manager, and Elasticsearch. Set up dedicated backends per namespace, tenant, or instance to isolate secrets across teams, or enable read-only secrets stored in external vaults. Secrets stay encrypted at rest and in transit. Optional caching reduces API calls, and audit trails log all access."
|
||||
"message": "The Enterprise Edition (EE) lets you add, edit, or delete secrets directly in the UI—no instance restarts needed. Organize secrets by namespace with granular RBAC permissions, and inherit them from parent to child namespaces. EE integrates with secrets managers like HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, Google Secret Manager, and Elasticsearch. Set up dedicated backends per namespace, tenant, or instance to isolate secrets across teams, or enable read-only secrets stored in external vaults. Secrets stay encrypted at rest and in transit. Optional caching reduces API calls, and audit trails log all access.",
|
||||
"detected_env": "Here are secret-type environment variables identified at instance start-time:",
|
||||
"empty_env": "You don't have any Secrets in your environment yet.",
|
||||
"add_env": {
|
||||
"intro": "To create a new Secret:",
|
||||
"first": "Encode the <strong><a href=\"https://kestra.io/docs/how-to-guides/secrets#using-secrets-in-kestra\" target=\"_blank\">secret value in Base64</a></strong>",
|
||||
"second": "Add an environment variable named '<strong>SECRET_{'{MY_SECRET_KEY}'}</strong>' with the above value",
|
||||
"third": "Restart your Kestra instance"
|
||||
}
|
||||
},
|
||||
"apps": {
|
||||
"title": "Build custom Apps with Kestra",
|
||||
@@ -905,8 +913,10 @@
|
||||
"enable concurrency": "Enable concurrency",
|
||||
"auditlogs": "Audit Logs",
|
||||
"iam": "IAM",
|
||||
"tenant": "Tenant",
|
||||
"tenants": "Tenants",
|
||||
"tenant": {
|
||||
"name": "Tenant",
|
||||
"names": "Tenants"
|
||||
},
|
||||
"ee-tooltip": {
|
||||
"features-blocked": "This feature requires Enterprise Edition.",
|
||||
"button": "Talk to us"
|
||||
@@ -1116,6 +1126,7 @@
|
||||
},
|
||||
"save": {
|
||||
"label": "Saved searches",
|
||||
"remove": "Remove",
|
||||
"empty": "You still haven't saved any search.",
|
||||
"tooltip": "You can't save empty search criteria.",
|
||||
"dialog": {
|
||||
@@ -1248,7 +1259,7 @@
|
||||
"task_runners": "Task Runners",
|
||||
"empty": {
|
||||
"announcements": {
|
||||
"title": "You have no annoucements yet!",
|
||||
"title": "You have no announcements yet!",
|
||||
"content": "Announcements allow you to notify your users about any changes or inform them about planned maintenance downtime. Simply select the announcement type, the date range for which it should be displayed, and write the announcement message."
|
||||
},
|
||||
"apps": {
|
||||
|
||||
@@ -303,6 +303,14 @@
|
||||
}
|
||||
},
|
||||
"secrets": {
|
||||
"add_env": {
|
||||
"first": "Codifica el <strong><a href=\"https://kestra.io/docs/how-to-guides/secrets#using-secrets-in-kestra\" target=\"_blank\">valor secreto en Base64</a></strong>",
|
||||
"intro": "Para crear un nuevo Secret:",
|
||||
"second": "Agrega una variable de entorno llamada '<strong>SECRET_{'{MY_SECRET_KEY}'}</strong>' con el valor anterior",
|
||||
"third": "Reinicia tu instancia de Kestra"
|
||||
},
|
||||
"detected_env": "Aquí están las variables de entorno de tipo secreto identificadas al momento de inicio de la instancia:",
|
||||
"empty_env": "Todavía no tienes ningún Secret en tu entorno.",
|
||||
"message": "La Enterprise Edition (EE) te permite agregar, editar o eliminar secretos directamente en la UI, sin necesidad de reiniciar la instancia. Organiza secretos por namespace con permisos RBAC granulares, y herédalos de namespaces padre a hijo. EE se integra con gestores de secretos como HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, Google Secret Manager y Elasticsearch. Configura backends dedicados por namespace, tenant o instancia para aislar secretos entre equipos, o habilita secretos de solo lectura almacenados en vaults externos. Los secretos permanecen cifrados en reposo y en tránsito. El almacenamiento en caché opcional reduce las llamadas a la API, y las auditorías registran todo el acceso.",
|
||||
"title": "Mejora tu Gestión de Secrets"
|
||||
},
|
||||
@@ -547,6 +555,7 @@
|
||||
},
|
||||
"empty": "Todavía no has guardado ninguna búsqueda.",
|
||||
"label": "Búsquedas guardadas",
|
||||
"remove": "Eliminar",
|
||||
"tooltip": "No puedes guardar criterios de búsqueda vacíos."
|
||||
},
|
||||
"settings": {
|
||||
@@ -1152,8 +1161,10 @@
|
||||
"templates deleted": "<code>{count}</code> Plantilla(s) eliminadas",
|
||||
"templates deprecated": "Las plantillas están obsoletas. Por favor usa subflows en su lugar. Consulta la <a href=\"https://kestra.io/docs/migration-guide/0.11.0/templates\" target=\"_blank\">sección de Migraciones</a> que explica cómo puedes migrar de plantillas a subflows.",
|
||||
"templates exported": "Plantillas exportadas",
|
||||
"tenant": "Tenant",
|
||||
"tenants": "Tenants",
|
||||
"tenant": {
|
||||
"name": "Mandante",
|
||||
"names": "Arrendatarios"
|
||||
},
|
||||
"theme": "Tema",
|
||||
"this_task_has": "Esta tarea tiene",
|
||||
"timezone": "Zona horaria",
|
||||
|
||||
@@ -303,6 +303,14 @@
|
||||
}
|
||||
},
|
||||
"secrets": {
|
||||
"add_env": {
|
||||
"first": "Encodez la <strong><a href=\"https://kestra.io/docs/how-to-guides/secrets#using-secrets-in-kestra\" target=\"_blank\">valeur secrète en Base64</a></strong>",
|
||||
"intro": "Pour créer un nouveau Secret :",
|
||||
"second": "Ajoutez une variable d'environnement nommée '<strong>SECRET_{'{MY_SECRET_KEY}'}</strong>' avec la valeur ci-dessus.",
|
||||
"third": "Redémarrez votre instance Kestra"
|
||||
},
|
||||
"detected_env": "Voici les variables d'environnement de type secret identifiées au démarrage de l'instance :",
|
||||
"empty_env": "Vous n'avez pas encore de Secrets dans votre environnement.",
|
||||
"message": "L'Enterprise Edition (EE) vous permet d'ajouter, de modifier ou de supprimer des secrets directement dans l'UI, sans redémarrage de l'instance. Organisez les secrets par namespace avec des permissions RBAC granulaires, et héritez-les des namespaces parents aux namespaces enfants. EE s'intègre avec des gestionnaires de secrets comme HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, Google Secret Manager et Elasticsearch. Configurez des backends dédiés par namespace, tenant ou instance pour isoler les secrets entre les équipes, ou activez des secrets en lecture seule stockés dans des coffres externes. Les secrets restent chiffrés au repos et en transit. Un cache optionnel réduit les appels API, et les pistes d'audit loguent tous les accès.",
|
||||
"title": "Améliorez votre gestion des Secrets"
|
||||
},
|
||||
@@ -547,6 +555,7 @@
|
||||
},
|
||||
"empty": "Vous n'avez pas encore enregistré de recherche.",
|
||||
"label": "Recherches enregistrées",
|
||||
"remove": "Supprimer",
|
||||
"tooltip": "Vous ne pouvez pas enregistrer des critères de recherche vides."
|
||||
},
|
||||
"settings": {
|
||||
@@ -1152,8 +1161,10 @@
|
||||
"templates deleted": "<code>{count}</code> Template(s) supprimé(s)",
|
||||
"templates deprecated": "Les templates sont obsolètes. Veuillez utiliser des sous-flux au lieu des templates. Consultez <a href=\"https://kestra.io/docs/migration-guide/0.11.0/templates\" target=\"_blank\">section Migrations</a> expliquant comment vous pouvez migrer vers les sous-flux.",
|
||||
"templates exported": "Templates exportés",
|
||||
"tenant": "Tenant",
|
||||
"tenants": "Tenants",
|
||||
"tenant": {
|
||||
"name": "Mandant",
|
||||
"names": "Mandants"
|
||||
},
|
||||
"theme": "Thème",
|
||||
"this_task_has": "Cette tâche a",
|
||||
"timezone": "Fuseau horaire",
|
||||
|
||||
@@ -303,6 +303,14 @@
|
||||
}
|
||||
},
|
||||
"secrets": {
|
||||
"add_env": {
|
||||
"first": "<strong><a href=\"https://kestra.io/docs/how-to-guides/secrets#using-secrets-in-kestra\" target=\"_blank\">गुप्त value को Base64 में एन्कोड करें</a></strong>",
|
||||
"intro": "नया Secret बनाने के लिए:",
|
||||
"second": "'<strong>SECRET_{'{MY_SECRET_KEY}'}</strong>' नामक एक environment variable जोड़ें उपरोक्त value के साथ",
|
||||
"third": "अपने Kestra instance को पुनः प्रारंभ करें"
|
||||
},
|
||||
"detected_env": "यहाँ instance start-time पर पहचाने गए secret-type environment variables हैं:",
|
||||
"empty_env": "आपके वातावरण में अभी तक कोई Secrets नहीं हैं।",
|
||||
"message": "एंटरप्राइज एडिशन (EE) आपको UI में सीधे secrets जोड़ने, संपादित करने या हटाने की अनुमति देता है—कोई instance पुनरारंभ की आवश्यकता नहीं है। secrets को namespace द्वारा संगठित करें, जिसमें सूक्ष्म RBAC अनुमतियाँ हों, और उन्हें parent से child namespaces में विरासत में लें। EE HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, Google Secret Manager, और Elasticsearch जैसे secrets प्रबंधकों के साथ एकीकृत होता है। टीमों के बीच secrets को अलग करने के लिए namespace, tenant, या instance के अनुसार समर्पित बैकएंड सेट करें, या बाहरी vaults में संग्रहीत read-only secrets को सक्षम करें। secrets को आराम और ट्रांजिट में एन्क्रिप्टेड रखा जाता है। वैकल्पिक caching API कॉल्स को कम करता है, और audit trails सभी एक्सेस को log करते हैं।",
|
||||
"title": "अपने Secrets Management को अपग्रेड करें"
|
||||
},
|
||||
@@ -547,6 +555,7 @@
|
||||
},
|
||||
"empty": "आपने अभी तक कोई खोज सहेजी नहीं है।",
|
||||
"label": "सहेजे गए खोजें",
|
||||
"remove": "हटाएं",
|
||||
"tooltip": "आप खाली खोज मानदंड सहेज नहीं सकते।"
|
||||
},
|
||||
"settings": {
|
||||
@@ -1152,8 +1161,10 @@
|
||||
"templates deleted": "<code>{count}</code> template(s) हटाए गए",
|
||||
"templates deprecated": "templates अप्रचलित हैं। कृपया इसके बजाय subflows का उपयोग करें। यह समझाने वाला <a href=\"https://kestra.io/docs/migration-guide/0.11.0/templates\" target=\"_blank\">माइग्रेशन अनुभाग</a> देखें कि आप टेम्पलेट्स से subflows में कैसे माइग्रेट कर सकते हैं।",
|
||||
"templates exported": "templates निर्यात किए गए",
|
||||
"tenant": "Tenant",
|
||||
"tenants": "Tenants",
|
||||
"tenant": {
|
||||
"name": "मंडल",
|
||||
"names": "मंडल"
|
||||
},
|
||||
"theme": "थीम",
|
||||
"this_task_has": "यह task है",
|
||||
"timezone": "समय क्षेत्र",
|
||||
|
||||
@@ -303,6 +303,14 @@
|
||||
}
|
||||
},
|
||||
"secrets": {
|
||||
"add_env": {
|
||||
"first": "Codifica il <strong><a href=\"https://kestra.io/docs/how-to-guides/secrets#using-secrets-in-kestra\" target=\"_blank\">valore segreto in Base64</a></strong>",
|
||||
"intro": "Per creare un nuovo Secret:",
|
||||
"second": "Aggiungi una variabile di ambiente chiamata '<strong>SECRET_{'{MY_SECRET_KEY}'}</strong>' con il valore sopra indicato",
|
||||
"third": "Riavvia la tua istanza di Kestra"
|
||||
},
|
||||
"detected_env": "Ecco le variabili d'ambiente di tipo secret identificate al momento dell'avvio dell'istanza:",
|
||||
"empty_env": "Non hai ancora alcun Secret nel tuo ambiente.",
|
||||
"message": "L'Enterprise Edition (EE) ti consente di aggiungere, modificare o eliminare segreti direttamente nell'interfaccia utente, senza bisogno di riavviare l'istanza. Organizza i segreti per namespace con permessi RBAC granulari e ereditali dai namespace genitori a quelli figli. EE si integra con gestori di segreti come HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, Google Secret Manager ed Elasticsearch. Configura backend dedicati per namespace, tenant o istanza per isolare i segreti tra i team, oppure abilita segreti di sola lettura memorizzati in vault esterni. I segreti rimangono crittografati a riposo e in transito. La cache opzionale riduce le chiamate API e le tracce di audit registrano tutti gli accessi.",
|
||||
"title": "Aggiorna il tuo Secrets Management"
|
||||
},
|
||||
@@ -547,6 +555,7 @@
|
||||
},
|
||||
"empty": "Non hai ancora salvato alcuna ricerca.",
|
||||
"label": "Ricerche salvate",
|
||||
"remove": "Rimuovi",
|
||||
"tooltip": "Non puoi salvare criteri di ricerca vuoti."
|
||||
},
|
||||
"settings": {
|
||||
@@ -1152,8 +1161,10 @@
|
||||
"templates deleted": "<code>{count}</code> Template/i eliminati",
|
||||
"templates deprecated": "I templates sono deprecati. Si prega di utilizzare i subflows. Vedi la <a href=\"https://kestra.io/docs/migration-guide/0.11.0/templates\" target=\"_blank\">sezione Migrazioni</a> che spiega come migrare dai templates ai subflows.",
|
||||
"templates exported": "Template esportati",
|
||||
"tenant": "Tenant",
|
||||
"tenants": "Tenants",
|
||||
"tenant": {
|
||||
"name": "Mandante",
|
||||
"names": "Mandanti"
|
||||
},
|
||||
"theme": "Tema",
|
||||
"this_task_has": "Questo task ha",
|
||||
"timezone": "Fuso orario",
|
||||
|
||||
@@ -303,6 +303,14 @@
|
||||
}
|
||||
},
|
||||
"secrets": {
|
||||
"add_env": {
|
||||
"first": "<strong><a href=\"https://kestra.io/docs/how-to-guides/secrets#using-secrets-in-kestra\" target=\"_blank\">シークレットの値をBase64でエンコード</a></strong>",
|
||||
"intro": "新しいSecretを作成するには:",
|
||||
"second": "`<strong>SECRET_{'{MY_SECRET_KEY}'}</strong>`という名前の環境変数を上記の値で追加してください。",
|
||||
"third": "Kestraインスタンスを再起動する"
|
||||
},
|
||||
"detected_env": "インスタンス開始時に識別されたシークレットタイプの環境変数は次のとおりです:",
|
||||
"empty_env": "まだ環境にSecretsがありません。",
|
||||
"message": "エンタープライズエディション (EE) では、UIで直接シークレットを追加、編集、または削除できます。インスタンスの再起動は不要です。シークレットをnamespaceごとに整理し、詳細なRBAC権限を設定し、親namespaceから子namespaceに継承します。EEは、HashiCorp Vault、AWS Secrets Manager、Azure Key Vault、Google Secret Manager、Elasticsearchなどのシークレットマネージャーと統合します。namespace、テナント、またはインスタンスごとに専用のバックエンドを設定して、チーム間でシークレットを分離するか、外部のvaultに保存された読み取り専用のシークレットを有効にします。シークレットは、保存時および転送時に暗号化されたままです。オプションのキャッシュによりAPIコールが削減され、監査証跡がすべてのアクセスをログに記録します。",
|
||||
"title": "シークレット管理をアップグレード"
|
||||
},
|
||||
@@ -547,6 +555,7 @@
|
||||
},
|
||||
"empty": "検索をまだ保存していません。",
|
||||
"label": "保存済み検索",
|
||||
"remove": "削除",
|
||||
"tooltip": "空の検索条件を保存することはできません。"
|
||||
},
|
||||
"settings": {
|
||||
@@ -1152,8 +1161,10 @@
|
||||
"templates deleted": "<code>{count}</code>件のテンプレートが削除されました",
|
||||
"templates deprecated": "テンプレートは非推奨です。代わりにsubflowsを使用してください。テンプレートからsubflowsへの移行方法については、<a href=\"https://kestra.io/docs/migration-guide/0.11.0/templates\" target=\"_blank\">移行セクション</a>を参照してください。",
|
||||
"templates exported": "テンプレートがエクスポートされました",
|
||||
"tenant": "Tenant",
|
||||
"tenants": "Tenants",
|
||||
"tenant": {
|
||||
"name": "テナント",
|
||||
"names": "テナント"
|
||||
},
|
||||
"theme": "テーマ",
|
||||
"this_task_has": "このtaskは",
|
||||
"timezone": "タイムゾーン",
|
||||
|
||||
@@ -303,6 +303,14 @@
|
||||
}
|
||||
},
|
||||
"secrets": {
|
||||
"add_env": {
|
||||
"first": "<strong><a href=\"https://kestra.io/docs/how-to-guides/secrets#using-secrets-in-kestra\" target=\"_blank\">비밀 값 Base64로 인코딩</a></strong>",
|
||||
"intro": "새 Secret을 생성하려면:",
|
||||
"second": "위의 값을 사용하여 '<strong>SECRET_{'{MY_SECRET_KEY}'}</strong>'라는 환경 변수를 추가하세요.",
|
||||
"third": "Kestra 인스턴스를 재시작하십시오"
|
||||
},
|
||||
"detected_env": "다음은 인스턴스 시작 시 식별된 secret-type 환경 변수입니다:",
|
||||
"empty_env": "아직 환경에 Secrets가 없습니다.",
|
||||
"message": "Enterprise Edition (EE)는 UI에서 비밀을 직접 추가, 편집 또는 삭제할 수 있게 해줍니다. 인스턴스를 재시작할 필요가 없습니다. 비밀을 namespace별로 세분화된 RBAC 권한으로 구성하고, 상위 namespace에서 하위 namespace로 상속할 수 있습니다. EE는 HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, Google Secret Manager, Elasticsearch와 같은 비밀 관리자와 통합됩니다. namespace, tenant, 인스턴스별로 전용 백엔드를 설정하여 팀 간 비밀을 격리하거나 외부 vault에 저장된 읽기 전용 비밀을 활성화할 수 있습니다. 비밀은 저장 중 및 전송 중에 암호화된 상태로 유지됩니다. 선택적 캐싱은 API 호출을 줄이고, 감사 추적은 모든 접근을 log합니다.",
|
||||
"title": "비밀 관리 업그레이드"
|
||||
},
|
||||
@@ -547,6 +555,7 @@
|
||||
},
|
||||
"empty": "아직 검색을 저장하지 않았습니다.",
|
||||
"label": "저장된 검색",
|
||||
"remove": "제거",
|
||||
"tooltip": "빈 검색 기준을 저장할 수 없습니다."
|
||||
},
|
||||
"settings": {
|
||||
@@ -1152,8 +1161,10 @@
|
||||
"templates deleted": "<code>{count}</code> 개의 템플릿이 삭제되었습니다",
|
||||
"templates deprecated": "템플릿은 사용 중지되었습니다. 대신 subflows를 사용하세요. 템플릿에서 subflows로 마이그레이션하는 방법을 설명하는 <a href=\"https://kestra.io/docs/migration-guide/0.11.0/templates\" target=\"_blank\">마이그레이션 섹션</a>을 참조하세요.",
|
||||
"templates exported": "템플릿이 내보내졌습니다",
|
||||
"tenant": "Tenant",
|
||||
"tenants": "Tenants",
|
||||
"tenant": {
|
||||
"name": "테넌트",
|
||||
"names": "테넌트"
|
||||
},
|
||||
"theme": "테마",
|
||||
"this_task_has": "이 task는",
|
||||
"timezone": "시간대",
|
||||
|
||||
@@ -303,6 +303,14 @@
|
||||
}
|
||||
},
|
||||
"secrets": {
|
||||
"add_env": {
|
||||
"first": "Zakoduj <strong><a href=\"https://kestra.io/docs/how-to-guides/secrets#using-secrets-in-kestra\" target=\"_blank\">wartość secret w Base64</a></strong>",
|
||||
"intro": "Aby utworzyć nowy Secret:",
|
||||
"second": "Dodaj zmienną środowiskową o nazwie '<strong>SECRET_{'{MY_SECRET_KEY}'}</strong>' z powyższą wartością",
|
||||
"third": "Uruchom ponownie instancję Kestra"
|
||||
},
|
||||
"detected_env": "Oto zmienne środowiskowe typu secret zidentyfikowane podczas uruchamiania instancji:",
|
||||
"empty_env": "Nie masz jeszcze żadnych Secrets w swoim środowisku.",
|
||||
"message": "Edycja Enterprise (EE) pozwala na dodawanie, edytowanie lub usuwanie sekretów bezpośrednio w UI — bez potrzeby ponownego uruchamiania instancji. Organizuj sekrety według namespace z precyzyjnymi uprawnieniami RBAC i dziedzicz je z namespace nadrzędnych do podrzędnych. EE integruje się z menedżerami sekretów, takimi jak HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, Google Secret Manager i Elasticsearch. Skonfiguruj dedykowane backendy dla każdego namespace, mandanta lub instancji, aby izolować sekrety w zespołach, lub włącz sekrety tylko do odczytu przechowywane w zewnętrznych vaultach. Sekrety pozostają zaszyfrowane w spoczynku i podczas przesyłania. Opcjonalne buforowanie zmniejsza liczbę wywołań API, a ścieżki audytu logują cały dostęp.",
|
||||
"title": "Ulepsz swoje zarządzanie Secrets Management"
|
||||
},
|
||||
@@ -547,6 +555,7 @@
|
||||
},
|
||||
"empty": "Nie zapisałeś jeszcze żadnego wyszukiwania.",
|
||||
"label": "Zapisane wyszukiwania",
|
||||
"remove": "Usuń",
|
||||
"tooltip": "Nie można zapisać pustych kryteriów wyszukiwania."
|
||||
},
|
||||
"settings": {
|
||||
@@ -1152,8 +1161,10 @@
|
||||
"templates deleted": "<code>{count}</code> Szablon(y) usunięte",
|
||||
"templates deprecated": "Szablony są przestarzałe. Proszę używać subflows. Zobacz <a href=\"https://kestra.io/docs/migration-guide/0.11.0/templates\" target=\"_blank\">sekcję migracji</a>, aby dowiedzieć się, jak migrować z szablonów do subflows.",
|
||||
"templates exported": "Szablony wyeksportowane",
|
||||
"tenant": "Tenant",
|
||||
"tenants": "Tenants",
|
||||
"tenant": {
|
||||
"name": "Mandant",
|
||||
"names": "Najemcy"
|
||||
},
|
||||
"theme": "Motyw",
|
||||
"this_task_has": "To zadanie ma",
|
||||
"timezone": "Strefa czasowa",
|
||||
|
||||
@@ -303,6 +303,14 @@
|
||||
}
|
||||
},
|
||||
"secrets": {
|
||||
"add_env": {
|
||||
"first": "Codifique o <strong><a href=\"https://kestra.io/docs/how-to-guides/secrets#using-secrets-in-kestra\" target=\"_blank\">valor secreto em Base64</a></strong>",
|
||||
"intro": "Para criar um novo Secret:",
|
||||
"second": "Adicione uma variável de ambiente chamada '<strong>SECRET_{'{MY_SECRET_KEY}'}</strong>' com o valor acima",
|
||||
"third": "Reinicie sua instância do Kestra"
|
||||
},
|
||||
"detected_env": "Aqui estão as variáveis de ambiente do tipo secreto identificadas no momento de inicialização da instância:",
|
||||
"empty_env": "Você ainda não tem nenhum Secret no seu ambiente.",
|
||||
"message": "A Enterprise Edition (EE) permite adicionar, editar ou excluir segredos diretamente na UI—sem necessidade de reiniciar a instância. Organize segredos por namespace com permissões RBAC granulares e herde-os de namespaces pai para filho. A EE integra-se com gerenciadores de segredos como HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, Google Secret Manager e Elasticsearch. Configure backends dedicados por namespace, tenant ou instância para isolar segredos entre equipes, ou habilite segredos somente leitura armazenados em cofres externos. Os segredos permanecem criptografados em repouso e em trânsito. O cache opcional reduz chamadas de API, e trilhas de auditoria registram todo o acesso.",
|
||||
"title": "Atualize seu Gerenciamento de Secrets"
|
||||
},
|
||||
@@ -547,6 +555,7 @@
|
||||
},
|
||||
"empty": "Você ainda não salvou nenhuma pesquisa.",
|
||||
"label": "Pesquisas salvas",
|
||||
"remove": "Remover",
|
||||
"tooltip": "Você não pode salvar critérios de pesquisa vazios."
|
||||
},
|
||||
"settings": {
|
||||
@@ -1152,8 +1161,10 @@
|
||||
"templates deleted": "<code>{count}</code> Template(s) deletado(s)",
|
||||
"templates deprecated": "Templates estão obsoletos. Por favor, use subflows. Veja a <a href=\"https://kestra.io/docs/migration-guide/0.11.0/templates\" target=\"_blank\">Seção de Migrações</a> explicando como você pode migrar de templates para subflows.",
|
||||
"templates exported": "Templates exportados",
|
||||
"tenant": "Tenant",
|
||||
"tenants": "Tenants",
|
||||
"tenant": {
|
||||
"name": "Mandante",
|
||||
"names": "Mandantes"
|
||||
},
|
||||
"theme": "Tema",
|
||||
"this_task_has": "Esta task tem",
|
||||
"timezone": "Fuso horário",
|
||||
|
||||
@@ -303,6 +303,14 @@
|
||||
}
|
||||
},
|
||||
"secrets": {
|
||||
"add_env": {
|
||||
"first": "Закодируйте <strong><a href=\"https://kestra.io/docs/how-to-guides/secrets#using-secrets-in-kestra\" target=\"_blank\">секретное значение в Base64</a></strong>",
|
||||
"intro": "Чтобы создать новый Secret:",
|
||||
"second": "Добавьте переменную окружения с именем '<strong>SECRET_{'{MY_SECRET_KEY}'}</strong>' с указанным выше значением",
|
||||
"third": "Перезапустите ваш экземпляр Kestra"
|
||||
},
|
||||
"detected_env": "Вот переменные окружения типа secret, определенные при запуске экземпляра:",
|
||||
"empty_env": "У вас еще нет Secrets в вашей среде.",
|
||||
"message": "Enterprise Edition (EE) позволяет добавлять, редактировать или удалять секреты непосредственно в UI — без необходимости перезапуска экземпляра. Организуйте секреты по namespace с детализированными RBAC-разрешениями и наследуйте их от родительских к дочерним namespace. EE интегрируется с менеджерами секретов, такими как HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, Google Secret Manager и Elasticsearch. Настройте выделенные бэкенды для каждого namespace, арендатора или экземпляра, чтобы изолировать секреты между командами, или включите только для чтения секреты, хранящиеся во внешних хранилищах. Секреты остаются зашифрованными в состоянии покоя и при передаче. Опциональное кэширование уменьшает количество API-запросов, а аудиторские следы фиксируют весь доступ.",
|
||||
"title": "Обновите управление Secrets"
|
||||
},
|
||||
@@ -547,6 +555,7 @@
|
||||
},
|
||||
"empty": "Вы еще не сохранили ни одного поиска.",
|
||||
"label": "Сохранённые поиски",
|
||||
"remove": "Удалить",
|
||||
"tooltip": "Вы не можете сохранить пустые критерии поиска."
|
||||
},
|
||||
"settings": {
|
||||
@@ -1152,8 +1161,10 @@
|
||||
"templates deleted": "<code>{count}</code> Шаблон(ы) удалены",
|
||||
"templates deprecated": "Шаблоны устарели. Пожалуйста, используйте subflows вместо них. См. <a href=\"https://kestra.io/docs/migration-guide/0.11.0/templates\" target=\"_blank\">раздел миграции</a>, объясняющий, как можно мигрировать с шаблонов на subflows.",
|
||||
"templates exported": "Шаблоны экспортированы",
|
||||
"tenant": "Tenant",
|
||||
"tenants": "Tenants",
|
||||
"tenant": {
|
||||
"name": "Мандант",
|
||||
"names": "Арендаторы"
|
||||
},
|
||||
"theme": "Тема",
|
||||
"this_task_has": "Эта задача имеет",
|
||||
"timezone": "Часовой пояс",
|
||||
|
||||
@@ -303,6 +303,14 @@
|
||||
}
|
||||
},
|
||||
"secrets": {
|
||||
"add_env": {
|
||||
"first": "将<strong><a href=\"https://kestra.io/docs/how-to-guides/secrets#using-secrets-in-kestra\" target=\"_blank\">secret value 编码为 Base64</a></strong>",
|
||||
"intro": "创建新Secret:",
|
||||
"second": "添加一个名为 '<strong>SECRET_{'{MY_SECRET_KEY}'}</strong>' 的环境变量,并使用上述值",
|
||||
"third": "重新启动您的Kestra实例"
|
||||
},
|
||||
"detected_env": "以下是在实例启动时识别的secret-type环境变量:",
|
||||
"empty_env": "您还没有在您的环境中添加任何Secrets。",
|
||||
"message": "企业版 (EE) 允许您直接在UI中添加、编辑或删除秘密,无需重启实例。通过细粒度的RBAC权限按namespace组织秘密,并从父namespace继承到子namespace。EE与HashiCorp Vault、AWS Secrets Manager、Azure Key Vault、Google Secret Manager和Elasticsearch等秘密管理器集成。为每个namespace、tenant或实例设置专用后端,以隔离团队之间的秘密,或启用存储在外部vault中的只读秘密。秘密在静态和传输中保持加密。可选的缓存减少API调用,并且审计跟踪记录所有访问。",
|
||||
"title": "升级您的Secrets管理"
|
||||
},
|
||||
@@ -547,6 +555,7 @@
|
||||
},
|
||||
"empty": "您还没有保存任何搜索。",
|
||||
"label": "已保存的搜索",
|
||||
"remove": "移除",
|
||||
"tooltip": "您不能保存空的搜索条件。"
|
||||
},
|
||||
"settings": {
|
||||
@@ -1152,8 +1161,10 @@
|
||||
"templates deleted": "<code>{count}</code> 个模板已删除",
|
||||
"templates deprecated": "模板已弃用。请使用子流程代替。查看 <a href=\"https://kestra.io/docs/migration-guide/0.11.0/templates\" target=\"_blank\">迁移部分</a> 了解如何从模板迁移到子流程。",
|
||||
"templates exported": "模板已导出",
|
||||
"tenant": "租户",
|
||||
"tenants": "租户",
|
||||
"tenant": {
|
||||
"name": "租户",
|
||||
"names": "租户"
|
||||
},
|
||||
"theme": "主题",
|
||||
"this_task_has": "此task已",
|
||||
"timezone": "时区",
|
||||
|
||||
@@ -309,6 +309,8 @@ public class ExecutionController {
|
||||
|
||||
private String runContextRender(Flow flow, Task task, Execution execution, TaskRun taskRun, String expression) throws IllegalVariableEvaluationException {
|
||||
RunContext runContext = runContextFactory.of(flow, task, execution, taskRun, false);
|
||||
String baseRender = runContext.render(expression);
|
||||
|
||||
if (expression.contains("{")) { // fast backoff from regex
|
||||
Matcher matcher = SECRET_FUNCTION.matcher(expression);
|
||||
String maskedExpression = expression;
|
||||
@@ -318,7 +320,8 @@ public class ExecutionController {
|
||||
}
|
||||
return runContext.render(maskedExpression);
|
||||
}
|
||||
return runContext.render(expression);
|
||||
|
||||
return baseRender;
|
||||
}
|
||||
|
||||
@SuperBuilder
|
||||
|
||||
@@ -4,16 +4,19 @@ import io.kestra.core.models.namespaces.Namespace;
|
||||
import io.kestra.core.models.topologies.FlowTopologyGraph;
|
||||
import io.kestra.core.repositories.ArrayListTotal;
|
||||
import io.kestra.core.repositories.FlowRepositoryInterface;
|
||||
import io.kestra.core.utils.NamespaceUtils;
|
||||
import io.kestra.core.tenant.TenantService;
|
||||
import io.kestra.core.topologies.FlowTopologyService;
|
||||
import io.kestra.core.utils.NamespaceUtils;
|
||||
import io.kestra.webserver.models.namespaces.NamespaceWithDisabled;
|
||||
import io.kestra.webserver.responses.PagedResults;
|
||||
import io.kestra.webserver.utils.PageableUtils;
|
||||
import io.micronaut.core.annotation.Nullable;
|
||||
import io.micronaut.data.model.Pageable;
|
||||
import io.micronaut.data.model.Sort;
|
||||
import io.micronaut.http.annotation.*;
|
||||
import io.micronaut.http.annotation.Controller;
|
||||
import io.micronaut.http.annotation.Get;
|
||||
import io.micronaut.http.annotation.PathVariable;
|
||||
import io.micronaut.http.annotation.QueryValue;
|
||||
import io.micronaut.http.exceptions.HttpStatusException;
|
||||
import io.micronaut.scheduling.TaskExecutors;
|
||||
import io.micronaut.scheduling.annotation.ExecuteOn;
|
||||
@@ -23,7 +26,9 @@ import io.swagger.v3.oas.annotations.Parameter;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.validation.constraints.Min;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Validated
|
||||
@@ -80,9 +85,13 @@ public class NamespaceController implements NamespaceControllerInterface<Namespa
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
Pageable pageable = PageableUtils.from(page, size, sort);
|
||||
|
||||
var total = distinctNamespaces.size();
|
||||
|
||||
Pageable pageable = PageableUtils.from(page, size, sort);
|
||||
if (total <= (pageable.getOffset() - pageable.getSize())) {
|
||||
return PagedResults.of(new ArrayListTotal<>(0));
|
||||
}
|
||||
|
||||
if (sort != null) {
|
||||
Sort.Order.Direction direction = pageable.getSort().getOrderBy().getFirst().getDirection();
|
||||
|
||||
@@ -1488,11 +1488,23 @@ class ExecutionControllerRunnerTest {
|
||||
void shouldMaskSecretWhenEvalPebbleExpression(Execution execution) {
|
||||
ExecutionController.EvalResult evalResult = client.toBlocking().retrieve(
|
||||
HttpRequest
|
||||
.POST("/api/v1/executions/" + execution.getId() + "/eval/" + execution.getTaskRunList().getFirst().getId(), "{{ secret('KEY') }}")
|
||||
.POST("/api/v1/executions/" + execution.getId() + "/eval/" + execution.getTaskRunList().getFirst().getId(), "{{ secret('MY_SECRET') }}")
|
||||
.contentType(MediaType.TEXT_PLAIN),
|
||||
ExecutionController.EvalResult.class
|
||||
);
|
||||
assertThat(evalResult.getError(), nullValue());
|
||||
assertThat(evalResult.getStackTrace(), nullValue());
|
||||
assertThat(evalResult.getResult(), is("******"));
|
||||
|
||||
evalResult = client.toBlocking().retrieve(
|
||||
HttpRequest
|
||||
.POST("/api/v1/executions/" + execution.getId() + "/eval/" + execution.getTaskRunList().getFirst().getId(), "{{ secret('NON_EXISTING_KEY') }}")
|
||||
.contentType(MediaType.TEXT_PLAIN),
|
||||
ExecutionController.EvalResult.class
|
||||
);
|
||||
assertThat(evalResult.getError(), is("io.pebbletemplates.pebble.error.PebbleException: Cannot find secret for key 'NON_EXISTING_KEY'. ({{ secret('NON_EXISTING_KEY') }}:1)"));
|
||||
assertThat(evalResult.getStackTrace(), startsWith("io.kestra.core.exceptions.IllegalVariableEvaluationException: io.pebbletemplates.pebble.error.PebbleException: Cannot find secret for key 'NON_EXISTING_KEY'. ({{ secret('NON_EXISTING_KEY') }}:1)"));
|
||||
assertThat(evalResult.getResult(), is(nullValue()));
|
||||
}
|
||||
|
||||
private ExecutionController.EvalResult eval(Execution execution, String expression, int index) {
|
||||
|
||||
@@ -103,6 +103,13 @@ public class NamespaceControllerTest {
|
||||
);
|
||||
assertThat(list.getTotal(), is(3L));
|
||||
assertThat(list.getResults().size(), is(3));
|
||||
|
||||
list = client.toBlocking().retrieve(
|
||||
HttpRequest.GET("/api/v1/namespaces/search?page=4&size=2&sort=id:desc"),
|
||||
Argument.of(PagedResults.class, NamespaceWithDisabled.class)
|
||||
);
|
||||
assertThat(list.getTotal(), is(0L));
|
||||
assertThat(list.getResults(), empty());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user