mirror of
https://github.com/kestra-io/kestra.git
synced 2025-12-26 14:00:23 -05:00
Compare commits
46 Commits
dependabot
...
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
|
required: true
|
||||||
|
|
||||||
jobs:
|
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:
|
publish:
|
||||||
name: Publish - Docker
|
name: Publish - Docker
|
||||||
needs: build-artifacts
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
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."
|
description: "The Sonatype GPG file."
|
||||||
required: true
|
required: true
|
||||||
jobs:
|
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:
|
Docker:
|
||||||
name: Publish Docker
|
name: Publish Docker
|
||||||
|
needs: build-artifacts
|
||||||
uses: ./.github/workflows/workflow-publish-docker.yml
|
uses: ./.github/workflows/workflow-publish-docker.yml
|
||||||
|
with:
|
||||||
|
plugin-version: ${{ github.event.inputs.plugin-version != null && github.event.inputs.plugin-version || 'LATEST' }}
|
||||||
secrets:
|
secrets:
|
||||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }}
|
DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||||
@@ -57,6 +66,7 @@ jobs:
|
|||||||
|
|
||||||
Github:
|
Github:
|
||||||
name: Github Release
|
name: Github Release
|
||||||
|
needs: build-artifacts
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
uses: ./.github/workflows/workflow-github-release.yml
|
uses: ./.github/workflows/workflow-github-release.yml
|
||||||
secrets:
|
secrets:
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import picocli.CommandLine;
|
|||||||
FlowNamespaceCommand.class,
|
FlowNamespaceCommand.class,
|
||||||
FlowDotCommand.class,
|
FlowDotCommand.class,
|
||||||
FlowExportCommand.class,
|
FlowExportCommand.class,
|
||||||
|
FlowUpdateCommand.class,
|
||||||
|
FlowUpdatesCommand.class
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import picocli.CommandLine.Command;
|
|||||||
mixinStandardHelpOptions = true,
|
mixinStandardHelpOptions = true,
|
||||||
subcommands = {
|
subcommands = {
|
||||||
PluginInstallCommand.class,
|
PluginInstallCommand.class,
|
||||||
|
PluginUninstallCommand.class,
|
||||||
PluginListCommand.class,
|
PluginListCommand.class,
|
||||||
PluginDocCommand.class,
|
PluginDocCommand.class,
|
||||||
PluginSearchCommand.class
|
PluginSearchCommand.class
|
||||||
|
|||||||
@@ -17,10 +17,10 @@ import java.util.List;
|
|||||||
|
|
||||||
@CommandLine.Command(
|
@CommandLine.Command(
|
||||||
name = "uninstall",
|
name = "uninstall",
|
||||||
description = "uninstall a plugin"
|
description = "Uninstall plugins"
|
||||||
)
|
)
|
||||||
public class PluginUninstallCommand extends AbstractCommand {
|
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<>();
|
List<String> dependencies = new ArrayList<>();
|
||||||
|
|
||||||
@Spec
|
@Spec
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package io.kestra.cli.commands.servers;
|
|||||||
|
|
||||||
import com.google.common.collect.ImmutableMap;
|
import com.google.common.collect.ImmutableMap;
|
||||||
import io.kestra.cli.services.FileChangedEventListener;
|
import io.kestra.cli.services.FileChangedEventListener;
|
||||||
|
import io.kestra.core.contexts.KestraContext;
|
||||||
import io.kestra.core.models.ServerType;
|
import io.kestra.core.models.ServerType;
|
||||||
import io.kestra.core.repositories.LocalFlowRepositoryLoader;
|
import io.kestra.core.repositories.LocalFlowRepositoryLoader;
|
||||||
import io.kestra.core.runners.StandAloneRunner;
|
import io.kestra.core.runners.StandAloneRunner;
|
||||||
@@ -88,9 +89,10 @@ public class StandAloneCommand extends AbstractServerCommand {
|
|||||||
this.skipExecutionService.setSkipFlows(skipFlows);
|
this.skipExecutionService.setSkipFlows(skipFlows);
|
||||||
this.skipExecutionService.setSkipNamespaces(skipNamespaces);
|
this.skipExecutionService.setSkipNamespaces(skipNamespaces);
|
||||||
this.skipExecutionService.setSkipTenants(skipTenants);
|
this.skipExecutionService.setSkipTenants(skipTenants);
|
||||||
|
|
||||||
this.startExecutorService.applyOptions(startExecutors, notStartExecutors);
|
this.startExecutorService.applyOptions(startExecutors, notStartExecutors);
|
||||||
|
|
||||||
|
KestraContext.getContext().injectWorkerConfigs(workerThread, null);
|
||||||
|
|
||||||
super.call();
|
super.call();
|
||||||
|
|
||||||
if (flowPath != null) {
|
if (flowPath != null) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package io.kestra.cli.commands.servers;
|
package io.kestra.cli.commands.servers;
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableMap;
|
import com.google.common.collect.ImmutableMap;
|
||||||
|
import io.kestra.core.contexts.KestraContext;
|
||||||
import io.kestra.core.models.ServerType;
|
import io.kestra.core.models.ServerType;
|
||||||
import io.kestra.core.runners.Worker;
|
import io.kestra.core.runners.Worker;
|
||||||
import io.kestra.core.utils.Await;
|
import io.kestra.core.utils.Await;
|
||||||
@@ -36,7 +37,11 @@ public class WorkerCommand extends AbstractServerCommand {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Integer call() throws Exception {
|
public Integer call() throws Exception {
|
||||||
|
|
||||||
|
KestraContext.getContext().injectWorkerConfigs(thread, workerGroupKey);
|
||||||
|
|
||||||
super.call();
|
super.call();
|
||||||
|
|
||||||
if (this.workerGroupKey != null && !this.workerGroupKey.matches("[a-zA-Z0-9_-]+")) {
|
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");
|
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.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
@@ -27,6 +29,10 @@ public abstract class KestraContext {
|
|||||||
// Properties
|
// Properties
|
||||||
public static final String KESTRA_SERVER_TYPE = "kestra.server-type";
|
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}.
|
* Gets the current {@link KestraContext}.
|
||||||
*
|
*
|
||||||
@@ -54,6 +60,12 @@ public abstract class KestraContext {
|
|||||||
*/
|
*/
|
||||||
public abstract ServerType getServerType();
|
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.
|
* Returns the Kestra Version.
|
||||||
*
|
*
|
||||||
@@ -110,6 +122,34 @@ public abstract class KestraContext {
|
|||||||
.orElse(ServerType.STANDALONE);
|
.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} **/
|
/** {@inheritDoc} **/
|
||||||
@Override
|
@Override
|
||||||
public void shutdown() {
|
public void shutdown() {
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ public class JsonSchemaGenerator {
|
|||||||
objectNode.put("type", "array");
|
objectNode.put("type", "array");
|
||||||
}
|
}
|
||||||
replaceAnyOfWithOneOf(objectNode);
|
replaceAnyOfWithOneOf(objectNode);
|
||||||
pullOfDefaultFromOneOf(objectNode);
|
pullDocumentationAndDefaultFromOneOf(objectNode);
|
||||||
removeRequiredOnPropsWithDefaults(objectNode);
|
removeRequiredOnPropsWithDefaults(objectNode);
|
||||||
|
|
||||||
return JacksonMapper.toMap(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.
|
// 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.
|
// 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.
|
// 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 -> {
|
objectNode.findParents("oneOf").forEach(jsonNode -> {
|
||||||
if (jsonNode instanceof ObjectNode oNode) {
|
if (jsonNode instanceof ObjectNode oNode) {
|
||||||
JsonNode oneOf = oNode.get("oneOf");
|
JsonNode oneOf = oNode.get("oneOf");
|
||||||
if (oneOf instanceof ArrayNode arrayNode) {
|
if (oneOf instanceof ArrayNode arrayNode) {
|
||||||
Iterator<JsonNode> it = arrayNode.elements();
|
Iterator<JsonNode> it = arrayNode.elements();
|
||||||
JsonNode defaultNode = null;
|
var nodesToPullUp = new HashMap<String, Optional<JsonNode>>(Map.ofEntries(
|
||||||
while (it.hasNext() && defaultNode == null) {
|
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();
|
JsonNode next = it.next();
|
||||||
if (next instanceof ObjectNode nextAsObj) {
|
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) {
|
// create nodes on parent
|
||||||
oNode.set("default", defaultNode);
|
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 {
|
try {
|
||||||
ObjectNode objectNode = generator.generateSchema(cls);
|
ObjectNode objectNode = generator.generateSchema(cls);
|
||||||
replaceAnyOfWithOneOf(objectNode);
|
replaceAnyOfWithOneOf(objectNode);
|
||||||
pullOfDefaultFromOneOf(objectNode);
|
pullDocumentationAndDefaultFromOneOf(objectNode);
|
||||||
removeRequiredOnPropsWithDefaults(objectNode);
|
removeRequiredOnPropsWithDefaults(objectNode);
|
||||||
|
|
||||||
return JacksonMapper.toMap(extractMainRef(objectNode));
|
return JacksonMapper.toMap(extractMainRef(objectNode));
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ public enum CrudEventType {
|
|||||||
LOGIN,
|
LOGIN,
|
||||||
LOGOUT,
|
LOGOUT,
|
||||||
IMPERSONATE,
|
IMPERSONATE,
|
||||||
LOGIN_FAILURE
|
LOGIN_FAILURE,
|
||||||
|
ACCOUNT_LOCKED
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ public class RunContextResponseInterceptor implements HttpResponseInterceptor {
|
|||||||
response instanceof BasicClassicHttpResponse httpResponse
|
response instanceof BasicClassicHttpResponse httpResponse
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
|
// FIXME temporary fix for https://github.com/kestra-io/kestra/issues/8092
|
||||||
runContext.logger().debug(
|
runContext.logger().debug(
|
||||||
"Request '{}' from '{}' with the response code '{}'",
|
"Request " + httpClientContext.getRequest().getUri() + " from '{}' with the response code '{}'",
|
||||||
httpClientContext.getRequest().getUri(),
|
|
||||||
httpClientContext.getEndpointDetails().getRemoteAddress(),
|
httpClientContext.getEndpointDetails().getRemoteAddress(),
|
||||||
response.getCode()
|
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 RESTARTED = SYSTEM_PREFIX + "restarted";
|
||||||
public static final String REPLAY = SYSTEM_PREFIX + "replay";
|
public static final String REPLAY = SYSTEM_PREFIX + "replay";
|
||||||
public static final String REPLAYED = SYSTEM_PREFIX + "replayed";
|
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.
|
* 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 {
|
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.")
|
@Schema(title = "The version of the plugin to use.")
|
||||||
String getVersion();
|
String getVersion();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ public record QueryFilter(
|
|||||||
NAMESPACE("namespace") {
|
NAMESPACE("namespace") {
|
||||||
@Override
|
@Override
|
||||||
public List<Op> supportedOp() {
|
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") {
|
LABELS("labels") {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ public record PluginArtifact(
|
|||||||
"([^: ]+):([^: ]+)(:([^: ]*)(:([^: ]+))?)?:([^: ]+)"
|
"([^: ]+):([^: ]+)(:([^: ]*)(:([^: ]+))?)?:([^: ]+)"
|
||||||
);
|
);
|
||||||
private static final Pattern FILENAME_PATTERN = Pattern.compile(
|
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";
|
public static final String JAR_EXTENSION = "jar";
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ public interface ExecutionRepositoryInterface extends SaveRepositoryInterface<Ex
|
|||||||
boolean allowDeleted
|
boolean allowDeleted
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Flux<Execution> findAllAsync(@Nullable String tenantId);
|
||||||
|
|
||||||
ArrayListTotal<TaskRun> findTaskRun(
|
ArrayListTotal<TaskRun> findTaskRun(
|
||||||
Pageable pageable,
|
Pageable pageable,
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ public interface LogRepositoryInterface extends SaveRepositoryInterface<LogEntry
|
|||||||
ZonedDateTime startDate
|
ZonedDateTime startDate
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Flux<LogEntry> findAllAsync(@Nullable String tenantId);
|
||||||
|
|
||||||
List<LogStatistics> statistics(
|
List<LogStatistics> statistics(
|
||||||
@Nullable String query,
|
@Nullable String query,
|
||||||
@Nullable String tenantId,
|
@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.kestra.plugin.core.dashboard.data.Metrics;
|
||||||
import io.micronaut.core.annotation.Nullable;
|
import io.micronaut.core.annotation.Nullable;
|
||||||
import io.micronaut.data.model.Pageable;
|
import io.micronaut.data.model.Pageable;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
|
||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -28,6 +29,8 @@ public interface MetricRepositoryInterface extends SaveRepositoryInterface<Metri
|
|||||||
|
|
||||||
Integer purge(Execution execution);
|
Integer purge(Execution execution);
|
||||||
|
|
||||||
|
Flux<MetricEntry> findAllAsync(@Nullable String tenantId);
|
||||||
|
|
||||||
default Function<String, String> sortMapping() throws IllegalArgumentException {
|
default Function<String, String> sortMapping() throws IllegalArgumentException {
|
||||||
return s -> s;
|
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);
|
return objectMapper.writerFor(type).writeValues(writer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -132,6 +132,15 @@ public class FlowService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<String> warnings = new ArrayList<>(checkValidSubflows(flow, tenantId));
|
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;
|
return warnings;
|
||||||
}
|
}
|
||||||
@@ -140,7 +149,11 @@ public class FlowService {
|
|||||||
try {
|
try {
|
||||||
Map<String, Class<?>> aliases = pluginRegistry.plugins().stream()
|
Map<String, Class<?>> aliases = pluginRegistry.plugins().stream()
|
||||||
.flatMap(plugin -> plugin.getAliases().values().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);
|
Map<String, Object> stringObjectMap = JacksonMapper.ofYaml().readValue(flowSource, JacksonMapper.MAP_TYPE_REFERENCE);
|
||||||
return relocations(aliases, stringObjectMap);
|
return relocations(aliases, stringObjectMap);
|
||||||
} catch (JsonProcessingException e) {
|
} catch (JsonProcessingException e) {
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package io.kestra.core.services;
|
package io.kestra.core.services;
|
||||||
|
|
||||||
import io.kestra.core.repositories.FlowRepositoryInterface;
|
import io.kestra.core.repositories.FlowRepositoryInterface;
|
||||||
|
import io.kestra.core.utils.NamespaceUtils;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
import jakarta.inject.Singleton;
|
import jakarta.inject.Singleton;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
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 tenant The tenant ID
|
||||||
* @param namespace The namespace - cannot be null.
|
* @param namespace The namespace - cannot be null.
|
||||||
@@ -29,7 +31,10 @@ public class NamespaceService {
|
|||||||
Objects.requireNonNull(namespace, "namespace cannot be null");
|
Objects.requireNonNull(namespace, "namespace cannot be null");
|
||||||
|
|
||||||
if (flowRepository.isPresent()) {
|
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 namespaces.stream().anyMatch(ns -> ns.equals(namespace) || ns.startsWith(namespace));
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import java.util.stream.Stream;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
@Singleton
|
@Singleton
|
||||||
public class FlowTopologyService {
|
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
|
@Inject
|
||||||
protected ConditionService conditionService;
|
protected ConditionService conditionService;
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
package io.kestra.core.utils;
|
package io.kestra.core.utils;
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||||
|
@Slf4j
|
||||||
public class MapUtils {
|
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) {
|
public static Map<String, Object> merge(Map<String, Object> a, Map<String, Object> b) {
|
||||||
if (a == null && b == null) {
|
if (a == null && b == null) {
|
||||||
return 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.
|
* @return the nested map.
|
||||||
*
|
*
|
||||||
* @throws IllegalArgumentException if the given map contains conflicting keys.
|
* @throws IllegalArgumentException if the given map contains conflicting keys.
|
||||||
@@ -156,13 +160,15 @@ public class MapUtils {
|
|||||||
currentMap.put(key, new HashMap<>());
|
currentMap.put(key, new HashMap<>());
|
||||||
} else if (!(currentMap.get(key) instanceof Map)) {
|
} else if (!(currentMap.get(key) instanceof Map)) {
|
||||||
var invalidKey = String.join(",", Arrays.copyOfRange(keys, 0, i));
|
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);
|
currentMap = (Map<String, Object>) currentMap.get(key);
|
||||||
}
|
}
|
||||||
String lastKey = keys[keys.length - 1];
|
String lastKey = keys[keys.length - 1];
|
||||||
if (currentMap.containsKey(lastKey)) {
|
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());
|
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.IllegalVariableEvaluationException;
|
||||||
import io.kestra.core.exceptions.InternalException;
|
import io.kestra.core.exceptions.InternalException;
|
||||||
import io.kestra.core.models.Label;
|
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.annotations.PluginProperty;
|
||||||
import io.kestra.core.models.conditions.Condition;
|
import io.kestra.core.models.conditions.Condition;
|
||||||
import io.kestra.core.models.conditions.ConditionContext;
|
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.TimeWindow;
|
||||||
import io.kestra.core.models.triggers.TriggerOutput;
|
|
||||||
import io.kestra.core.models.triggers.multipleflows.MultipleCondition;
|
import io.kestra.core.models.triggers.multipleflows.MultipleCondition;
|
||||||
import io.kestra.core.models.triggers.multipleflows.MultipleConditionStorageInterface;
|
import io.kestra.core.models.triggers.multipleflows.MultipleConditionStorageInterface;
|
||||||
import io.kestra.core.models.triggers.multipleflows.MultipleConditionWindow;
|
import io.kestra.core.models.triggers.multipleflows.MultipleConditionWindow;
|
||||||
import io.kestra.core.runners.RunContext;
|
import io.kestra.core.runners.RunContext;
|
||||||
import io.kestra.core.services.LabelService;
|
import io.kestra.core.services.LabelService;
|
||||||
import io.kestra.core.utils.IdUtils;
|
|
||||||
import io.kestra.core.utils.ListUtils;
|
import io.kestra.core.utils.ListUtils;
|
||||||
import io.kestra.core.utils.MapUtils;
|
import io.kestra.core.utils.MapUtils;
|
||||||
import io.kestra.core.utils.TruthUtils;
|
import io.kestra.core.utils.TruthUtils;
|
||||||
import io.kestra.core.validations.ConditionValidation;
|
|
||||||
import io.kestra.core.validations.PreconditionFilterValidation;
|
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.Hidden;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import jakarta.validation.constraints.NotEmpty;
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
import jakarta.validation.constraints.NotNull;
|
|
||||||
import jakarta.validation.constraints.Pattern;
|
import jakarta.validation.constraints.Pattern;
|
||||||
import lombok.*;
|
import lombok.*;
|
||||||
import lombok.experimental.SuperBuilder;
|
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 lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.stream.Streams;
|
import org.apache.commons.lang3.stream.Streams;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.List;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
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.topologies.FlowTopologyService.SIMULATED_EXECUTION;
|
||||||
import static io.kestra.core.utils.Rethrow.throwPredicate;
|
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"
|
aliases = "io.kestra.core.models.triggers.types.Flow"
|
||||||
)
|
)
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@ConditionValidation
|
|
||||||
public class Flow extends AbstractTrigger implements TriggerOutput<Flow.Output> {
|
public class Flow extends AbstractTrigger implements TriggerOutput<Flow.Output> {
|
||||||
private static final String TRIGGER_VAR = "trigger";
|
private static final String TRIGGER_VAR = "trigger";
|
||||||
private static final String OUTPUTS_VAR = "outputs";
|
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"));
|
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")
|
@SuppressWarnings("unchecked")
|
||||||
@Test
|
@Test
|
||||||
void dashboard() throws URISyntaxException {
|
void dashboard() throws URISyntaxException {
|
||||||
@@ -314,10 +338,11 @@ class JsonSchemaGeneratorTest {
|
|||||||
private TestEnum testEnum;
|
private TestEnum testEnum;
|
||||||
|
|
||||||
@PluginProperty
|
@PluginProperty
|
||||||
@Schema(title = "Title from the attribute")
|
@Schema(title = "Title from the attribute", description = "Description from the attribute")
|
||||||
private TestClass testClass;
|
private TestClass testClass;
|
||||||
|
|
||||||
@PluginProperty(internalStorageURI = true)
|
@PluginProperty(internalStorageURI = true)
|
||||||
|
@Schema(title = "Title from the attribute", description = "Description from the attribute")
|
||||||
private String uri;
|
private String uri;
|
||||||
|
|
||||||
@PluginProperty
|
@PluginProperty
|
||||||
@@ -392,4 +417,49 @@ class JsonSchemaGeneratorTest {
|
|||||||
@NotNull
|
@NotNull
|
||||||
private Property<TaskWithEnum.TestClass> requiredWithNoDefault;
|
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());
|
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
|
@Test
|
||||||
void shouldThrowIllegalArgumentExceptionGivenInvalidFilenameMissingVersion() {
|
void shouldThrowIllegalArgumentExceptionGivenInvalidFilenameMissingVersion() {
|
||||||
String fileName = "io_kestra_plugin__plugin-serdes__custom-classifier.jar";
|
String fileName = "io_kestra_plugin__plugin-serdes__custom-classifier.jar";
|
||||||
|
|||||||
@@ -784,4 +784,12 @@ public abstract class AbstractExecutionRepositoryTest {
|
|||||||
.taskRunList(List.of())
|
.taskRunList(List.of())
|
||||||
.build();
|
.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 reactor.core.publisher.Flux;
|
||||||
|
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.hasSize;
|
||||||
import static org.hamcrest.Matchers.is;
|
import static org.hamcrest.Matchers.is;
|
||||||
|
|
||||||
@KestraTest
|
@KestraTest
|
||||||
@@ -205,7 +206,7 @@ public abstract class AbstractLogRepositoryTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void findAsych() {
|
void findAsync() {
|
||||||
logRepository.save(logEntry(Level.INFO).build());
|
logRepository.save(logEntry(Level.INFO).build());
|
||||||
logRepository.save(logEntry(Level.ERROR).build());
|
logRepository.save(logEntry(Level.ERROR).build());
|
||||||
logRepository.save(logEntry(Level.WARN).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);
|
Flux<LogEntry> find = logRepository.findAsync(null, "io.kestra.unittest", Level.INFO, startDate);
|
||||||
List<LogEntry> logEntries = find.collectList().block();
|
List<LogEntry> logEntries = find.collectList().block();
|
||||||
assertThat(logEntries.size(), is(3));
|
assertThat(logEntries, hasSize(3));
|
||||||
|
|
||||||
find = logRepository.findAsync(null, null, Level.ERROR, startDate);
|
find = logRepository.findAsync(null, null, Level.ERROR, startDate);
|
||||||
logEntries = find.collectList().block();
|
logEntries = find.collectList().block();
|
||||||
assertThat(logEntries.size(), is(1));
|
assertThat(logEntries, hasSize(1));
|
||||||
|
|
||||||
find = logRepository.findAsync(null, "io.kestra.unused", Level.INFO, startDate);
|
find = logRepository.findAsync(null, "io.kestra.unused", Level.INFO, startDate);
|
||||||
logEntries = find.collectList().block();
|
logEntries = find.collectList().block();
|
||||||
assertThat(logEntries.size(), is(0));
|
assertThat(logEntries, hasSize(0));
|
||||||
|
|
||||||
find = logRepository.findAsync(null, null, Level.INFO, startDate.plusSeconds(2));
|
find = logRepository.findAsync(null, null, Level.INFO, startDate.plusSeconds(2));
|
||||||
logEntries = find.collectList().block();
|
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.micronaut.data.model.Pageable;
|
||||||
import io.kestra.core.junit.annotations.KestraTest;
|
import io.kestra.core.junit.annotations.KestraTest;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
import org.junit.jupiter.api.Disabled;
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
@@ -17,6 +16,7 @@ import java.time.ZonedDateTime;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.hasSize;
|
||||||
import static org.hamcrest.Matchers.is;
|
import static org.hamcrest.Matchers.is;
|
||||||
|
|
||||||
@KestraTest
|
@KestraTest
|
||||||
@@ -95,6 +95,20 @@ public abstract class AbstractMetricRepositoryTest {
|
|||||||
assertThat(tasksWithMetrics.size(), is(2));
|
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) {
|
private Counter counter(String metricName) {
|
||||||
return Counter.of(metricName, 1);
|
return Counter.of(metricName, 1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package io.kestra.core.runners;
|
|||||||
|
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
import static org.hamcrest.Matchers.*;
|
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.ExecuteFlow;
|
||||||
import io.kestra.core.junit.annotations.KestraTest;
|
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.Execution;
|
||||||
import io.kestra.core.models.executions.LogEntry;
|
import io.kestra.core.models.executions.LogEntry;
|
||||||
import io.kestra.core.models.flows.State;
|
import io.kestra.core.models.flows.State;
|
||||||
import io.kestra.core.queues.MessageTooBigException;
|
|
||||||
import io.kestra.core.queues.QueueException;
|
import io.kestra.core.queues.QueueException;
|
||||||
import io.kestra.core.queues.QueueFactoryInterface;
|
import io.kestra.core.queues.QueueFactoryInterface;
|
||||||
import io.kestra.core.queues.QueueInterface;
|
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.EachSequentialTest;
|
||||||
import io.kestra.plugin.core.flow.FlowCaseTest;
|
import io.kestra.plugin.core.flow.FlowCaseTest;
|
||||||
import io.kestra.plugin.core.flow.ForEachItemCaseTest;
|
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 io.kestra.plugin.core.flow.WorkingDirectoryTest;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
import jakarta.inject.Named;
|
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.Map;
|
||||||
import java.util.concurrent.CopyOnWriteArrayList;
|
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Tag;
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.TestInstance;
|
import org.junit.jupiter.api.TestInstance;
|
||||||
import org.junitpioneer.jupiter.RetryingTest;
|
import org.junitpioneer.jupiter.RetryingTest;
|
||||||
import org.slf4j.event.Level;
|
|
||||||
import reactor.core.publisher.Flux;
|
|
||||||
|
|
||||||
@KestraTest(startRunner = true)
|
@KestraTest(startRunner = true)
|
||||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
@@ -370,7 +359,7 @@ public abstract class AbstractRunnerTest {
|
|||||||
forEachItemCaseTest.forEachItemWithSubflowOutputs();
|
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"})
|
@LoadFlows({"flows/valids/restart-for-each-item.yaml", "flows/valids/restart-child.yaml"})
|
||||||
void restartForEachItem() throws Exception {
|
void restartForEachItem() throws Exception {
|
||||||
forEachItemCaseTest.restartForEachItem();
|
forEachItemCaseTest.restartForEachItem();
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import io.kestra.core.runners.FlowListeners;
|
|||||||
import io.kestra.core.utils.Await;
|
import io.kestra.core.utils.Await;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
import jakarta.inject.Named;
|
import jakarta.inject.Named;
|
||||||
|
import org.junit.jupiter.api.Disabled;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
|
|
||||||
@@ -525,6 +526,7 @@ public class SchedulerScheduleTest extends AbstractSchedulerTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@Disabled("too flaky on CI")
|
||||||
void recoverLASTLongRunningExecution() throws Exception {
|
void recoverLASTLongRunningExecution() throws Exception {
|
||||||
// mock flow listeners
|
// mock flow listeners
|
||||||
FlowListeners flowListenersServiceSpy = spy(this.flowListenersService);
|
FlowListeners flowListenersServiceSpy = spy(this.flowListenersService);
|
||||||
@@ -596,6 +598,7 @@ public class SchedulerScheduleTest extends AbstractSchedulerTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@Disabled("too flaky on CI")
|
||||||
void recoverNONELongRunningExecution() throws Exception {
|
void recoverNONELongRunningExecution() throws Exception {
|
||||||
// mock flow listeners
|
// mock flow listeners
|
||||||
FlowListeners flowListenersServiceSpy = spy(this.flowListenersService);
|
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));
|
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
|
@Test
|
||||||
void aliases() {
|
void aliases() {
|
||||||
List<FlowService.Relocation> warnings = flowService.relocations("""
|
List<FlowService.Relocation> warnings = flowService.relocations("""
|
||||||
|
|||||||
@@ -125,12 +125,13 @@ class MapUtilsTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldThrowExceptionWhenNestingMapGivenFlattenMapWithConflicts() {
|
void shouldReturnMapAndIgnoreConflicts() {
|
||||||
Assertions.assertThrows(IllegalArgumentException.class, () -> {
|
Map<String, Object> results = MapUtils.flattenToNestedMap(Map.of(
|
||||||
MapUtils.flattenToNestedMap(Map.of(
|
"k1.k2", "v1",
|
||||||
"k1.k2", "v1",
|
"k1.k2.k3", "v2"
|
||||||
"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.concurrent.atomic.AtomicReference;
|
||||||
import java.util.stream.IntStream;
|
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 io.kestra.core.utils.Rethrow.throwRunnable;
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
import static org.hamcrest.Matchers.greaterThan;
|
import static org.hamcrest.Matchers.greaterThan;
|
||||||
@@ -136,7 +137,6 @@ public class ForEachItemCaseTest {
|
|||||||
Flux<Execution> receive = TestsUtils.receive(executionQueue, either -> {
|
Flux<Execution> receive = TestsUtils.receive(executionQueue, either -> {
|
||||||
Execution execution = either.getLeft();
|
Execution execution = either.getLeft();
|
||||||
if (execution.getFlowId().equals("for-each-item-subflow")) {
|
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()) {
|
if (execution.getState().getCurrent().isTerminated()) {
|
||||||
triggered.set(execution);
|
triggered.set(execution);
|
||||||
countDownLatch.countDown();
|
countDownLatch.countDown();
|
||||||
@@ -204,8 +204,9 @@ public class ForEachItemCaseTest {
|
|||||||
// assert on the main flow execution
|
// assert on the main flow execution
|
||||||
assertThat(execution.getTaskRunList(), hasSize(3));
|
assertThat(execution.getTaskRunList(), hasSize(3));
|
||||||
assertThat(execution.getTaskRunList().get(2).getAttempts(), hasSize(1));
|
assertThat(execution.getTaskRunList().get(2).getAttempts(), hasSize(1));
|
||||||
assertThat(execution.getTaskRunList().get(2).getAttempts().getFirst().getState().getCurrent(), is(State.Type.FAILED));
|
assertThat(execution.getTaskRunList().get(2).getAttempts().getFirst().getState().getCurrent(), is(
|
||||||
assertThat(execution.getState().getCurrent(), is(State.Type.FAILED));
|
FAILED));
|
||||||
|
assertThat(execution.getState().getCurrent(), is(FAILED));
|
||||||
Map<String, Object> outputs = execution.getTaskRunList().get(2).getOutputs();
|
Map<String, Object> outputs = execution.getTaskRunList().get(2).getOutputs();
|
||||||
assertThat(outputs.get("numberOfBatches"), is(26));
|
assertThat(outputs.get("numberOfBatches"), is(26));
|
||||||
assertThat(outputs.get("iterations"), notNullValue());
|
assertThat(outputs.get("iterations"), notNullValue());
|
||||||
@@ -215,7 +216,7 @@ public class ForEachItemCaseTest {
|
|||||||
assertThat(iterations.get("FAILED"), is(26));
|
assertThat(iterations.get("FAILED"), is(26));
|
||||||
|
|
||||||
// assert on the last subflow execution
|
// 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(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((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));
|
assertThat(triggered.get().getTaskRunList(), hasSize(1));
|
||||||
@@ -290,7 +291,7 @@ public class ForEachItemCaseTest {
|
|||||||
(flow, execution1) -> flowIO.readExecutionInputs(flow, execution1, inputs),
|
(flow, execution1) -> flowIO.readExecutionInputs(flow, execution1, inputs),
|
||||||
Duration.ofSeconds(30));
|
Duration.ofSeconds(30));
|
||||||
assertThat(execution.getTaskRunList(), hasSize(3));
|
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
|
// here we must have 1 failed subflows
|
||||||
assertTrue(countDownLatch.await(1, TimeUnit.MINUTES));
|
assertTrue(countDownLatch.await(1, TimeUnit.MINUTES));
|
||||||
@@ -303,11 +304,15 @@ public class ForEachItemCaseTest {
|
|||||||
successLatch.countDown();
|
successLatch.countDown();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//Wait before restarting until the failed execution tasks are persisted.
|
||||||
|
Thread.sleep(1000L);
|
||||||
|
|
||||||
Execution restarted = executionService.restart(execution, null);
|
Execution restarted = executionService.restart(execution, null);
|
||||||
execution = runnerUtils.awaitExecution(
|
execution = runnerUtils.awaitExecution(
|
||||||
e -> e.getState().getCurrent() == State.Type.SUCCESS && e.getFlowId().equals("restart-for-each-item"),
|
e -> e.getState().getCurrent() == State.Type.SUCCESS && e.getFlowId().equals("restart-for-each-item"),
|
||||||
throwRunnable(() -> executionQueue.emit(restarted)),
|
throwRunnable(() -> executionQueue.emit(restarted)),
|
||||||
Duration.ofSeconds(10)
|
Duration.ofSeconds(20)
|
||||||
);
|
);
|
||||||
assertThat(execution.getTaskRunList(), hasSize(4));
|
assertThat(execution.getTaskRunList(), hasSize(4));
|
||||||
assertTrue(successLatch.await(1, TimeUnit.MINUTES));
|
assertTrue(successLatch.await(1, TimeUnit.MINUTES));
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
version=0.22.0-SNAPSHOT
|
version=0.22.0
|
||||||
|
|
||||||
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||||
org.gradle.parallel=true
|
org.gradle.parallel=true
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import java.util.*;
|
|||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
public abstract class AbstractJdbcExecutionRepository extends AbstractJdbcRepository implements ExecutionRepositoryInterface, JdbcQueueIndexerInterface<Execution> {
|
public abstract class AbstractJdbcExecutionRepository extends AbstractJdbcRepository implements ExecutionRepositoryInterface, JdbcQueueIndexerInterface<Execution> {
|
||||||
private static final int FETCH_SIZE = 100;
|
private static final int FETCH_SIZE = 100;
|
||||||
@@ -343,6 +344,28 @@ public abstract class AbstractJdbcExecutionRepository extends AbstractJdbcReposi
|
|||||||
return select;
|
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
|
@Override
|
||||||
public ArrayListTotal<Execution> findByFlowId(String tenantId, String namespace, String id, Pageable pageable) {
|
public ArrayListTotal<Execution> findByFlowId(String tenantId, String namespace, String id, Pageable pageable) {
|
||||||
return this.jdbcRepository
|
return this.jdbcRepository
|
||||||
|
|||||||
@@ -183,6 +183,28 @@ public abstract class AbstractJdbcLogRepository extends AbstractJdbcRepository i
|
|||||||
}), FluxSink.OverflowStrategy.BUFFER);
|
}), 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
|
@Override
|
||||||
public List<LogStatistics> statistics(
|
public List<LogStatistics> statistics(
|
||||||
@Nullable String query,
|
@Nullable String query,
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import lombok.Getter;
|
|||||||
import org.jooq.Record;
|
import org.jooq.Record;
|
||||||
import org.jooq.*;
|
import org.jooq.*;
|
||||||
import org.jooq.impl.DSL;
|
import org.jooq.impl.DSL;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.FluxSink;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
@@ -27,6 +29,7 @@ import java.time.temporal.ChronoUnit;
|
|||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
public abstract class AbstractJdbcMetricRepository extends AbstractJdbcRepository implements MetricRepositoryInterface {
|
public abstract class AbstractJdbcMetricRepository extends AbstractJdbcRepository implements MetricRepositoryInterface {
|
||||||
protected io.kestra.jdbc.AbstractJdbcRepository<MetricEntry> jdbcRepository;
|
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
|
@Override
|
||||||
public List<String> flowMetrics(
|
public List<String> flowMetrics(
|
||||||
String tenantId,
|
String tenantId,
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ dependencies {
|
|||||||
// ugly hack on crypto plugin
|
// ugly hack on crypto plugin
|
||||||
api("org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion")
|
api("org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion")
|
||||||
api("org.bouncycastle:bcpg-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.
|
// 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-core:$jacksonVersion")
|
||||||
api("com.fasterxml.jackson.core:jackson-databind:$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";
|
import Utils from "../utils/utils";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
|
||||||
|
|
||||||
},
|
|
||||||
props: {
|
props: {
|
||||||
value: {
|
value: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<span v-if="required" class="me-1 text-danger">*</span>
|
<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">
|
<div class="mt-1 mb-2 wrapper" :class="props.class">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="input"
|
v-model="input"
|
||||||
@input="handleInput"
|
:id="uid"
|
||||||
:placeholder
|
:placeholder
|
||||||
:disabled
|
:disabled
|
||||||
type="textarea"
|
type="textarea"
|
||||||
@@ -14,10 +14,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {ref, watch} from "vue";
|
import {useId, computed} from "vue";
|
||||||
|
|
||||||
defineOptions({inheritAttrs: false});
|
defineOptions({inheritAttrs: false});
|
||||||
|
|
||||||
|
const uid = useId();
|
||||||
|
|
||||||
const emits = defineEmits(["update:modelValue"]);
|
const emits = defineEmits(["update:modelValue"]);
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {type: [String, Number, Boolean], default: undefined},
|
modelValue: {type: [String, Number, Boolean], default: undefined},
|
||||||
@@ -28,20 +30,12 @@
|
|||||||
class: {type: String, default: undefined},
|
class: {type: String, default: undefined},
|
||||||
});
|
});
|
||||||
|
|
||||||
const input = ref(props.modelValue);
|
const input = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
const handleInput = (value: string) => {
|
set: (value) => {
|
||||||
emits("update:modelValue", value);
|
emits("update:modelValue", value);
|
||||||
};
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.modelValue,
|
|
||||||
(newValue) => {
|
|
||||||
if (newValue !== input.value) {
|
|
||||||
input.value = newValue;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
@@ -112,11 +112,12 @@
|
|||||||
>
|
>
|
||||||
<PluginDocumentation
|
<PluginDocumentation
|
||||||
v-if="currentView === views.DOC"
|
v-if="currentView === views.DOC"
|
||||||
class="plugin-doc combined-right-view enhance-readability"
|
class="combined-right-view enhance-readability"
|
||||||
:override-intro="intro"
|
:override-intro="intro"
|
||||||
|
absolute
|
||||||
/>
|
/>
|
||||||
<div
|
<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"
|
v-else-if="currentView === views.CHART"
|
||||||
>
|
>
|
||||||
<div v-if="selectedChart" class="w-100">
|
<div v-if="selectedChart" class="w-100">
|
||||||
@@ -129,7 +130,7 @@
|
|||||||
<small>{{ selectedChart.chartOptions.description }}</small>
|
<small>{{ selectedChart.chartOptions.description }}</small>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="w-100">
|
<div :style="`position: relative; width:calc(${100}% - 10px)`">
|
||||||
<component
|
<component
|
||||||
:key="selectedChart.id"
|
:key="selectedChart.id"
|
||||||
:is="types[selectedChart.type]"
|
:is="types[selectedChart.type]"
|
||||||
@@ -171,6 +172,8 @@
|
|||||||
defineEmits(["save"])
|
defineEmits(["save"])
|
||||||
</script>
|
</script>
|
||||||
<script>
|
<script>
|
||||||
|
import {shallowRef} from "vue";
|
||||||
|
|
||||||
import Editor from "../../inputs/Editor.vue";
|
import Editor from "../../inputs/Editor.vue";
|
||||||
import yaml from "yaml";
|
import yaml from "yaml";
|
||||||
import ContentSave from "vue-material-design-icons/ContentSave.vue";
|
import ContentSave from "vue-material-design-icons/ContentSave.vue";
|
||||||
@@ -312,11 +315,11 @@
|
|||||||
charts: [],
|
charts: [],
|
||||||
chartError: null,
|
chartError: null,
|
||||||
types: {
|
types: {
|
||||||
"io.kestra.plugin.core.dashboard.chart.TimeSeries": TimeSeries,
|
"io.kestra.plugin.core.dashboard.chart.TimeSeries": shallowRef(TimeSeries),
|
||||||
"io.kestra.plugin.core.dashboard.chart.Bar": Bar,
|
"io.kestra.plugin.core.dashboard.chart.Bar": shallowRef(Bar),
|
||||||
"io.kestra.plugin.core.dashboard.chart.Markdown": Markdown,
|
"io.kestra.plugin.core.dashboard.chart.Markdown": shallowRef(Markdown),
|
||||||
"io.kestra.plugin.core.dashboard.chart.Table": Table,
|
"io.kestra.plugin.core.dashboard.chart.Table": shallowRef(Table),
|
||||||
"io.kestra.plugin.core.dashboard.chart.Pie": Pie,
|
"io.kestra.plugin.core.dashboard.chart.Pie": shallowRef(Pie),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -54,6 +54,7 @@
|
|||||||
import Pencil from "vue-material-design-icons/Pencil.vue";
|
import Pencil from "vue-material-design-icons/Pencil.vue";
|
||||||
import Plus from "vue-material-design-icons/Plus.vue";
|
import Plus from "vue-material-design-icons/Plus.vue";
|
||||||
import ViewDashboardEdit from "vue-material-design-icons/ViewDashboardEdit.vue";
|
import ViewDashboardEdit from "vue-material-design-icons/ViewDashboardEdit.vue";
|
||||||
|
import useRouteContext from "../../../mixins/useRouteContext.js";
|
||||||
|
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
const {t} = useI18n({useScope: "global"});
|
const {t} = useI18n({useScope: "global"});
|
||||||
@@ -72,4 +73,6 @@
|
|||||||
const routeInfo = computed(() => ({
|
const routeInfo = computed(() => ({
|
||||||
title: props.title ?? t("homeDashboard.title"),
|
title: props.title ?? t("homeDashboard.title"),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
useRouteContext(routeInfo);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
|
|
||||||
defineOptions({inheritAttrs: false});
|
defineOptions({inheritAttrs: false});
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
identifier: {type: Number, required: true},
|
identifier: {type: [Number, String], required: true},
|
||||||
chart: {type: Object, required: true},
|
chart: {type: Object, required: true},
|
||||||
isPreview: {type: Boolean, required: false, default: false}
|
isPreview: {type: Boolean, required: false, default: false}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
|
|
||||||
defineOptions({inheritAttrs: false});
|
defineOptions({inheritAttrs: false});
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
identifier: {type: Number, required: true},
|
identifier: {type: [Number, String], required: true},
|
||||||
chart: {type: Object, required: true},
|
chart: {type: Object, required: true},
|
||||||
isPreview: {type: Boolean, required: false, default: false}
|
isPreview: {type: Boolean, required: false, default: false}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
|
|
||||||
defineOptions({inheritAttrs: false});
|
defineOptions({inheritAttrs: false});
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
identifier: {type: Number, required: true},
|
identifier: {type: [Number, String], required: true},
|
||||||
chart: {type: Object, required: true},
|
chart: {type: Object, required: true},
|
||||||
isPreview: {type: Boolean, required: false, default: false}
|
isPreview: {type: Boolean, required: false, default: false}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
|
|
||||||
defineOptions({inheritAttrs: false});
|
defineOptions({inheritAttrs: false});
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
identifier: {type: Number, required: true},
|
identifier: {type: [Number, String], required: true},
|
||||||
chart: {type: Object, required: true},
|
chart: {type: Object, required: true},
|
||||||
isPreview: {type: Boolean, required: false, default: false}
|
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>
|
</div>
|
||||||
<h2>{{ title }}</h2>
|
<h2>{{ title }}</h2>
|
||||||
<p><slot name="message" /></p>
|
<p><slot name="message" /></p>
|
||||||
<a class="el-button el-button--large el-button--primary" target="_blank" :href="getADemoUrl.href">
|
<DemoButtons />
|
||||||
{{ $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>
|
</div>
|
||||||
</EmptyTemplate>
|
</EmptyTemplate>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<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";
|
import EmptyTemplate from "../layout/EmptyTemplate.vue";
|
||||||
|
import DemoButtons from "./DemoButtons.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;
|
|
||||||
});
|
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
title: string;
|
title: string;
|
||||||
|
|||||||
@@ -76,6 +76,7 @@
|
|||||||
<editor
|
<editor
|
||||||
ref="debugEditor"
|
ref="debugEditor"
|
||||||
:full-height="false"
|
:full-height="false"
|
||||||
|
:custom-height="20"
|
||||||
:input="true"
|
:input="true"
|
||||||
:navbar="false"
|
:navbar="false"
|
||||||
:model-value="computedDebugValue"
|
:model-value="computedDebugValue"
|
||||||
@@ -100,6 +101,7 @@
|
|||||||
:read-only="true"
|
:read-only="true"
|
||||||
:input="true"
|
:input="true"
|
||||||
:full-height="false"
|
:full-height="false"
|
||||||
|
:custom-height="20"
|
||||||
:navbar="false"
|
:navbar="false"
|
||||||
:model-value="debugExpression"
|
:model-value="debugExpression"
|
||||||
:lang="isJSON ? 'json' : ''"
|
:lang="isJSON ? 'json' : ''"
|
||||||
|
|||||||
@@ -201,7 +201,7 @@
|
|||||||
|
|
||||||
import {useI18n} from "vue-i18n";
|
import {useI18n} from "vue-i18n";
|
||||||
import {useStore} from "vuex";
|
import {useStore} from "vuex";
|
||||||
import {useRoute, useRouter} from "vue-router";
|
import {useRoute, useRouter, LocationQueryRaw} from "vue-router";
|
||||||
import {useFilters} from "./composables/useFilters";
|
import {useFilters} from "./composables/useFilters";
|
||||||
import action from "../../models/action";
|
import action from "../../models/action";
|
||||||
import permission from "../../models/permission";
|
import permission from "../../models/permission";
|
||||||
@@ -632,21 +632,9 @@
|
|||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => route.query,
|
() => route.query,
|
||||||
(q: any) => {
|
(q: LocationQueryRaw) => {
|
||||||
// Handling change of label filters from direct click events
|
const routeFilters = decodeParams(route.name, q, props.include, OPTIONS, props.isDefaultDashboard) as CurrentItem[];
|
||||||
if (
|
currentFilters.value = routeFilters;
|
||||||
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;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{immediate: true},
|
{immediate: true},
|
||||||
);
|
);
|
||||||
@@ -736,7 +724,7 @@
|
|||||||
|
|
||||||
const handleClickedItems = (value) => {
|
const handleClickedItems = (value) => {
|
||||||
if (value) currentFilters.value = value;
|
if (value) currentFilters.value = value;
|
||||||
select.value?.focus();
|
triggerSearch();
|
||||||
};
|
};
|
||||||
|
|
||||||
const triggerSearch = () => {
|
const triggerSearch = () => {
|
||||||
|
|||||||
@@ -41,14 +41,20 @@
|
|||||||
class="me-2"
|
class="me-2"
|
||||||
>
|
>
|
||||||
<span class="small">
|
<span class="small">
|
||||||
<Label :option="value" />
|
<Label :option="value" :prefix="props.prefix" />
|
||||||
</span>
|
</span>
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-auto">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
@@ -96,10 +102,15 @@
|
|||||||
@import "../styles/filter";
|
@import "../styles/filter";
|
||||||
|
|
||||||
.dropdown {
|
.dropdown {
|
||||||
width: 400px;
|
width: 800px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.items {
|
.items {
|
||||||
|
background-color: var(--el-bg-color-overlay);
|
||||||
max-height: 170px !important; // 5 visible items
|
max-height: 170px !important; // 5 visible items
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -15,8 +15,9 @@ $filters-font-xs: var(--el-font-size-extra-small);
|
|||||||
|
|
||||||
.items {
|
.items {
|
||||||
.el-tag {
|
.el-tag {
|
||||||
background: $filters-border-color;
|
padding: 0;
|
||||||
color: $filters-gray-900;
|
color: var(--ks-tag-content);
|
||||||
|
background: var(--ks-tag-background-active);
|
||||||
}
|
}
|
||||||
|
|
||||||
.small {
|
.small {
|
||||||
|
|||||||
@@ -121,22 +121,22 @@ export const encodeSearchParams = (filters, OPTIONS) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return filters.reduce((query, filter) => {
|
return filters.reduce((query, filter) => {
|
||||||
if(filter.field === "labels" && filter.operation) {
|
if(filter.operation) {
|
||||||
Object.assign(query, encode(filter.value, "labels", "EQUALS"));
|
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);
|
if (key) {
|
||||||
const key = match ? match.key : filter.label === "text" ? "q" : null;
|
if (key !== "date") {
|
||||||
const operation = filter.comparator?.value || match?.comparators?.find(c => c.value === filter.operation)?.value || "EQUALS";
|
Object.assign(query, encode(filter.value, key, operation));
|
||||||
|
} else if (filter.value?.length > 0) {
|
||||||
if (key) {
|
const {startDate, endDate} = filter.value[0];
|
||||||
if (key !== "date") {
|
if(startDate && endDate) {
|
||||||
Object.assign(query, encode(filter.value, key, operation));
|
query["filters[startDate][GREATER_THAN_OR_EQUAL_TO]"] = startDate;
|
||||||
} else if (filter.value?.length > 0) {
|
query["filters[endDate][LESS_THAN_OR_EQUAL_TO]"] = endDate;
|
||||||
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>
|
</el-card>
|
||||||
<template v-if="blueprint.description">
|
<template v-if="blueprint.description">
|
||||||
<h4>{{ $t('about_this_blueprint') }}</h4>
|
<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">
|
<div v-for="(tag, index) in blueprint.tags" :key="index" class="tag-box">
|
||||||
<el-tag type="info" size="small">
|
<el-tag type="info" size="small">
|
||||||
{{ tag }}
|
{{ tag }}
|
||||||
@@ -130,6 +130,10 @@
|
|||||||
type: String,
|
type: String,
|
||||||
default: "flow",
|
default: "flow",
|
||||||
},
|
},
|
||||||
|
combinedView: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
goBack() {
|
goBack() {
|
||||||
@@ -153,7 +157,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async created() {
|
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 => {
|
.then(data => {
|
||||||
this.blueprint = data;
|
this.blueprint = data;
|
||||||
if (this.kind === "flow") {
|
if (this.kind === "flow") {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="20">
|
<el-col :span="20">
|
||||||
<InputText
|
<InputText
|
||||||
:model-value="YamUtils.stringify(element)"
|
:model-value="element"
|
||||||
@update:model-value="(v) => handleInput(v, index)"
|
@update:model-value="(v) => handleInput(v, index)"
|
||||||
:placeholder="$t('value')"
|
:placeholder="$t('value')"
|
||||||
class="w-100"
|
class="w-100"
|
||||||
@@ -35,7 +35,11 @@
|
|||||||
defineOptions({inheritAttrs: false});
|
defineOptions({inheritAttrs: false});
|
||||||
|
|
||||||
const emits = defineEmits(["update:modelValue"]);
|
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(
|
const items = ref(
|
||||||
!Array.isArray(props.modelValue) ? [props.modelValue] : props.modelValue,
|
!Array.isArray(props.modelValue) ? [props.modelValue] : props.modelValue,
|
||||||
|
|||||||
@@ -37,11 +37,14 @@
|
|||||||
import {computed, getCurrentInstance} from "vue";
|
import {computed, getCurrentInstance} from "vue";
|
||||||
import {useStore} from "vuex"
|
import {useStore} from "vuex"
|
||||||
import {useRouter, useRoute} from "vue-router";
|
import {useRouter, useRoute} from "vue-router";
|
||||||
|
import {useI18n} from "vue-i18n";
|
||||||
import EditorButtons from "./EditorButtons.vue";
|
import EditorButtons from "./EditorButtons.vue";
|
||||||
import ValidationError from "../flows/ValidationError.vue";
|
import ValidationError from "../flows/ValidationError.vue";
|
||||||
|
|
||||||
import localUtils from "../../utils/utils";
|
import localUtils from "../../utils/utils";
|
||||||
|
|
||||||
|
const {t} = useI18n();
|
||||||
|
|
||||||
const exportYaml = () => {
|
const exportYaml = () => {
|
||||||
const blob = new Blob([store.getters["flow/flowYaml"]], {type: "text/yaml"});
|
const blob = new Blob([store.getters["flow/flowYaml"]], {type: "text/yaml"});
|
||||||
localUtils.downloadUrl(window.URL.createObjectURL(blob), "flow.yaml");
|
localUtils.downloadUrl(window.URL.createObjectURL(blob), "flow.yaml");
|
||||||
@@ -57,12 +60,33 @@
|
|||||||
const isAllowedEdit = computed(() => store.getters["flow/isAllowedEdit"])
|
const isAllowedEdit = computed(() => store.getters["flow/isAllowedEdit"])
|
||||||
const flowHaveTasks = computed(() => store.getters["flow/flowHaveTasks"])
|
const flowHaveTasks = computed(() => store.getters["flow/flowHaveTasks"])
|
||||||
const flowErrors = computed(() => store.getters["flow/flowErrors"])
|
const flowErrors = computed(() => store.getters["flow/flowErrors"])
|
||||||
const flowWarnings = computed(() => store.getters["flow/flowWarnings"])
|
|
||||||
const flowInfos = computed(() => store.getters["flow/flowInfos"])
|
const flowInfos = computed(() => store.getters["flow/flowInfos"])
|
||||||
const flowParsed = computed(() => store.getters["flow/flow"])
|
const flowParsed = computed(() => store.getters["flow/flow"])
|
||||||
const tabs = computed<{dirty:boolean}[]>(() => store.state.editor.tabs)
|
const tabs = computed<{dirty:boolean}[]>(() => store.state.editor.tabs)
|
||||||
const metadata = computed(() => store.state.flow.metadata);
|
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(){
|
async function save(){
|
||||||
await store.dispatch("flow/saveAll")
|
await store.dispatch("flow/saveAll")
|
||||||
|
|||||||
@@ -1148,7 +1148,7 @@
|
|||||||
height: 30px;
|
height: 30px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
font-size: var(--el-font-size-small);
|
font-size: var(--el-font-size-small);
|
||||||
color: $gray-900;
|
color: var(--ks-content-primary);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--ks-content-secondary);
|
color: var(--ks-content-secondary);
|
||||||
|
|||||||
@@ -250,7 +250,7 @@
|
|||||||
v-if="viewType === editorViewTypes.SOURCE_BLUEPRINTS"
|
v-if="viewType === editorViewTypes.SOURCE_BLUEPRINTS"
|
||||||
class="combined-right-view enhance-readability"
|
class="combined-right-view enhance-readability"
|
||||||
>
|
>
|
||||||
<Blueprints @loaded="blueprintsLoaded = true" embed kind="flow" />
|
<Blueprints @loaded="blueprintsLoaded = true" embed kind="flow" combined-view />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -560,7 +560,31 @@
|
|||||||
|
|
||||||
const baseOutdatedTranslationKey = computed(() => store.getters["flow/baseOutdatedTranslationKey"]);
|
const baseOutdatedTranslationKey = computed(() => store.getters["flow/baseOutdatedTranslationKey"]);
|
||||||
const flowErrors = computed(() => store.getters["flow/flowErrors"]);
|
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 flowInfos = computed(() => store.getters["flow/flowInfos"]);
|
||||||
const flowHaveTasks = computed(() => store.getters["flow/flowHaveTasks"]);
|
const flowHaveTasks = computed(() => store.getters["flow/flowHaveTasks"]);
|
||||||
|
|
||||||
@@ -862,11 +886,11 @@
|
|||||||
|
|
||||||
const onUpdateMetadata = (event, shouldSave) => {
|
const onUpdateMetadata = (event, shouldSave) => {
|
||||||
if(shouldSave) {
|
if(shouldSave) {
|
||||||
metadata.value = {...metadata.value, ...(event.concurrency?.limit === 0 ? {concurrency: null} : event)};
|
store.commit("flow/setMetadata", {...metadata.value, ...(event.concurrency?.limit === 0 ? {concurrency: null} : event)});
|
||||||
onSaveMetadata();
|
store.dispatch("flow/onSaveMetadata");
|
||||||
validateFlow(flowYaml.value)
|
store.dispatch("flow/validateFlow", {flow: flowYaml.value});
|
||||||
} else {
|
} 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;
|
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) => {
|
const handleReorder = (yaml) => {
|
||||||
store.commit("flow/setFlowYaml", yaml);
|
store.commit("flow/setFlowYaml", yaml);
|
||||||
store.commit("flow/setHaveChange", true)
|
store.commit("flow/setHaveChange", true)
|
||||||
@@ -953,7 +961,7 @@
|
|||||||
|
|
||||||
const save = async () => {
|
const save = async () => {
|
||||||
const result = await store.dispatch("flow/save", {
|
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"){
|
if(result === "redirect_to_update"){
|
||||||
await router.push({
|
await router.push({
|
||||||
|
|||||||
@@ -209,7 +209,7 @@
|
|||||||
this.initMonaco(monaco)
|
this.initMonaco(monaco)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!this.monacoYamlConfigured && (this.creating || this.current?.flow)) {
|
if (!this.monacoYamlConfigured && this.language === "yaml") {
|
||||||
this.$store.commit("core/setMonacoYamlConfigured", true);
|
this.$store.commit("core/setMonacoYamlConfigured", true);
|
||||||
configureMonacoYaml(monaco, {
|
configureMonacoYaml(monaco, {
|
||||||
enableSchemaRequest: true,
|
enableSchemaRequest: true,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<template>
|
<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
|
<select-table
|
||||||
:data="filteredKvs"
|
:data="filteredKvs"
|
||||||
@@ -190,7 +192,6 @@
|
|||||||
import ContentSave from "vue-material-design-icons/ContentSave.vue";
|
import ContentSave from "vue-material-design-icons/ContentSave.vue";
|
||||||
import TimeSelect from "../executions/date-select/TimeSelect.vue";
|
import TimeSelect from "../executions/date-select/TimeSelect.vue";
|
||||||
import Check from "vue-material-design-icons/Check.vue";
|
import Check from "vue-material-design-icons/Check.vue";
|
||||||
import KestraFilter from "../filter/KestraFilter.vue";
|
|
||||||
import NamespaceSelect from "../namespace/NamespaceSelect.vue";
|
import NamespaceSelect from "../namespace/NamespaceSelect.vue";
|
||||||
|
|
||||||
import Utils from "../../utils/utils";
|
import Utils from "../../utils/utils";
|
||||||
@@ -258,6 +259,13 @@
|
|||||||
if (this.$refs.form) {
|
if (this.$refs.form) {
|
||||||
this.$refs.form.clearValidate("value");
|
this.$refs.form.clearValidate("value");
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
search(newValue) {
|
||||||
|
if (newValue !== undefined) {
|
||||||
|
this.$router.push({query: {
|
||||||
|
q: newValue
|
||||||
|
}})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@@ -272,7 +280,7 @@
|
|||||||
},
|
},
|
||||||
kvs: undefined,
|
kvs: undefined,
|
||||||
namespaceIterator: undefined,
|
namespaceIterator: undefined,
|
||||||
search: "",
|
search: this.$route.query?.q ?? "",
|
||||||
rules: {
|
rules: {
|
||||||
key: [
|
key: [
|
||||||
{required: true, trigger: "change"},
|
{required: true, trigger: "change"},
|
||||||
@@ -411,13 +419,15 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
reloadKvs() {
|
async reloadKvs() {
|
||||||
this.namespaceIterator = undefined;
|
this.namespaceIterator = undefined;
|
||||||
|
|
||||||
|
const previousLength = this.secrets?.length ?? 0;
|
||||||
|
await this.$refs.selectTable.resetInfiniteScroll();
|
||||||
this.kvs = [];
|
this.kvs = [];
|
||||||
this.$refs.selectTable.resetInfiniteScroll();
|
|
||||||
|
|
||||||
// If we are in the global KV view we let the infinite scroll handling the fetch
|
// 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();
|
this.fetchKvs();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -24,9 +24,11 @@
|
|||||||
import Navbar from "../layout/TopNavBar.vue";
|
import Navbar from "../layout/TopNavBar.vue";
|
||||||
import {useI18n} from "vue-i18n";
|
import {useI18n} from "vue-i18n";
|
||||||
import {computed, ref} from "vue";
|
import {computed, ref} from "vue";
|
||||||
|
import useRouteContext from "../../mixins/useRouteContext.js";
|
||||||
|
|
||||||
const addKvModalVisible = ref(false);
|
const addKvModalVisible = ref(false);
|
||||||
|
|
||||||
const {t} = useI18n({useScope: "global"});
|
const {t} = useI18n({useScope: "global"});
|
||||||
const routeInfo = computed(() => ({title: t("kv.name")}));
|
const routeInfo = computed(() => ({title: t("kv.name")}));
|
||||||
|
useRouteContext(routeInfo);
|
||||||
</script>
|
</script>
|
||||||
@@ -25,12 +25,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import {default as vElTableInfiniteScroll} from "el-table-infinite-scroll";
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import NoData from "./NoData.vue";
|
import NoData from "./NoData.vue";
|
||||||
|
import elTableInfiniteScroll from "el-table-infinite-scroll";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {NoData},
|
components: {NoData},
|
||||||
@@ -61,9 +58,13 @@
|
|||||||
return this.infiniteScrollDisabled === false;
|
return this.infiniteScrollDisabled === false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
directives: {
|
||||||
|
elTableInfiniteScroll
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async resetInfiniteScroll() {
|
async resetInfiniteScroll() {
|
||||||
this.infiniteScrollDisabled = false;
|
this.infiniteScrollDisabled = false;
|
||||||
|
this.tableHeight = await this.computeTableHeight();
|
||||||
},
|
},
|
||||||
async waitTableRender() {
|
async waitTableRender() {
|
||||||
if (this.tableView === undefined) {
|
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) {
|
selectionChanged(selection) {
|
||||||
this.hasSelection = selection.length > 0;
|
this.hasSelection = selection.length > 0;
|
||||||
this.$emit("selection-change", selection);
|
this.$emit("selection-change", selection);
|
||||||
@@ -114,6 +105,10 @@
|
|||||||
return "auto";
|
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%)`;
|
return this.stillHaveDataToFetch || this.tableView === undefined ? "100%" : `min(${this.tableView.scrollHeight}px, 100%)`;
|
||||||
},
|
},
|
||||||
async infiniteScrollLoadWithDisableHandling() {
|
async infiniteScrollLoadWithDisableHandling() {
|
||||||
|
|||||||
@@ -60,6 +60,7 @@
|
|||||||
import {useRoute} from "vue-router";
|
import {useRoute} from "vue-router";
|
||||||
import useNamespaces, {Namespace} from "../../composables/useNamespaces";
|
import useNamespaces, {Namespace} from "../../composables/useNamespaces";
|
||||||
import {useI18n} from "vue-i18n";
|
import {useI18n} from "vue-i18n";
|
||||||
|
import useRouteContext from "../../mixins/useRouteContext.ts";
|
||||||
|
|
||||||
const {t} = useI18n({useScope: "global"});
|
const {t} = useI18n({useScope: "global"});
|
||||||
|
|
||||||
@@ -74,6 +75,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const routeInfo = computed(() => ({title: t("namespaces")}));
|
const routeInfo = computed(() => ({title: t("namespaces")}));
|
||||||
|
useRouteContext(routeInfo);
|
||||||
|
|
||||||
const user = computed(() => store.state.auth.user);
|
const user = computed(() => store.state.auth.user);
|
||||||
const isUserEmpty = computed(() => Object.keys(user.value).length === 0);
|
const isUserEmpty = computed(() => Object.keys(user.value).length === 0);
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
</schema-to-html>
|
</schema-to-html>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</template>
|
</template>
|
||||||
<markdown v-else :source="introContent" />
|
<markdown v-else :source="introContent" :class="{'position-absolute': absolute}" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -38,6 +38,10 @@
|
|||||||
overrideIntro: {
|
overrideIntro: {
|
||||||
type: String,
|
type: String,
|
||||||
default: null
|
default: null
|
||||||
|
},
|
||||||
|
absolute: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|||||||
@@ -10,23 +10,57 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
</Navbar>
|
</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' : ''">
|
<section
|
||||||
<template v-if="configs?.secretsEnabled === undefined">
|
data-component="FILENAME_PLACEHOLDER"
|
||||||
<Layout
|
class="d-flex flex-column fill-height padding-bottom"
|
||||||
:title="$t('demos.secrets.title')"
|
:class="configs?.secretsEnabled === undefined ? 'mt-0 p-0' : 'container'"
|
||||||
:image="{source: sourceImg, alt: $t('demos.secrets.title')}"
|
>
|
||||||
>
|
<EmptyTemplate v-if="configs?.secretsEnabled === undefined" class="d-flex flex-column text-start m-0 p-0 mw-100">
|
||||||
<template #message>
|
<div class="no-secret-manager-block d-flex flex-column gap-6">
|
||||||
{{ $t('demos.secrets.message') }}
|
<div class="header-block d-flex align-items-center">
|
||||||
</template>
|
<div class="d-flex flex-column">
|
||||||
</Layout>
|
<h5 class="mb-3">
|
||||||
<el-divider />
|
{{ $t('demos.secrets.title') }}
|
||||||
<p>Here are secret-type environment variables identified at instance start-time:</p>
|
</h5>
|
||||||
</template>
|
<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
|
<SecretsTable
|
||||||
:filterable="configs?.secretsEnabled !== undefined"
|
v-else
|
||||||
:key-only="configs?.secretsEnabled === undefined"
|
filterable
|
||||||
:namespace="configs?.secretsEnabled === true ? undefined : (configs?.systemNamespace ?? 'system')"
|
|
||||||
:add-secret-modal-visible="addSecretModalVisible"
|
:add-secret-modal-visible="addSecretModalVisible"
|
||||||
@update:add-secret-modal-visible="addSecretModalVisible = $event"
|
@update:add-secret-modal-visible="addSecretModalVisible = $event"
|
||||||
/>
|
/>
|
||||||
@@ -40,21 +74,50 @@
|
|||||||
import {useI18n} from "vue-i18n";
|
import {useI18n} from "vue-i18n";
|
||||||
import {computed, ref} from "vue";
|
import {computed, ref} from "vue";
|
||||||
import {useStore} from "vuex";
|
import {useStore} from "vuex";
|
||||||
|
import useRouteContext from "../../mixins/useRouteContext.js";
|
||||||
import sourceImg from "../../assets/demo/secrets.png";
|
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 store = useStore();
|
||||||
|
|
||||||
const configs = computed(() => store.getters["misc/configs"]);
|
const configs = computed(() => store.getters["misc/configs"]);
|
||||||
|
|
||||||
const addSecretModalVisible = ref(false);
|
const addSecretModalVisible = ref(false);
|
||||||
|
const hasData = ref(undefined);
|
||||||
|
|
||||||
const {t} = useI18n({useScope: "global"});
|
const {t} = useI18n({useScope: "global"});
|
||||||
const routeInfo = computed(() => ({title: t("secret.names")}));
|
const routeInfo = computed(() => ({title: t("secret.names")}));
|
||||||
|
|
||||||
|
useRouteContext(routeInfo);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
:deep(.message-block) {
|
.no-secret-manager-block {
|
||||||
width: 100%;
|
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>
|
</style>
|
||||||
@@ -1,148 +1,155 @@
|
|||||||
<template>
|
<template>
|
||||||
<KestraFilter v-if="filterable" :placeholder="$t('search')" :decode="false" />
|
<div class="d-flex flex-column fill-height">
|
||||||
|
<template v-if="filterable">
|
||||||
<select-table
|
<KestraFilter v-if="namespace" :placeholder="$t('search')" :decode="false" />
|
||||||
:data="secrets"
|
<section v-else class="d-inline-flex mb-3 filters">
|
||||||
v-bind="$attrs"
|
<el-input v-model="search" :placeholder="$t('search')" />
|
||||||
ref="selectTable"
|
</section>
|
||||||
: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>
|
|
||||||
</template>
|
</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>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -176,9 +183,13 @@
|
|||||||
Id,
|
Id,
|
||||||
Drawer
|
Drawer
|
||||||
},
|
},
|
||||||
inheritAttrs: false,
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapState("auth", ["user"]),
|
...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() {
|
secretModalTitle() {
|
||||||
return this.secret?.update ? this.$t("secret.update", {name: this.secret.key}) : this.$t("secret.add");
|
return this.secret?.update ? this.$t("secret.update", {name: this.secret.key}) : this.$t("secret.add");
|
||||||
},
|
},
|
||||||
@@ -219,13 +230,35 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: [
|
emits: [
|
||||||
"update:addSecretModalVisible"
|
"update:addSecretModalVisible",
|
||||||
|
"update:isSecretReadOnly",
|
||||||
|
"hasData"
|
||||||
],
|
],
|
||||||
watch: {
|
watch: {
|
||||||
addSecretModalVisible(newValue) {
|
addSecretModalVisible(newValue) {
|
||||||
if (!newValue) {
|
if (!newValue) {
|
||||||
this.resetForm();
|
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() {
|
data() {
|
||||||
@@ -263,7 +296,9 @@
|
|||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
hasData: undefined,
|
||||||
|
search: this.$route.query?.q ?? ""
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -275,7 +310,14 @@
|
|||||||
},
|
},
|
||||||
async fetchSecrets() {
|
async fetchSecrets() {
|
||||||
if (this.secretsIterator === undefined) {
|
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;
|
let emitReadOnly = false;
|
||||||
@@ -288,9 +330,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (fetch.length === 0) {
|
if (fetch.length === 0) {
|
||||||
|
this.hasData = false;
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.hasData = true;
|
||||||
this.secrets = [...(this.secrets || []), ...fetch];
|
this.secrets = [...(this.secrets || []), ...fetch];
|
||||||
|
|
||||||
return fetch;
|
return fetch;
|
||||||
@@ -338,26 +382,26 @@
|
|||||||
removeSecretTag(index) {
|
removeSecretTag(index) {
|
||||||
this.secret.tags.splice(index, 1);
|
this.secret.tags.splice(index, 1);
|
||||||
},
|
},
|
||||||
reloadSecrets() {
|
async reloadSecrets() {
|
||||||
this.secretsIterator = undefined;
|
this.secretsIterator = undefined;
|
||||||
|
|
||||||
|
const previousLength = this.secrets?.length ?? 0;
|
||||||
|
await this.$refs.selectTable.resetInfiniteScroll();
|
||||||
this.secrets = [];
|
this.secrets = [];
|
||||||
this.$refs.selectTable.resetInfiniteScroll();
|
|
||||||
|
|
||||||
// If we are in the global Secrets view we let the infinite scroll handling the fetch
|
// 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();
|
this.fetchSecrets();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
removeSecret(key) {
|
removeSecret({key, namespace}) {
|
||||||
this.$toast().confirm(this.$t("delete confirm", {name: key}), () => {
|
this.$toast().confirm(this.$t("delete confirm", {name: key}), () => {
|
||||||
return this.$store
|
return this.$store
|
||||||
.dispatch("namespace/deleteSecrets", {namespace: this.$route.params.id, key: key})
|
.dispatch("namespace/deleteSecrets", {namespace: namespace, key})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$toast().deleted(key);
|
this.$toast().deleted(key);
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => this.reloadSecrets())
|
||||||
this.reloadSecrets();
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
isSecretValueUpdated() {
|
isSecretValueUpdated() {
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
|
export type FetchResult<T> = { total: number, results: T[] };
|
||||||
|
|
||||||
export abstract class EntityIterator<T> {
|
export abstract class EntityIterator<T> {
|
||||||
private readonly fetchSize: number;
|
readonly fetchSize: number;
|
||||||
private total: number | undefined;
|
private _total: number | undefined;
|
||||||
private page = 0;
|
private page = 0;
|
||||||
private alreadyFetched: T[] = [];
|
private alreadyFetched: T[] = [];
|
||||||
private buffered: T[] = [];
|
private buffered: T[] = [];
|
||||||
private readonly options: any;
|
readonly options: any;
|
||||||
|
|
||||||
protected constructor(fetchSize: number, options?: any) {
|
protected constructor(fetchSize: number, options?: any) {
|
||||||
if (fetchSize <= 0) {
|
if (fetchSize <= 0) {
|
||||||
@@ -14,6 +16,10 @@ export abstract class EntityIterator<T> {
|
|||||||
this.options = options ?? {};
|
this.options = options ?? {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get total(): number | undefined {
|
||||||
|
return this._total;
|
||||||
|
}
|
||||||
|
|
||||||
fetchOptions() {
|
fetchOptions() {
|
||||||
return {
|
return {
|
||||||
commit: false,
|
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
|
* If no buffer is available, fetches the next entity and returns the first entity while buffering the rest
|
||||||
*/
|
*/
|
||||||
async single(): Promise<T | undefined> {
|
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);
|
return Promise.resolve(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,12 +55,12 @@ export abstract class EntityIterator<T> {
|
|||||||
* Fetches the next batch of entities
|
* Fetches the next batch of entities
|
||||||
*/
|
*/
|
||||||
async next(): Promise<T[]> {
|
async next(): Promise<T[]> {
|
||||||
if (this.total === this.alreadyFetched.length) {
|
if (this.stopCondition()) {
|
||||||
return Promise.resolve([]);
|
return Promise.resolve([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const entityFetch = await this.fetchCall();
|
const entityFetch = await this.fetchCall();
|
||||||
this.total = entityFetch.total;
|
this._total = entityFetch.total;
|
||||||
this.alreadyFetched = [...this.alreadyFetched, ...entityFetch.results];
|
this.alreadyFetched = [...this.alreadyFetched, ...entityFetch.results];
|
||||||
return entityFetch.results;
|
return entityFetch.results;
|
||||||
}
|
}
|
||||||
@@ -64,9 +74,9 @@ export abstract class EntityIterator<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.next();
|
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());
|
entitiesFetchPromises.push(this.next());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {Store} from "vuex";
|
import {Store} from "vuex";
|
||||||
import {EntityIterator} from "./entityIterator.ts";
|
import {EntityIterator, FetchResult} from "./entityIterator.ts";
|
||||||
import {NamespaceIterator} from "./useNamespaces.ts";
|
import {NamespaceIterator} from "./useNamespaces.ts";
|
||||||
import {Me} from "../stores/auth";
|
import {Me} from "../stores/auth";
|
||||||
import permissions from "../models/permission";
|
import permissions from "../models/permission";
|
||||||
@@ -14,10 +14,12 @@ export interface NamespaceSecret {
|
|||||||
|
|
||||||
export type SecretIterator = NamespaceSecretIterator | AllSecretIterator;
|
export type SecretIterator = NamespaceSecretIterator | AllSecretIterator;
|
||||||
|
|
||||||
|
type NamespaceSecretFetchResult = FetchResult<NamespaceSecret> & { readOnly: boolean };
|
||||||
|
|
||||||
export class NamespaceSecretIterator extends EntityIterator<NamespaceSecret>{
|
export class NamespaceSecretIterator extends EntityIterator<NamespaceSecret>{
|
||||||
private readonly store: Store<any>;
|
private readonly store: Store<any>;
|
||||||
readonly namespace: string;
|
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) {
|
constructor(store: Store<any>, namespace: string, fetchSize: number, options?: any) {
|
||||||
super(fetchSize, options);
|
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();
|
return this.doFetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async doFetch(): Promise<{ total: number; results: NamespaceSecret[], readOnly: boolean }> {
|
private async doFetch(): Promise<NamespaceSecretFetchResult> {
|
||||||
const fetch = await this.store.dispatch("namespace/listSecrets", this.fetchOptions());
|
const fetch = (await this.store.dispatch("namespace/listSecrets", this.fetchOptions())) as NamespaceSecretFetchResult;
|
||||||
this.areNamespaceSecretsReadOnly = fetch.readOnly ?? true;
|
this.areNamespaceSecretsReadOnly = fetch.readOnly ?? true;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -52,9 +54,9 @@ export class NamespaceSecretIterator extends EntityIterator<NamespaceSecret>{
|
|||||||
export class AllSecretIterator extends EntityIterator<NamespaceSecret>{
|
export class AllSecretIterator extends EntityIterator<NamespaceSecret>{
|
||||||
private readonly store: Store<any>;
|
private readonly store: Store<any>;
|
||||||
private readonly user: Me;
|
private readonly user: Me;
|
||||||
private namespaceIterator: NamespaceIterator;
|
private namespaceIterator: NamespaceIterator | undefined;
|
||||||
private namespaceSecretIterator: NamespaceSecretIterator;
|
private namespaceSecretIterator: NamespaceSecretIterator | undefined;
|
||||||
private areNamespaceSecretsReadOnly: {[namespace: string]: boolean} = ref({});
|
private areNamespaceSecretsReadOnly = ref({}) as unknown as {[namespace: string]: boolean};
|
||||||
|
|
||||||
constructor(store: Store<any>, fetchSize: number, options?: any) {
|
constructor(store: Store<any>, fetchSize: number, options?: any) {
|
||||||
super(fetchSize, options);
|
super(fetchSize, options);
|
||||||
@@ -62,7 +64,11 @@ export class AllSecretIterator extends EntityIterator<NamespaceSecret>{
|
|||||||
this.user = this.store.state?.["auth"]?.user;
|
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) {
|
if (this.namespaceIterator === undefined) {
|
||||||
this.namespaceIterator = new NamespaceIterator(this.store, 20, {
|
this.namespaceIterator = new NamespaceIterator(this.store, 20, {
|
||||||
commit: false,
|
commit: false,
|
||||||
@@ -83,16 +89,25 @@ export class AllSecretIterator extends EntityIterator<NamespaceSecret>{
|
|||||||
|
|
||||||
this.namespaceSecretIterator = new NamespaceSecretIterator(this.store, namespace.id, this.fetchSize, this.options);
|
this.namespaceSecretIterator = new NamespaceSecretIterator(this.store, namespace.id, this.fetchSize, this.options);
|
||||||
}
|
}
|
||||||
const fetch = await this.namespaceSecretIterator.fetchCall();
|
|
||||||
if (fetch.results.length > 0) {
|
const fetch = {
|
||||||
this.areNamespaceSecretsReadOnly[this.namespaceSecretIterator.namespace] = fetch.readOnly;
|
results: await this.namespaceSecretIterator.next(),
|
||||||
return {
|
namespace: this.namespaceSecretIterator.namespace,
|
||||||
...fetch,
|
areNamespaceSecretsReadOnly: this.namespaceSecretIterator.areNamespaceSecretsReadOnly,
|
||||||
results: fetch.results.map(secret => ({...secret, namespace: this.namespaceSecretIterator.namespace}))
|
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);
|
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);
|
return new AllSecretIterator(store, secretsFetchSize, options);
|
||||||
}
|
}
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
:blueprint-id="selectedBlueprintId"
|
:blueprint-id="selectedBlueprintId"
|
||||||
blueprint-type="community"
|
blueprint-type="community"
|
||||||
@back="selectedBlueprintId = undefined"
|
@back="selectedBlueprintId = undefined"
|
||||||
|
:combined-view
|
||||||
/>
|
/>
|
||||||
<blueprints-browser
|
<blueprints-browser
|
||||||
@loaded="$emit('loaded', $event)"
|
@loaded="$emit('loaded', $event)"
|
||||||
@@ -75,7 +76,11 @@
|
|||||||
tab: {
|
tab: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "community"
|
default: "community"
|
||||||
}
|
},
|
||||||
|
combinedView: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
<div class="left">
|
<div class="left">
|
||||||
<div class="blueprint">
|
<div class="blueprint">
|
||||||
<div class="ps-0 title">
|
<div class="ps-0 title">
|
||||||
{{ blueprint.title }}
|
{{ blueprint.title ?? blueprint.id }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!system" class="tags text-uppercase">
|
<div v-if="!system" class="tags text-uppercase">
|
||||||
<div v-for="(tag, index) in blueprint.tags" :key="index" class="tag-box">
|
<div v-for="(tag, index) in blueprint.tags" :key="index" class="tag-box">
|
||||||
|
|||||||
@@ -138,6 +138,9 @@ export function useLeftMenu() {
|
|||||||
icon: {
|
icon: {
|
||||||
element: shallowRef(ShieldKeyOutline),
|
element: shallowRef(ShieldKeyOutline),
|
||||||
class: "menu-icon"
|
class: "menu-icon"
|
||||||
|
},
|
||||||
|
attributes: {
|
||||||
|
locked: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -233,7 +236,7 @@ export function useLeftMenu() {
|
|||||||
{
|
{
|
||||||
href: {name: "admin/tenants/list"},
|
href: {name: "admin/tenants/list"},
|
||||||
routes: routeStartWith("admin/tenants"),
|
routes: routeStartWith("admin/tenants"),
|
||||||
title: t("tenants"),
|
title: t("tenant.names"),
|
||||||
icon: {
|
icon: {
|
||||||
element: shallowRef(ShieldLockOutline),
|
element: shallowRef(ShieldLockOutline),
|
||||||
class: "menu-icon"
|
class: "menu-icon"
|
||||||
|
|||||||
@@ -768,31 +768,6 @@ export default {
|
|||||||
|
|
||||||
return undefined;
|
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){
|
flowInfos(state, getters){
|
||||||
if (getters.isFlow) {
|
if (getters.isFlow) {
|
||||||
const infos = state.flowValidation?.infos ?? [];
|
const infos = state.flowValidation?.infos ?? [];
|
||||||
|
|||||||
@@ -303,6 +303,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"secrets": {
|
"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.",
|
"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"
|
"title": "Aktualisieren Sie Ihr Secrets Management"
|
||||||
},
|
},
|
||||||
@@ -547,6 +555,7 @@
|
|||||||
},
|
},
|
||||||
"empty": "Sie haben noch keine Suche gespeichert.",
|
"empty": "Sie haben noch keine Suche gespeichert.",
|
||||||
"label": "Gespeicherte Suchen",
|
"label": "Gespeicherte Suchen",
|
||||||
|
"remove": "Entfernen",
|
||||||
"tooltip": "Sie können keine leeren Suchkriterien speichern."
|
"tooltip": "Sie können keine leeren Suchkriterien speichern."
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
@@ -1152,8 +1161,10 @@
|
|||||||
"templates deleted": "<code>{count}</code> Vorlage(n) gelöscht",
|
"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 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",
|
"templates exported": "Vorlagen exportiert",
|
||||||
"tenant": "Tenant",
|
"tenant": {
|
||||||
"tenants": "Tenants",
|
"name": "Mandant",
|
||||||
|
"names": "Mandanten"
|
||||||
|
},
|
||||||
"theme": "Modus",
|
"theme": "Modus",
|
||||||
"this_task_has": "Diese Task hat",
|
"this_task_has": "Diese Task hat",
|
||||||
"timezone": "Zeitzone",
|
"timezone": "Zeitzone",
|
||||||
|
|||||||
@@ -807,7 +807,15 @@
|
|||||||
"get_a_demo_button": "Get a Demo",
|
"get_a_demo_button": "Get a Demo",
|
||||||
"secrets": {
|
"secrets": {
|
||||||
"title": "Upgrade your Secrets Management",
|
"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": {
|
"apps": {
|
||||||
"title": "Build custom Apps with Kestra",
|
"title": "Build custom Apps with Kestra",
|
||||||
@@ -905,8 +913,10 @@
|
|||||||
"enable concurrency": "Enable concurrency",
|
"enable concurrency": "Enable concurrency",
|
||||||
"auditlogs": "Audit Logs",
|
"auditlogs": "Audit Logs",
|
||||||
"iam": "IAM",
|
"iam": "IAM",
|
||||||
"tenant": "Tenant",
|
"tenant": {
|
||||||
"tenants": "Tenants",
|
"name": "Tenant",
|
||||||
|
"names": "Tenants"
|
||||||
|
},
|
||||||
"ee-tooltip": {
|
"ee-tooltip": {
|
||||||
"features-blocked": "This feature requires Enterprise Edition.",
|
"features-blocked": "This feature requires Enterprise Edition.",
|
||||||
"button": "Talk to us"
|
"button": "Talk to us"
|
||||||
@@ -1116,6 +1126,7 @@
|
|||||||
},
|
},
|
||||||
"save": {
|
"save": {
|
||||||
"label": "Saved searches",
|
"label": "Saved searches",
|
||||||
|
"remove": "Remove",
|
||||||
"empty": "You still haven't saved any search.",
|
"empty": "You still haven't saved any search.",
|
||||||
"tooltip": "You can't save empty search criteria.",
|
"tooltip": "You can't save empty search criteria.",
|
||||||
"dialog": {
|
"dialog": {
|
||||||
@@ -1248,7 +1259,7 @@
|
|||||||
"task_runners": "Task Runners",
|
"task_runners": "Task Runners",
|
||||||
"empty": {
|
"empty": {
|
||||||
"announcements": {
|
"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."
|
"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": {
|
"apps": {
|
||||||
|
|||||||
@@ -303,6 +303,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"secrets": {
|
"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.",
|
"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"
|
"title": "Mejora tu Gestión de Secrets"
|
||||||
},
|
},
|
||||||
@@ -547,6 +555,7 @@
|
|||||||
},
|
},
|
||||||
"empty": "Todavía no has guardado ninguna búsqueda.",
|
"empty": "Todavía no has guardado ninguna búsqueda.",
|
||||||
"label": "Búsquedas guardadas",
|
"label": "Búsquedas guardadas",
|
||||||
|
"remove": "Eliminar",
|
||||||
"tooltip": "No puedes guardar criterios de búsqueda vacíos."
|
"tooltip": "No puedes guardar criterios de búsqueda vacíos."
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
@@ -1152,8 +1161,10 @@
|
|||||||
"templates deleted": "<code>{count}</code> Plantilla(s) eliminadas",
|
"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 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",
|
"templates exported": "Plantillas exportadas",
|
||||||
"tenant": "Tenant",
|
"tenant": {
|
||||||
"tenants": "Tenants",
|
"name": "Mandante",
|
||||||
|
"names": "Arrendatarios"
|
||||||
|
},
|
||||||
"theme": "Tema",
|
"theme": "Tema",
|
||||||
"this_task_has": "Esta tarea tiene",
|
"this_task_has": "Esta tarea tiene",
|
||||||
"timezone": "Zona horaria",
|
"timezone": "Zona horaria",
|
||||||
|
|||||||
@@ -303,6 +303,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"secrets": {
|
"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.",
|
"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"
|
"title": "Améliorez votre gestion des Secrets"
|
||||||
},
|
},
|
||||||
@@ -547,6 +555,7 @@
|
|||||||
},
|
},
|
||||||
"empty": "Vous n'avez pas encore enregistré de recherche.",
|
"empty": "Vous n'avez pas encore enregistré de recherche.",
|
||||||
"label": "Recherches enregistrées",
|
"label": "Recherches enregistrées",
|
||||||
|
"remove": "Supprimer",
|
||||||
"tooltip": "Vous ne pouvez pas enregistrer des critères de recherche vides."
|
"tooltip": "Vous ne pouvez pas enregistrer des critères de recherche vides."
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
@@ -1152,8 +1161,10 @@
|
|||||||
"templates deleted": "<code>{count}</code> Template(s) supprimé(s)",
|
"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 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",
|
"templates exported": "Templates exportés",
|
||||||
"tenant": "Tenant",
|
"tenant": {
|
||||||
"tenants": "Tenants",
|
"name": "Mandant",
|
||||||
|
"names": "Mandants"
|
||||||
|
},
|
||||||
"theme": "Thème",
|
"theme": "Thème",
|
||||||
"this_task_has": "Cette tâche a",
|
"this_task_has": "Cette tâche a",
|
||||||
"timezone": "Fuseau horaire",
|
"timezone": "Fuseau horaire",
|
||||||
|
|||||||
@@ -303,6 +303,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"secrets": {
|
"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 करते हैं।",
|
"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 को अपग्रेड करें"
|
"title": "अपने Secrets Management को अपग्रेड करें"
|
||||||
},
|
},
|
||||||
@@ -547,6 +555,7 @@
|
|||||||
},
|
},
|
||||||
"empty": "आपने अभी तक कोई खोज सहेजी नहीं है।",
|
"empty": "आपने अभी तक कोई खोज सहेजी नहीं है।",
|
||||||
"label": "सहेजे गए खोजें",
|
"label": "सहेजे गए खोजें",
|
||||||
|
"remove": "हटाएं",
|
||||||
"tooltip": "आप खाली खोज मानदंड सहेज नहीं सकते।"
|
"tooltip": "आप खाली खोज मानदंड सहेज नहीं सकते।"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
@@ -1152,8 +1161,10 @@
|
|||||||
"templates deleted": "<code>{count}</code> template(s) हटाए गए",
|
"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 deprecated": "templates अप्रचलित हैं। कृपया इसके बजाय subflows का उपयोग करें। यह समझाने वाला <a href=\"https://kestra.io/docs/migration-guide/0.11.0/templates\" target=\"_blank\">माइग्रेशन अनुभाग</a> देखें कि आप टेम्पलेट्स से subflows में कैसे माइग्रेट कर सकते हैं।",
|
||||||
"templates exported": "templates निर्यात किए गए",
|
"templates exported": "templates निर्यात किए गए",
|
||||||
"tenant": "Tenant",
|
"tenant": {
|
||||||
"tenants": "Tenants",
|
"name": "मंडल",
|
||||||
|
"names": "मंडल"
|
||||||
|
},
|
||||||
"theme": "थीम",
|
"theme": "थीम",
|
||||||
"this_task_has": "यह task है",
|
"this_task_has": "यह task है",
|
||||||
"timezone": "समय क्षेत्र",
|
"timezone": "समय क्षेत्र",
|
||||||
|
|||||||
@@ -303,6 +303,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"secrets": {
|
"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.",
|
"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"
|
"title": "Aggiorna il tuo Secrets Management"
|
||||||
},
|
},
|
||||||
@@ -547,6 +555,7 @@
|
|||||||
},
|
},
|
||||||
"empty": "Non hai ancora salvato alcuna ricerca.",
|
"empty": "Non hai ancora salvato alcuna ricerca.",
|
||||||
"label": "Ricerche salvate",
|
"label": "Ricerche salvate",
|
||||||
|
"remove": "Rimuovi",
|
||||||
"tooltip": "Non puoi salvare criteri di ricerca vuoti."
|
"tooltip": "Non puoi salvare criteri di ricerca vuoti."
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
@@ -1152,8 +1161,10 @@
|
|||||||
"templates deleted": "<code>{count}</code> Template/i eliminati",
|
"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 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",
|
"templates exported": "Template esportati",
|
||||||
"tenant": "Tenant",
|
"tenant": {
|
||||||
"tenants": "Tenants",
|
"name": "Mandante",
|
||||||
|
"names": "Mandanti"
|
||||||
|
},
|
||||||
"theme": "Tema",
|
"theme": "Tema",
|
||||||
"this_task_has": "Questo task ha",
|
"this_task_has": "Questo task ha",
|
||||||
"timezone": "Fuso orario",
|
"timezone": "Fuso orario",
|
||||||
|
|||||||
@@ -303,6 +303,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"secrets": {
|
"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コールが削減され、監査証跡がすべてのアクセスをログに記録します。",
|
"message": "エンタープライズエディション (EE) では、UIで直接シークレットを追加、編集、または削除できます。インスタンスの再起動は不要です。シークレットをnamespaceごとに整理し、詳細なRBAC権限を設定し、親namespaceから子namespaceに継承します。EEは、HashiCorp Vault、AWS Secrets Manager、Azure Key Vault、Google Secret Manager、Elasticsearchなどのシークレットマネージャーと統合します。namespace、テナント、またはインスタンスごとに専用のバックエンドを設定して、チーム間でシークレットを分離するか、外部のvaultに保存された読み取り専用のシークレットを有効にします。シークレットは、保存時および転送時に暗号化されたままです。オプションのキャッシュによりAPIコールが削減され、監査証跡がすべてのアクセスをログに記録します。",
|
||||||
"title": "シークレット管理をアップグレード"
|
"title": "シークレット管理をアップグレード"
|
||||||
},
|
},
|
||||||
@@ -547,6 +555,7 @@
|
|||||||
},
|
},
|
||||||
"empty": "検索をまだ保存していません。",
|
"empty": "検索をまだ保存していません。",
|
||||||
"label": "保存済み検索",
|
"label": "保存済み検索",
|
||||||
|
"remove": "削除",
|
||||||
"tooltip": "空の検索条件を保存することはできません。"
|
"tooltip": "空の検索条件を保存することはできません。"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
@@ -1152,8 +1161,10 @@
|
|||||||
"templates deleted": "<code>{count}</code>件のテンプレートが削除されました",
|
"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 deprecated": "テンプレートは非推奨です。代わりにsubflowsを使用してください。テンプレートからsubflowsへの移行方法については、<a href=\"https://kestra.io/docs/migration-guide/0.11.0/templates\" target=\"_blank\">移行セクション</a>を参照してください。",
|
||||||
"templates exported": "テンプレートがエクスポートされました",
|
"templates exported": "テンプレートがエクスポートされました",
|
||||||
"tenant": "Tenant",
|
"tenant": {
|
||||||
"tenants": "Tenants",
|
"name": "テナント",
|
||||||
|
"names": "テナント"
|
||||||
|
},
|
||||||
"theme": "テーマ",
|
"theme": "テーマ",
|
||||||
"this_task_has": "このtaskは",
|
"this_task_has": "このtaskは",
|
||||||
"timezone": "タイムゾーン",
|
"timezone": "タイムゾーン",
|
||||||
|
|||||||
@@ -303,6 +303,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"secrets": {
|
"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합니다.",
|
"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": "비밀 관리 업그레이드"
|
"title": "비밀 관리 업그레이드"
|
||||||
},
|
},
|
||||||
@@ -547,6 +555,7 @@
|
|||||||
},
|
},
|
||||||
"empty": "아직 검색을 저장하지 않았습니다.",
|
"empty": "아직 검색을 저장하지 않았습니다.",
|
||||||
"label": "저장된 검색",
|
"label": "저장된 검색",
|
||||||
|
"remove": "제거",
|
||||||
"tooltip": "빈 검색 기준을 저장할 수 없습니다."
|
"tooltip": "빈 검색 기준을 저장할 수 없습니다."
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
@@ -1152,8 +1161,10 @@
|
|||||||
"templates deleted": "<code>{count}</code> 개의 템플릿이 삭제되었습니다",
|
"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 deprecated": "템플릿은 사용 중지되었습니다. 대신 subflows를 사용하세요. 템플릿에서 subflows로 마이그레이션하는 방법을 설명하는 <a href=\"https://kestra.io/docs/migration-guide/0.11.0/templates\" target=\"_blank\">마이그레이션 섹션</a>을 참조하세요.",
|
||||||
"templates exported": "템플릿이 내보내졌습니다",
|
"templates exported": "템플릿이 내보내졌습니다",
|
||||||
"tenant": "Tenant",
|
"tenant": {
|
||||||
"tenants": "Tenants",
|
"name": "테넌트",
|
||||||
|
"names": "테넌트"
|
||||||
|
},
|
||||||
"theme": "테마",
|
"theme": "테마",
|
||||||
"this_task_has": "이 task는",
|
"this_task_has": "이 task는",
|
||||||
"timezone": "시간대",
|
"timezone": "시간대",
|
||||||
|
|||||||
@@ -303,6 +303,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"secrets": {
|
"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.",
|
"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"
|
"title": "Ulepsz swoje zarządzanie Secrets Management"
|
||||||
},
|
},
|
||||||
@@ -547,6 +555,7 @@
|
|||||||
},
|
},
|
||||||
"empty": "Nie zapisałeś jeszcze żadnego wyszukiwania.",
|
"empty": "Nie zapisałeś jeszcze żadnego wyszukiwania.",
|
||||||
"label": "Zapisane wyszukiwania",
|
"label": "Zapisane wyszukiwania",
|
||||||
|
"remove": "Usuń",
|
||||||
"tooltip": "Nie można zapisać pustych kryteriów wyszukiwania."
|
"tooltip": "Nie można zapisać pustych kryteriów wyszukiwania."
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
@@ -1152,8 +1161,10 @@
|
|||||||
"templates deleted": "<code>{count}</code> Szablon(y) usunięte",
|
"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 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",
|
"templates exported": "Szablony wyeksportowane",
|
||||||
"tenant": "Tenant",
|
"tenant": {
|
||||||
"tenants": "Tenants",
|
"name": "Mandant",
|
||||||
|
"names": "Najemcy"
|
||||||
|
},
|
||||||
"theme": "Motyw",
|
"theme": "Motyw",
|
||||||
"this_task_has": "To zadanie ma",
|
"this_task_has": "To zadanie ma",
|
||||||
"timezone": "Strefa czasowa",
|
"timezone": "Strefa czasowa",
|
||||||
|
|||||||
@@ -303,6 +303,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"secrets": {
|
"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.",
|
"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"
|
"title": "Atualize seu Gerenciamento de Secrets"
|
||||||
},
|
},
|
||||||
@@ -547,6 +555,7 @@
|
|||||||
},
|
},
|
||||||
"empty": "Você ainda não salvou nenhuma pesquisa.",
|
"empty": "Você ainda não salvou nenhuma pesquisa.",
|
||||||
"label": "Pesquisas salvas",
|
"label": "Pesquisas salvas",
|
||||||
|
"remove": "Remover",
|
||||||
"tooltip": "Você não pode salvar critérios de pesquisa vazios."
|
"tooltip": "Você não pode salvar critérios de pesquisa vazios."
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
@@ -1152,8 +1161,10 @@
|
|||||||
"templates deleted": "<code>{count}</code> Template(s) deletado(s)",
|
"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 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",
|
"templates exported": "Templates exportados",
|
||||||
"tenant": "Tenant",
|
"tenant": {
|
||||||
"tenants": "Tenants",
|
"name": "Mandante",
|
||||||
|
"names": "Mandantes"
|
||||||
|
},
|
||||||
"theme": "Tema",
|
"theme": "Tema",
|
||||||
"this_task_has": "Esta task tem",
|
"this_task_has": "Esta task tem",
|
||||||
"timezone": "Fuso horário",
|
"timezone": "Fuso horário",
|
||||||
|
|||||||
@@ -303,6 +303,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"secrets": {
|
"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-запросов, а аудиторские следы фиксируют весь доступ.",
|
"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"
|
"title": "Обновите управление Secrets"
|
||||||
},
|
},
|
||||||
@@ -547,6 +555,7 @@
|
|||||||
},
|
},
|
||||||
"empty": "Вы еще не сохранили ни одного поиска.",
|
"empty": "Вы еще не сохранили ни одного поиска.",
|
||||||
"label": "Сохранённые поиски",
|
"label": "Сохранённые поиски",
|
||||||
|
"remove": "Удалить",
|
||||||
"tooltip": "Вы не можете сохранить пустые критерии поиска."
|
"tooltip": "Вы не можете сохранить пустые критерии поиска."
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
@@ -1152,8 +1161,10 @@
|
|||||||
"templates deleted": "<code>{count}</code> Шаблон(ы) удалены",
|
"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 deprecated": "Шаблоны устарели. Пожалуйста, используйте subflows вместо них. См. <a href=\"https://kestra.io/docs/migration-guide/0.11.0/templates\" target=\"_blank\">раздел миграции</a>, объясняющий, как можно мигрировать с шаблонов на subflows.",
|
||||||
"templates exported": "Шаблоны экспортированы",
|
"templates exported": "Шаблоны экспортированы",
|
||||||
"tenant": "Tenant",
|
"tenant": {
|
||||||
"tenants": "Tenants",
|
"name": "Мандант",
|
||||||
|
"names": "Арендаторы"
|
||||||
|
},
|
||||||
"theme": "Тема",
|
"theme": "Тема",
|
||||||
"this_task_has": "Эта задача имеет",
|
"this_task_has": "Эта задача имеет",
|
||||||
"timezone": "Часовой пояс",
|
"timezone": "Часовой пояс",
|
||||||
|
|||||||
@@ -303,6 +303,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"secrets": {
|
"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调用,并且审计跟踪记录所有访问。",
|
"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管理"
|
"title": "升级您的Secrets管理"
|
||||||
},
|
},
|
||||||
@@ -547,6 +555,7 @@
|
|||||||
},
|
},
|
||||||
"empty": "您还没有保存任何搜索。",
|
"empty": "您还没有保存任何搜索。",
|
||||||
"label": "已保存的搜索",
|
"label": "已保存的搜索",
|
||||||
|
"remove": "移除",
|
||||||
"tooltip": "您不能保存空的搜索条件。"
|
"tooltip": "您不能保存空的搜索条件。"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
@@ -1152,8 +1161,10 @@
|
|||||||
"templates deleted": "<code>{count}</code> 个模板已删除",
|
"templates deleted": "<code>{count}</code> 个模板已删除",
|
||||||
"templates deprecated": "模板已弃用。请使用子流程代替。查看 <a href=\"https://kestra.io/docs/migration-guide/0.11.0/templates\" target=\"_blank\">迁移部分</a> 了解如何从模板迁移到子流程。",
|
"templates deprecated": "模板已弃用。请使用子流程代替。查看 <a href=\"https://kestra.io/docs/migration-guide/0.11.0/templates\" target=\"_blank\">迁移部分</a> 了解如何从模板迁移到子流程。",
|
||||||
"templates exported": "模板已导出",
|
"templates exported": "模板已导出",
|
||||||
"tenant": "租户",
|
"tenant": {
|
||||||
"tenants": "租户",
|
"name": "租户",
|
||||||
|
"names": "租户"
|
||||||
|
},
|
||||||
"theme": "主题",
|
"theme": "主题",
|
||||||
"this_task_has": "此task已",
|
"this_task_has": "此task已",
|
||||||
"timezone": "时区",
|
"timezone": "时区",
|
||||||
|
|||||||
@@ -309,6 +309,8 @@ public class ExecutionController {
|
|||||||
|
|
||||||
private String runContextRender(Flow flow, Task task, Execution execution, TaskRun taskRun, String expression) throws IllegalVariableEvaluationException {
|
private String runContextRender(Flow flow, Task task, Execution execution, TaskRun taskRun, String expression) throws IllegalVariableEvaluationException {
|
||||||
RunContext runContext = runContextFactory.of(flow, task, execution, taskRun, false);
|
RunContext runContext = runContextFactory.of(flow, task, execution, taskRun, false);
|
||||||
|
String baseRender = runContext.render(expression);
|
||||||
|
|
||||||
if (expression.contains("{")) { // fast backoff from regex
|
if (expression.contains("{")) { // fast backoff from regex
|
||||||
Matcher matcher = SECRET_FUNCTION.matcher(expression);
|
Matcher matcher = SECRET_FUNCTION.matcher(expression);
|
||||||
String maskedExpression = expression;
|
String maskedExpression = expression;
|
||||||
@@ -318,7 +320,8 @@ public class ExecutionController {
|
|||||||
}
|
}
|
||||||
return runContext.render(maskedExpression);
|
return runContext.render(maskedExpression);
|
||||||
}
|
}
|
||||||
return runContext.render(expression);
|
|
||||||
|
return baseRender;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuperBuilder
|
@SuperBuilder
|
||||||
|
|||||||
@@ -4,16 +4,19 @@ import io.kestra.core.models.namespaces.Namespace;
|
|||||||
import io.kestra.core.models.topologies.FlowTopologyGraph;
|
import io.kestra.core.models.topologies.FlowTopologyGraph;
|
||||||
import io.kestra.core.repositories.ArrayListTotal;
|
import io.kestra.core.repositories.ArrayListTotal;
|
||||||
import io.kestra.core.repositories.FlowRepositoryInterface;
|
import io.kestra.core.repositories.FlowRepositoryInterface;
|
||||||
import io.kestra.core.utils.NamespaceUtils;
|
|
||||||
import io.kestra.core.tenant.TenantService;
|
import io.kestra.core.tenant.TenantService;
|
||||||
import io.kestra.core.topologies.FlowTopologyService;
|
import io.kestra.core.topologies.FlowTopologyService;
|
||||||
|
import io.kestra.core.utils.NamespaceUtils;
|
||||||
import io.kestra.webserver.models.namespaces.NamespaceWithDisabled;
|
import io.kestra.webserver.models.namespaces.NamespaceWithDisabled;
|
||||||
import io.kestra.webserver.responses.PagedResults;
|
import io.kestra.webserver.responses.PagedResults;
|
||||||
import io.kestra.webserver.utils.PageableUtils;
|
import io.kestra.webserver.utils.PageableUtils;
|
||||||
import io.micronaut.core.annotation.Nullable;
|
import io.micronaut.core.annotation.Nullable;
|
||||||
import io.micronaut.data.model.Pageable;
|
import io.micronaut.data.model.Pageable;
|
||||||
import io.micronaut.data.model.Sort;
|
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.http.exceptions.HttpStatusException;
|
||||||
import io.micronaut.scheduling.TaskExecutors;
|
import io.micronaut.scheduling.TaskExecutors;
|
||||||
import io.micronaut.scheduling.annotation.ExecuteOn;
|
import io.micronaut.scheduling.annotation.ExecuteOn;
|
||||||
@@ -23,7 +26,9 @@ import io.swagger.v3.oas.annotations.Parameter;
|
|||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
import jakarta.validation.constraints.Min;
|
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;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Validated
|
@Validated
|
||||||
@@ -80,9 +85,13 @@ public class NamespaceController implements NamespaceControllerInterface<Namespa
|
|||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Pageable pageable = PageableUtils.from(page, size, sort);
|
||||||
|
|
||||||
var total = distinctNamespaces.size();
|
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) {
|
if (sort != null) {
|
||||||
Sort.Order.Direction direction = pageable.getSort().getOrderBy().getFirst().getDirection();
|
Sort.Order.Direction direction = pageable.getSort().getOrderBy().getFirst().getDirection();
|
||||||
|
|||||||
@@ -1488,11 +1488,23 @@ class ExecutionControllerRunnerTest {
|
|||||||
void shouldMaskSecretWhenEvalPebbleExpression(Execution execution) {
|
void shouldMaskSecretWhenEvalPebbleExpression(Execution execution) {
|
||||||
ExecutionController.EvalResult evalResult = client.toBlocking().retrieve(
|
ExecutionController.EvalResult evalResult = client.toBlocking().retrieve(
|
||||||
HttpRequest
|
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),
|
.contentType(MediaType.TEXT_PLAIN),
|
||||||
ExecutionController.EvalResult.class
|
ExecutionController.EvalResult.class
|
||||||
);
|
);
|
||||||
|
assertThat(evalResult.getError(), nullValue());
|
||||||
|
assertThat(evalResult.getStackTrace(), nullValue());
|
||||||
assertThat(evalResult.getResult(), is("******"));
|
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) {
|
private ExecutionController.EvalResult eval(Execution execution, String expression, int index) {
|
||||||
|
|||||||
@@ -103,6 +103,13 @@ public class NamespaceControllerTest {
|
|||||||
);
|
);
|
||||||
assertThat(list.getTotal(), is(3L));
|
assertThat(list.getTotal(), is(3L));
|
||||||
assertThat(list.getResults().size(), is(3));
|
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
|
@Test
|
||||||
|
|||||||
Reference in New Issue
Block a user