mirror of
https://github.com/kestra-io/kestra.git
synced 2025-12-26 05:00:31 -05:00
Compare commits
74 Commits
dependabot
...
v1.1.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f6a1cf377 | ||
|
|
420e081c69 | ||
|
|
0a7fffe1c5 | ||
|
|
48d14c9ed9 | ||
|
|
21a42a072a | ||
|
|
4f48ea0c21 | ||
|
|
890fa791e8 | ||
|
|
5e57de5cdf | ||
|
|
cf2c6cd2b1 | ||
|
|
b688dbc30b | ||
|
|
40877cc1cc | ||
|
|
c0f178a159 | ||
|
|
c64a083ac7 | ||
|
|
ccf9d9b303 | ||
|
|
25dbdbd713 | ||
|
|
d54477051f | ||
|
|
54a63d1b04 | ||
|
|
6f271e5694 | ||
|
|
0a718dab30 | ||
|
|
ec522a6d44 | ||
|
|
ad73a46b0c | ||
|
|
ca56559c49 | ||
|
|
ed739ec257 | ||
|
|
9effef9fcd | ||
|
|
ffc61b2482 | ||
|
|
fbbc0824ff | ||
|
|
842b8d604b | ||
|
|
bd5ac06c5b | ||
|
|
335fe1e88c | ||
|
|
5c52ab300a | ||
|
|
756069f1a6 | ||
|
|
faba958f08 | ||
|
|
a772a61d62 | ||
|
|
f2cb79cb98 | ||
|
|
9ea0b1cebb | ||
|
|
867dc20d47 | ||
|
|
c669759afb | ||
|
|
7e3cd8a2cb | ||
|
|
f203c5f43a | ||
|
|
f4e90cc540 | ||
|
|
ce0fd58c94 | ||
|
|
f1b950941c | ||
|
|
559f3f2634 | ||
|
|
9bc65b84f1 | ||
|
|
223b137381 | ||
|
|
80d1df6eeb | ||
|
|
a87e7f3b8d | ||
|
|
710862ef33 | ||
|
|
d74f535ea1 | ||
|
|
1673f24356 | ||
|
|
2ad90625b8 | ||
|
|
e77b80a1a8 | ||
|
|
6223b1f672 | ||
|
|
23329f4d48 | ||
|
|
ed60cb6670 | ||
|
|
f6306883b4 | ||
|
|
89433dc04c | ||
|
|
4837408c59 | ||
|
|
5a8c36caa5 | ||
|
|
a2335abc0c | ||
|
|
310a7bbbe9 | ||
|
|
162feaf38c | ||
|
|
94050be49c | ||
|
|
848a5ac9d7 | ||
|
|
9ac7a9ce9a | ||
|
|
c42838f3e1 | ||
|
|
c499d62b63 | ||
|
|
8fbc62e12c | ||
|
|
ae143f29f4 | ||
|
|
e4a11fc9ce | ||
|
|
ebacfc70b9 | ||
|
|
5bf67180a3 | ||
|
|
1e670b5e7e | ||
|
|
0dacad5ee1 |
@@ -2,6 +2,7 @@ package io.kestra.cli.commands.migrations.metadata;
|
|||||||
|
|
||||||
import io.kestra.cli.AbstractCommand;
|
import io.kestra.cli.AbstractCommand;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.inject.Provider;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import picocli.CommandLine;
|
import picocli.CommandLine;
|
||||||
|
|
||||||
@@ -12,13 +13,13 @@ import picocli.CommandLine;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class KvMetadataMigrationCommand extends AbstractCommand {
|
public class KvMetadataMigrationCommand extends AbstractCommand {
|
||||||
@Inject
|
@Inject
|
||||||
private MetadataMigrationService metadataMigrationService;
|
private Provider<MetadataMigrationService> metadataMigrationServiceProvider;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Integer call() throws Exception {
|
public Integer call() throws Exception {
|
||||||
super.call();
|
super.call();
|
||||||
try {
|
try {
|
||||||
metadataMigrationService.kvMigration();
|
metadataMigrationServiceProvider.get().kvMigration();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
System.err.println("❌ KV Metadata migration failed: " + e.getMessage());
|
System.err.println("❌ KV Metadata migration failed: " + e.getMessage());
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package io.kestra.cli.commands.migrations.metadata;
|
|||||||
|
|
||||||
import io.kestra.cli.AbstractCommand;
|
import io.kestra.cli.AbstractCommand;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.inject.Provider;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import picocli.CommandLine;
|
import picocli.CommandLine;
|
||||||
|
|
||||||
@@ -12,13 +13,13 @@ import picocli.CommandLine;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class SecretsMetadataMigrationCommand extends AbstractCommand {
|
public class SecretsMetadataMigrationCommand extends AbstractCommand {
|
||||||
@Inject
|
@Inject
|
||||||
private MetadataMigrationService metadataMigrationService;
|
private Provider<MetadataMigrationService> metadataMigrationServiceProvider;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Integer call() throws Exception {
|
public Integer call() throws Exception {
|
||||||
super.call();
|
super.call();
|
||||||
try {
|
try {
|
||||||
metadataMigrationService.secretMigration();
|
metadataMigrationServiceProvider.get().secretMigration();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
System.err.println("❌ Secrets Metadata migration failed: " + e.getMessage());
|
System.err.println("❌ Secrets Metadata migration failed: " + e.getMessage());
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
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.cli.services.TenantIdSelectorService;
|
||||||
import io.kestra.core.models.ServerType;
|
import io.kestra.core.models.ServerType;
|
||||||
|
import io.kestra.core.repositories.LocalFlowRepositoryLoader;
|
||||||
import io.kestra.core.runners.ExecutorInterface;
|
import io.kestra.core.runners.ExecutorInterface;
|
||||||
import io.kestra.core.services.SkipExecutionService;
|
import io.kestra.core.services.SkipExecutionService;
|
||||||
import io.kestra.core.services.StartExecutorService;
|
import io.kestra.core.services.StartExecutorService;
|
||||||
@@ -10,6 +12,8 @@ import io.micronaut.context.ApplicationContext;
|
|||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
import picocli.CommandLine;
|
import picocli.CommandLine;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -19,6 +23,9 @@ import java.util.Map;
|
|||||||
description = "Start the Kestra executor"
|
description = "Start the Kestra executor"
|
||||||
)
|
)
|
||||||
public class ExecutorCommand extends AbstractServerCommand {
|
public class ExecutorCommand extends AbstractServerCommand {
|
||||||
|
@CommandLine.Spec
|
||||||
|
CommandLine.Model.CommandSpec spec;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
private ApplicationContext applicationContext;
|
private ApplicationContext applicationContext;
|
||||||
|
|
||||||
@@ -28,22 +35,28 @@ public class ExecutorCommand extends AbstractServerCommand {
|
|||||||
@Inject
|
@Inject
|
||||||
private StartExecutorService startExecutorService;
|
private StartExecutorService startExecutorService;
|
||||||
|
|
||||||
@CommandLine.Option(names = {"--skip-executions"}, split=",", description = "The list of execution identifiers to skip, separated by a coma; for troubleshooting purpose only")
|
@CommandLine.Option(names = {"-f", "--flow-path"}, description = "Tenant identifier required to load flows from the specified path")
|
||||||
|
private File flowPath;
|
||||||
|
|
||||||
|
@CommandLine.Option(names = "--tenant", description = "Tenant identifier, Required to load flows from path")
|
||||||
|
private String tenantId;
|
||||||
|
|
||||||
|
@CommandLine.Option(names = {"--skip-executions"}, split=",", description = "List of execution IDs to skip, separated by commas; for troubleshooting only")
|
||||||
private List<String> skipExecutions = Collections.emptyList();
|
private List<String> skipExecutions = Collections.emptyList();
|
||||||
|
|
||||||
@CommandLine.Option(names = {"--skip-flows"}, split=",", description = "The list of flow identifiers (tenant|namespace|flowId) to skip, separated by a coma; for troubleshooting purpose only")
|
@CommandLine.Option(names = {"--skip-flows"}, split=",", description = "List of flow identifiers (tenant|namespace|flowId) to skip, separated by a coma; for troubleshooting only")
|
||||||
private List<String> skipFlows = Collections.emptyList();
|
private List<String> skipFlows = Collections.emptyList();
|
||||||
|
|
||||||
@CommandLine.Option(names = {"--skip-namespaces"}, split=",", description = "The list of namespace identifiers (tenant|namespace) to skip, separated by a coma; for troubleshooting purpose only")
|
@CommandLine.Option(names = {"--skip-namespaces"}, split=",", description = "List of namespace identifiers (tenant|namespace) to skip, separated by a coma; for troubleshooting only")
|
||||||
private List<String> skipNamespaces = Collections.emptyList();
|
private List<String> skipNamespaces = Collections.emptyList();
|
||||||
|
|
||||||
@CommandLine.Option(names = {"--skip-tenants"}, split=",", description = "The list of tenants to skip, separated by a coma; for troubleshooting purpose only")
|
@CommandLine.Option(names = {"--skip-tenants"}, split=",", description = "List of tenants to skip, separated by a coma; for troubleshooting only")
|
||||||
private List<String> skipTenants = Collections.emptyList();
|
private List<String> skipTenants = Collections.emptyList();
|
||||||
|
|
||||||
@CommandLine.Option(names = {"--start-executors"}, split=",", description = "The list of Kafka Stream executors to start, separated by a command. Use it only with the Kafka queue, for debugging purpose.")
|
@CommandLine.Option(names = {"--start-executors"}, split=",", description = "List of Kafka Stream executors to start, separated by a command. Use it only with the Kafka queue; for debugging only")
|
||||||
private List<String> startExecutors = Collections.emptyList();
|
private List<String> startExecutors = Collections.emptyList();
|
||||||
|
|
||||||
@CommandLine.Option(names = {"--not-start-executors"}, split=",", description = "The list of Kafka Stream executors to not start, separated by a command. Use it only with the Kafka queue, for debugging purpose.")
|
@CommandLine.Option(names = {"--not-start-executors"}, split=",", description = "Lst of Kafka Stream executors to not start, separated by a command. Use it only with the Kafka queue; for debugging only")
|
||||||
private List<String> notStartExecutors = Collections.emptyList();
|
private List<String> notStartExecutors = Collections.emptyList();
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
@@ -64,6 +77,16 @@ public class ExecutorCommand extends AbstractServerCommand {
|
|||||||
|
|
||||||
super.call();
|
super.call();
|
||||||
|
|
||||||
|
if (flowPath != null) {
|
||||||
|
try {
|
||||||
|
LocalFlowRepositoryLoader localFlowRepositoryLoader = applicationContext.getBean(LocalFlowRepositoryLoader.class);
|
||||||
|
TenantIdSelectorService tenantIdSelectorService = applicationContext.getBean(TenantIdSelectorService.class);
|
||||||
|
localFlowRepositoryLoader.load(tenantIdSelectorService.getTenantId(this.tenantId), this.flowPath);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new CommandLine.ParameterException(this.spec.commandLine(), "Invalid flow path", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ExecutorInterface executorService = applicationContext.getBean(ExecutorInterface.class);
|
ExecutorInterface executorService = applicationContext.getBean(ExecutorInterface.class);
|
||||||
executorService.run();
|
executorService.run();
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ public class IndexerCommand extends AbstractServerCommand {
|
|||||||
@Inject
|
@Inject
|
||||||
private SkipExecutionService skipExecutionService;
|
private SkipExecutionService skipExecutionService;
|
||||||
|
|
||||||
@CommandLine.Option(names = {"--skip-indexer-records"}, split=",", description = "a list of indexer record keys, separated by a coma; for troubleshooting purpose only")
|
@CommandLine.Option(names = {"--skip-indexer-records"}, split=",", description = "a list of indexer record keys, separated by a coma; for troubleshooting only")
|
||||||
private List<String> skipIndexerRecords = Collections.emptyList();
|
private List<String> skipIndexerRecords = Collections.emptyList();
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ public class StandAloneCommand extends AbstractServerCommand {
|
|||||||
@Nullable
|
@Nullable
|
||||||
private FileChangedEventListener fileWatcher;
|
private FileChangedEventListener fileWatcher;
|
||||||
|
|
||||||
@CommandLine.Option(names = {"-f", "--flow-path"}, description = "the flow path containing flow to inject at startup (when running with a memory flow repository)")
|
@CommandLine.Option(names = {"-f", "--flow-path"}, description = "Tenant identifier required to load flows from the specified path")
|
||||||
private File flowPath;
|
private File flowPath;
|
||||||
|
|
||||||
@CommandLine.Option(names = "--tenant", description = "Tenant identifier, Required to load flows from path with the enterprise edition")
|
@CommandLine.Option(names = "--tenant", description = "Tenant identifier, Required to load flows from path with the enterprise edition")
|
||||||
@@ -51,19 +51,19 @@ public class StandAloneCommand extends AbstractServerCommand {
|
|||||||
@CommandLine.Option(names = {"--worker-thread"}, description = "the number of worker threads, defaults to eight times the number of available processors. Set it to 0 to avoid starting a worker.")
|
@CommandLine.Option(names = {"--worker-thread"}, description = "the number of worker threads, defaults to eight times the number of available processors. Set it to 0 to avoid starting a worker.")
|
||||||
private int workerThread = defaultWorkerThread();
|
private int workerThread = defaultWorkerThread();
|
||||||
|
|
||||||
@CommandLine.Option(names = {"--skip-executions"}, split=",", description = "a list of execution identifiers to skip, separated by a coma; for troubleshooting purpose only")
|
@CommandLine.Option(names = {"--skip-executions"}, split=",", description = "a list of execution identifiers to skip, separated by a coma; for troubleshooting only")
|
||||||
private List<String> skipExecutions = Collections.emptyList();
|
private List<String> skipExecutions = Collections.emptyList();
|
||||||
|
|
||||||
@CommandLine.Option(names = {"--skip-flows"}, split=",", description = "a list of flow identifiers (namespace.flowId) to skip, separated by a coma; for troubleshooting purpose only")
|
@CommandLine.Option(names = {"--skip-flows"}, split=",", description = "a list of flow identifiers (namespace.flowId) to skip, separated by a coma; for troubleshooting only")
|
||||||
private List<String> skipFlows = Collections.emptyList();
|
private List<String> skipFlows = Collections.emptyList();
|
||||||
|
|
||||||
@CommandLine.Option(names = {"--skip-namespaces"}, split=",", description = "a list of namespace identifiers (tenant|namespace) to skip, separated by a coma; for troubleshooting purpose only")
|
@CommandLine.Option(names = {"--skip-namespaces"}, split=",", description = "a list of namespace identifiers (tenant|namespace) to skip, separated by a coma; for troubleshooting only")
|
||||||
private List<String> skipNamespaces = Collections.emptyList();
|
private List<String> skipNamespaces = Collections.emptyList();
|
||||||
|
|
||||||
@CommandLine.Option(names = {"--skip-tenants"}, split=",", description = "a list of tenants to skip, separated by a coma; for troubleshooting purpose only")
|
@CommandLine.Option(names = {"--skip-tenants"}, split=",", description = "a list of tenants to skip, separated by a coma; for troubleshooting only")
|
||||||
private List<String> skipTenants = Collections.emptyList();
|
private List<String> skipTenants = Collections.emptyList();
|
||||||
|
|
||||||
@CommandLine.Option(names = {"--skip-indexer-records"}, split=",", description = "a list of indexer record keys, separated by a coma; for troubleshooting purpose only")
|
@CommandLine.Option(names = {"--skip-indexer-records"}, split=",", description = "a list of indexer record keys, separated by a coma; for troubleshooting only")
|
||||||
private List<String> skipIndexerRecords = Collections.emptyList();
|
private List<String> skipIndexerRecords = Collections.emptyList();
|
||||||
|
|
||||||
@CommandLine.Option(names = {"--no-tutorials"}, description = "Flag to disable auto-loading of tutorial flows.")
|
@CommandLine.Option(names = {"--no-tutorials"}, description = "Flag to disable auto-loading of tutorial flows.")
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ public class WebServerCommand extends AbstractServerCommand {
|
|||||||
@Option(names = {"--no-indexer"}, description = "Flag to disable starting an embedded indexer.")
|
@Option(names = {"--no-indexer"}, description = "Flag to disable starting an embedded indexer.")
|
||||||
private boolean indexerDisabled = false;
|
private boolean indexerDisabled = false;
|
||||||
|
|
||||||
@CommandLine.Option(names = {"--skip-indexer-records"}, split=",", description = "a list of indexer record keys, separated by a coma; for troubleshooting purpose only")
|
@CommandLine.Option(names = {"--skip-indexer-records"}, split=",", description = "a list of indexer record keys, separated by a coma; for troubleshooting only")
|
||||||
private List<String> skipIndexerRecords = Collections.emptyList();
|
private List<String> skipIndexerRecords = Collections.emptyList();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -30,15 +30,15 @@ micronaut:
|
|||||||
read-idle-timeout: 60m
|
read-idle-timeout: 60m
|
||||||
write-idle-timeout: 60m
|
write-idle-timeout: 60m
|
||||||
idle-timeout: 60m
|
idle-timeout: 60m
|
||||||
netty:
|
|
||||||
max-zstd-encode-size: 67108864 # increased to 64MB from the default of 32MB
|
|
||||||
max-chunk-size: 10MB
|
|
||||||
max-header-size: 32768 # increased from the default of 8k
|
|
||||||
responses:
|
responses:
|
||||||
file:
|
file:
|
||||||
cache-seconds: 86400
|
cache-seconds: 86400
|
||||||
cache-control:
|
cache-control:
|
||||||
public: true
|
public: true
|
||||||
|
netty:
|
||||||
|
max-zstd-encode-size: 67108864 # increased to 64MB from the default of 32MB
|
||||||
|
max-chunk-size: 10MB
|
||||||
|
max-header-size: 32768 # increased from the default of 8k
|
||||||
|
|
||||||
# Access log configuration, see https://docs.micronaut.io/latest/guide/index.html#accessLogger
|
# Access log configuration, see https://docs.micronaut.io/latest/guide/index.html#accessLogger
|
||||||
access-logger:
|
access-logger:
|
||||||
|
|||||||
@@ -68,7 +68,8 @@ class NoConfigCommandTest {
|
|||||||
|
|
||||||
|
|
||||||
assertThat(exitCode).isNotZero();
|
assertThat(exitCode).isNotZero();
|
||||||
assertThat(out.toString()).isEmpty();
|
// check that the only log is an access log: this has the advantage to also check that access log is working!
|
||||||
|
assertThat(out.toString()).contains("POST /api/v1/main/flows HTTP/1.1 | status: 500");
|
||||||
assertThat(err.toString()).contains("No bean of type [io.kestra.core.repositories.FlowRepositoryInterface] exists");
|
assertThat(err.toString()).contains("No bean of type [io.kestra.core.repositories.FlowRepositoryInterface] exists");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import io.kestra.core.models.annotations.Plugin;
|
|||||||
import io.kestra.core.models.dashboards.filters.AbstractFilter;
|
import io.kestra.core.models.dashboards.filters.AbstractFilter;
|
||||||
import io.kestra.core.repositories.QueryBuilderInterface;
|
import io.kestra.core.repositories.QueryBuilderInterface;
|
||||||
import io.kestra.plugin.core.dashboard.data.IData;
|
import io.kestra.plugin.core.dashboard.data.IData;
|
||||||
|
import jakarta.annotation.Nullable;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import jakarta.validation.constraints.Pattern;
|
import jakarta.validation.constraints.Pattern;
|
||||||
@@ -33,9 +35,12 @@ public abstract class DataFilter<F extends Enum<F>, C extends ColumnDescriptor<F
|
|||||||
@Pattern(regexp = JAVA_IDENTIFIER_REGEX)
|
@Pattern(regexp = JAVA_IDENTIFIER_REGEX)
|
||||||
private String type;
|
private String type;
|
||||||
|
|
||||||
|
@Valid
|
||||||
private Map<String, C> columns;
|
private Map<String, C> columns;
|
||||||
|
|
||||||
@Setter
|
@Setter
|
||||||
|
@Valid
|
||||||
|
@Nullable
|
||||||
private List<AbstractFilter<F>> where;
|
private List<AbstractFilter<F>> where;
|
||||||
|
|
||||||
private List<OrderBy> orderBy;
|
private List<OrderBy> orderBy;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import io.kestra.core.models.annotations.Plugin;
|
|||||||
import io.kestra.core.models.dashboards.ChartOption;
|
import io.kestra.core.models.dashboards.ChartOption;
|
||||||
import io.kestra.core.models.dashboards.DataFilter;
|
import io.kestra.core.models.dashboards.DataFilter;
|
||||||
import io.kestra.core.validations.DataChartValidation;
|
import io.kestra.core.validations.DataChartValidation;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
@@ -20,6 +21,7 @@ import lombok.experimental.SuperBuilder;
|
|||||||
@DataChartValidation
|
@DataChartValidation
|
||||||
public abstract class DataChart<P extends ChartOption, D extends DataFilter<?, ?>> extends Chart<P> implements io.kestra.core.models.Plugin {
|
public abstract class DataChart<P extends ChartOption, D extends DataFilter<?, ?>> extends Chart<P> implements io.kestra.core.models.Plugin {
|
||||||
@NotNull
|
@NotNull
|
||||||
|
@Valid
|
||||||
private D data;
|
private D data;
|
||||||
|
|
||||||
public Integer minNumberOfAggregations() {
|
public Integer minNumberOfAggregations() {
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
package io.kestra.core.models.dashboards.filters;
|
package io.kestra.core.models.dashboards.filters;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import com.fasterxml.jackson.annotation.JsonSubTypes;
|
import com.fasterxml.jackson.annotation.JsonSubTypes;
|
||||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||||
import io.micronaut.core.annotation.Introspected;
|
import io.micronaut.core.annotation.Introspected;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.experimental.SuperBuilder;
|
import lombok.experimental.SuperBuilder;
|
||||||
@@ -32,6 +35,9 @@ import lombok.experimental.SuperBuilder;
|
|||||||
@SuperBuilder
|
@SuperBuilder
|
||||||
@Introspected
|
@Introspected
|
||||||
public abstract class AbstractFilter<F extends Enum<F>> {
|
public abstract class AbstractFilter<F extends Enum<F>> {
|
||||||
|
@NotNull
|
||||||
|
@JsonProperty(value = "field", required = true)
|
||||||
|
@Valid
|
||||||
private F field;
|
private F field;
|
||||||
private String labelKey;
|
private String labelKey;
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
package io.kestra.core.models.executions;
|
package io.kestra.core.models.executions;
|
||||||
|
|
||||||
import io.micronaut.core.annotation.Introspected;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Value;
|
|
||||||
import io.kestra.core.models.tasks.Output;
|
import io.kestra.core.models.tasks.Output;
|
||||||
import io.kestra.core.models.triggers.AbstractTrigger;
|
import io.kestra.core.models.triggers.AbstractTrigger;
|
||||||
|
import io.micronaut.core.annotation.Introspected;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Value;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import jakarta.validation.constraints.NotNull;
|
|
||||||
|
|
||||||
@Value
|
@Value
|
||||||
@Builder
|
@Builder
|
||||||
@@ -21,6 +22,7 @@ public class ExecutionTrigger {
|
|||||||
@NotNull
|
@NotNull
|
||||||
String type;
|
String type;
|
||||||
|
|
||||||
|
@Schema(type = "object", additionalProperties = Schema.AdditionalPropertiesValue.TRUE)
|
||||||
Map<String, Object> variables;
|
Map<String, Object> variables;
|
||||||
|
|
||||||
URI logFile;
|
URI logFile;
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ import static io.kestra.core.utils.Rethrow.throwFunction;
|
|||||||
@JsonDeserialize(using = Property.PropertyDeserializer.class)
|
@JsonDeserialize(using = Property.PropertyDeserializer.class)
|
||||||
@JsonSerialize(using = Property.PropertySerializer.class)
|
@JsonSerialize(using = Property.PropertySerializer.class)
|
||||||
@Builder
|
@Builder
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor(access = AccessLevel.PACKAGE)
|
@AllArgsConstructor(access = AccessLevel.PACKAGE)
|
||||||
@Schema(
|
@Schema(
|
||||||
oneOf = {
|
oneOf = {
|
||||||
@@ -51,6 +50,7 @@ public class Property<T> {
|
|||||||
.copy()
|
.copy()
|
||||||
.configure(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS, false);
|
.configure(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS, false);
|
||||||
|
|
||||||
|
private final boolean skipCache;
|
||||||
private String expression;
|
private String expression;
|
||||||
private T value;
|
private T value;
|
||||||
|
|
||||||
@@ -60,13 +60,23 @@ public class Property<T> {
|
|||||||
@Deprecated
|
@Deprecated
|
||||||
// Note: when not used, this constructor would not be deleted but made private so it can only be used by ofExpression(String) and the deserializer
|
// Note: when not used, this constructor would not be deleted but made private so it can only be used by ofExpression(String) and the deserializer
|
||||||
public Property(String expression) {
|
public Property(String expression) {
|
||||||
this.expression = expression;
|
this(expression, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Property(String expression, boolean skipCache) {
|
||||||
|
this.expression = expression;
|
||||||
|
this.skipCache = skipCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use {@link #ofValue(Object)} instead.
|
||||||
|
*/
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
|
@Deprecated
|
||||||
public Property(Map<?, ?> map) {
|
public Property(Map<?, ?> map) {
|
||||||
try {
|
try {
|
||||||
expression = MAPPER.writeValueAsString(map);
|
expression = MAPPER.writeValueAsString(map);
|
||||||
|
this.skipCache = false;
|
||||||
} catch (JsonProcessingException e) {
|
} catch (JsonProcessingException e) {
|
||||||
throw new IllegalArgumentException(e);
|
throw new IllegalArgumentException(e);
|
||||||
}
|
}
|
||||||
@@ -79,9 +89,6 @@ public class Property<T> {
|
|||||||
/**
|
/**
|
||||||
* Returns a new {@link Property} with no cached rendered value,
|
* Returns a new {@link Property} with no cached rendered value,
|
||||||
* so that the next render will evaluate its original Pebble expression.
|
* so that the next render will evaluate its original Pebble expression.
|
||||||
* <p>
|
|
||||||
* The returned property will still cache its rendered result.
|
|
||||||
* To re-evaluate on a subsequent render, call {@code skipCache()} again.
|
|
||||||
*
|
*
|
||||||
* @return a new {@link Property} without a pre-rendered value
|
* @return a new {@link Property} without a pre-rendered value
|
||||||
*/
|
*/
|
||||||
@@ -133,6 +140,7 @@ public class Property<T> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Build a new Property object with a Pebble expression.<br>
|
* Build a new Property object with a Pebble expression.<br>
|
||||||
|
* This property object will not cache its rendered value.
|
||||||
* <p>
|
* <p>
|
||||||
* Use {@link #ofValue(Object)} to build a property with a value instead.
|
* Use {@link #ofValue(Object)} to build a property with a value instead.
|
||||||
*/
|
*/
|
||||||
@@ -142,11 +150,11 @@ public class Property<T> {
|
|||||||
throw new IllegalArgumentException("'expression' must be a valid Pebble expression");
|
throw new IllegalArgumentException("'expression' must be a valid Pebble expression");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Property<>(expression);
|
return new Property<>(expression, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render a property then convert it to its target type.<br>
|
* Render a property, then convert it to its target type.<br>
|
||||||
* <p>
|
* <p>
|
||||||
* This method is designed to be used only by the {@link io.kestra.core.runners.RunContextProperty}.
|
* This method is designed to be used only by the {@link io.kestra.core.runners.RunContextProperty}.
|
||||||
*
|
*
|
||||||
@@ -164,7 +172,7 @@ public class Property<T> {
|
|||||||
* @see io.kestra.core.runners.RunContextProperty#as(Class, Map)
|
* @see io.kestra.core.runners.RunContextProperty#as(Class, Map)
|
||||||
*/
|
*/
|
||||||
public static <T> T as(Property<T> property, PropertyContext context, Class<T> clazz, Map<String, Object> variables) throws IllegalVariableEvaluationException {
|
public static <T> T as(Property<T> property, PropertyContext context, Class<T> clazz, Map<String, Object> variables) throws IllegalVariableEvaluationException {
|
||||||
if (property.value == null) {
|
if (property.skipCache || property.value == null) {
|
||||||
String rendered = context.render(property.expression, variables);
|
String rendered = context.render(property.expression, variables);
|
||||||
property.value = MAPPER.convertValue(rendered, clazz);
|
property.value = MAPPER.convertValue(rendered, clazz);
|
||||||
}
|
}
|
||||||
@@ -192,7 +200,7 @@ public class Property<T> {
|
|||||||
*/
|
*/
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public static <T, I> T asList(Property<T> property, PropertyContext context, Class<I> itemClazz, Map<String, Object> variables) throws IllegalVariableEvaluationException {
|
public static <T, I> T asList(Property<T> property, PropertyContext context, Class<I> itemClazz, Map<String, Object> variables) throws IllegalVariableEvaluationException {
|
||||||
if (property.value == null) {
|
if (property.skipCache || property.value == null) {
|
||||||
JavaType type = MAPPER.getTypeFactory().constructCollectionLikeType(List.class, itemClazz);
|
JavaType type = MAPPER.getTypeFactory().constructCollectionLikeType(List.class, itemClazz);
|
||||||
try {
|
try {
|
||||||
String trimmedExpression = property.expression.trim();
|
String trimmedExpression = property.expression.trim();
|
||||||
@@ -244,7 +252,7 @@ public class Property<T> {
|
|||||||
*/
|
*/
|
||||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||||
public static <T, K, V> T asMap(Property<T> property, RunContext runContext, Class<K> keyClass, Class<V> valueClass, Map<String, Object> variables) throws IllegalVariableEvaluationException {
|
public static <T, K, V> T asMap(Property<T> property, RunContext runContext, Class<K> keyClass, Class<V> valueClass, Map<String, Object> variables) throws IllegalVariableEvaluationException {
|
||||||
if (property.value == null) {
|
if (property.skipCache || property.value == null) {
|
||||||
JavaType targetMapType = MAPPER.getTypeFactory().constructMapType(Map.class, keyClass, valueClass);
|
JavaType targetMapType = MAPPER.getTypeFactory().constructMapType(Map.class, keyClass, valueClass);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -82,8 +82,7 @@ public abstract class FilesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static String resolveUniqueNameForFile(final Path path) {
|
private static String resolveUniqueNameForFile(final Path path) {
|
||||||
String filename = path.getFileName().toString();
|
String filename = path.getFileName().toString().replace(' ', '+');
|
||||||
String encodedFilename = java.net.URLEncoder.encode(filename, java.nio.charset.StandardCharsets.UTF_8);
|
return IdUtils.from(path.toString()) + "-" + filename;
|
||||||
return IdUtils.from(path.toString()) + "-" + encodedFilename;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,10 +151,7 @@ abstract class AbstractFileFunction implements Function {
|
|||||||
// if there is a trigger of type execution, we also allow accessing a file from the parent execution
|
// if there is a trigger of type execution, we also allow accessing a file from the parent execution
|
||||||
Map<String, String> trigger = (Map<String, String>) context.getVariable(TRIGGER);
|
Map<String, String> trigger = (Map<String, String>) context.getVariable(TRIGGER);
|
||||||
|
|
||||||
if (!isFileUriValid(trigger.get(NAMESPACE), trigger.get("flowId"), trigger.get("executionId"), path)) {
|
return isFileUriValid(trigger.get(NAMESPACE), trigger.get("flowId"), trigger.get("executionId"), path);
|
||||||
throw new IllegalArgumentException("Unable to read the file '" + path + "' as it didn't belong to the parent execution");
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -383,6 +383,7 @@ public class ExecutionService {
|
|||||||
if (!isFlowable || s.equals(taskRunId)) {
|
if (!isFlowable || s.equals(taskRunId)) {
|
||||||
TaskRun newTaskRun;
|
TaskRun newTaskRun;
|
||||||
|
|
||||||
|
State.Type targetState = newState;
|
||||||
if (task instanceof Pause pauseTask) {
|
if (task instanceof Pause pauseTask) {
|
||||||
State.Type terminalState = newState == State.Type.RUNNING ? State.Type.SUCCESS : newState;
|
State.Type terminalState = newState == State.Type.RUNNING ? State.Type.SUCCESS : newState;
|
||||||
Pause.Resumed _resumed = resumed != null ? resumed : Pause.Resumed.now(terminalState);
|
Pause.Resumed _resumed = resumed != null ? resumed : Pause.Resumed.now(terminalState);
|
||||||
@@ -392,23 +393,23 @@ public class ExecutionService {
|
|||||||
// if it's a Pause task with no subtask, we terminate the task
|
// if it's a Pause task with no subtask, we terminate the task
|
||||||
if (ListUtils.isEmpty(pauseTask.getTasks()) && ListUtils.isEmpty(pauseTask.getErrors()) && ListUtils.isEmpty(pauseTask.getFinally())) {
|
if (ListUtils.isEmpty(pauseTask.getTasks()) && ListUtils.isEmpty(pauseTask.getErrors()) && ListUtils.isEmpty(pauseTask.getFinally())) {
|
||||||
if (newState == State.Type.RUNNING) {
|
if (newState == State.Type.RUNNING) {
|
||||||
newTaskRun = newTaskRun.withState(State.Type.SUCCESS);
|
targetState = State.Type.SUCCESS;
|
||||||
} else if (newState == State.Type.KILLING) {
|
} else if (newState == State.Type.KILLING) {
|
||||||
newTaskRun = newTaskRun.withState(State.Type.KILLED);
|
targetState = State.Type.KILLED;
|
||||||
} else {
|
|
||||||
newTaskRun = newTaskRun.withState(newState);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// we should set the state to RUNNING so that subtasks are executed
|
// we should set the state to RUNNING so that subtasks are executed
|
||||||
newTaskRun = newTaskRun.withState(State.Type.RUNNING);
|
targetState = State.Type.RUNNING;
|
||||||
}
|
}
|
||||||
|
newTaskRun = newTaskRun.withState(targetState);
|
||||||
} else {
|
} else {
|
||||||
newTaskRun = originalTaskRun.withState(newState);
|
newTaskRun = originalTaskRun.withState(targetState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (originalTaskRun.getAttempts() != null && !originalTaskRun.getAttempts().isEmpty()) {
|
if (originalTaskRun.getAttempts() != null && !originalTaskRun.getAttempts().isEmpty()) {
|
||||||
ArrayList<TaskRunAttempt> attempts = new ArrayList<>(originalTaskRun.getAttempts());
|
ArrayList<TaskRunAttempt> attempts = new ArrayList<>(originalTaskRun.getAttempts());
|
||||||
attempts.set(attempts.size() - 1, attempts.getLast().withState(newState));
|
attempts.set(attempts.size() - 1, attempts.getLast().withState(targetState));
|
||||||
newTaskRun = newTaskRun.withAttempts(attempts);
|
newTaskRun = newTaskRun.withAttempts(attempts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,11 +33,13 @@ public class ExecutionsDataFilterValidator implements ConstraintValidator<Execut
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
executionsDataFilter.getWhere().forEach(filter -> {
|
if (executionsDataFilter.getWhere() != null) {
|
||||||
if (filter.getField() == Executions.Fields.LABELS && filter.getLabelKey() == null) {
|
executionsDataFilter.getWhere().forEach(filter -> {
|
||||||
violations.add("Label filters must have a `labelKey`.");
|
if (filter.getField() == Executions.Fields.LABELS && filter.getLabelKey() == null) {
|
||||||
}
|
violations.add("Label filters must have a `labelKey`.");
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!violations.isEmpty()) {
|
if (!violations.isEmpty()) {
|
||||||
context.disableDefaultConstraintViolation();
|
context.disableDefaultConstraintViolation();
|
||||||
|
|||||||
@@ -20,8 +20,6 @@ import java.io.BufferedOutputStream;
|
|||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URLEncoder;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
@@ -60,7 +58,15 @@ import static io.kestra.core.utils.Rethrow.throwConsumer;
|
|||||||
public class Download extends AbstractHttp implements RunnableTask<Download.Output> {
|
public class Download extends AbstractHttp implements RunnableTask<Download.Output> {
|
||||||
@Schema(title = "Should the task fail when downloading an empty file.")
|
@Schema(title = "Should the task fail when downloading an empty file.")
|
||||||
@Builder.Default
|
@Builder.Default
|
||||||
private final Property<Boolean> failOnEmptyResponse = Property.ofValue(true);
|
private Property<Boolean> failOnEmptyResponse = Property.ofValue(true);
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
title = "Name of the file inside the output.",
|
||||||
|
description = """
|
||||||
|
If not provided, the filename will be extracted from the `Content-Disposition` header.
|
||||||
|
If no `Content-Disposition` header, a name would be generated."""
|
||||||
|
)
|
||||||
|
private Property<String> saveAs;
|
||||||
|
|
||||||
public Output run(RunContext runContext) throws Exception {
|
public Output run(RunContext runContext) throws Exception {
|
||||||
Logger logger = runContext.logger();
|
Logger logger = runContext.logger();
|
||||||
@@ -111,20 +117,22 @@ public class Download extends AbstractHttp implements RunnableTask<Download.Outp
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String filename = null;
|
String rFilename = runContext.render(this.saveAs).as(String.class).orElse(null);
|
||||||
if (response.getHeaders().firstValue("Content-Disposition").isPresent()) {
|
if (rFilename == null) {
|
||||||
String contentDisposition = response.getHeaders().firstValue("Content-Disposition").orElseThrow();
|
if (response.getHeaders().firstValue("Content-Disposition").isPresent()) {
|
||||||
filename = filenameFromHeader(runContext, contentDisposition);
|
String contentDisposition = response.getHeaders().firstValue("Content-Disposition").orElseThrow();
|
||||||
}
|
rFilename = filenameFromHeader(runContext, contentDisposition);
|
||||||
if (filename != null) {
|
if (rFilename != null) {
|
||||||
filename = URLEncoder.encode(filename, StandardCharsets.UTF_8);
|
rFilename = rFilename.replace(' ', '+');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug("File '{}' downloaded with size '{}'", from, size);
|
logger.debug("File '{}' downloaded with size '{}'", from, size);
|
||||||
|
|
||||||
return Output.builder()
|
return Output.builder()
|
||||||
.code(response.getStatus().getCode())
|
.code(response.getStatus().getCode())
|
||||||
.uri(runContext.storage().putFile(tempFile, filename))
|
.uri(runContext.storage().putFile(tempFile, rFilename))
|
||||||
.headers(response.getHeaders().map())
|
.headers(response.getHeaders().map())
|
||||||
.length(size.get())
|
.length(size.get())
|
||||||
.build();
|
.build();
|
||||||
|
|||||||
@@ -267,6 +267,18 @@ public abstract class AbstractRunnerTest {
|
|||||||
multipleConditionTriggerCaseTest.flowTriggerMultiplePreconditions();
|
multipleConditionTriggerCaseTest.flowTriggerMultiplePreconditions();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@LoadFlows({"flows/valids/flow-trigger-multiple-conditions-flow-a.yaml", "flows/valids/flow-trigger-multiple-conditions-flow-listen.yaml"})
|
||||||
|
void flowTriggerMultipleConditions() throws Exception {
|
||||||
|
multipleConditionTriggerCaseTest.flowTriggerMultipleConditions();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@LoadFlows({"flows/valids/flow-trigger-mixed-conditions-flow-a.yaml", "flows/valids/flow-trigger-mixed-conditions-flow-listen.yaml"})
|
||||||
|
void flowTriggerMixedConditions() throws Exception {
|
||||||
|
multipleConditionTriggerCaseTest.flowTriggerMixedConditions();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@LoadFlows({"flows/valids/each-null.yaml"})
|
@LoadFlows({"flows/valids/each-null.yaml"})
|
||||||
void eachWithNull() throws Exception {
|
void eachWithNull() throws Exception {
|
||||||
|
|||||||
@@ -445,6 +445,7 @@ class ExecutionServiceTest {
|
|||||||
|
|
||||||
assertThat(killed.getState().getCurrent()).isEqualTo(State.Type.CANCELLED);
|
assertThat(killed.getState().getCurrent()).isEqualTo(State.Type.CANCELLED);
|
||||||
assertThat(killed.findTaskRunsByTaskId("pause").getFirst().getState().getCurrent()).isEqualTo(State.Type.KILLED);
|
assertThat(killed.findTaskRunsByTaskId("pause").getFirst().getState().getCurrent()).isEqualTo(State.Type.KILLED);
|
||||||
|
assertThat(killed.findTaskRunsByTaskId("pause").getFirst().getAttempts().getFirst().getState().getCurrent()).isEqualTo(State.Type.KILLED);
|
||||||
assertThat(killed.getState().getHistories()).hasSize(5);
|
assertThat(killed.getState().getHistories()).hasSize(5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -106,28 +106,28 @@ class FilesServiceTest {
|
|||||||
var runContext = runContextFactory.of();
|
var runContext = runContextFactory.of();
|
||||||
|
|
||||||
Path fileWithSpace = tempDir.resolve("with space.txt");
|
Path fileWithSpace = tempDir.resolve("with space.txt");
|
||||||
Path fileWithUnicode = tempDir.resolve("สวัสดี.txt");
|
Path fileWithUnicode = tempDir.resolve("สวัสดี&.txt");
|
||||||
|
|
||||||
Files.writeString(fileWithSpace, "content");
|
Files.writeString(fileWithSpace, "content");
|
||||||
Files.writeString(fileWithUnicode, "content");
|
Files.writeString(fileWithUnicode, "content");
|
||||||
|
|
||||||
Path targetFileWithSpace = runContext.workingDir().path().resolve("with space.txt");
|
Path targetFileWithSpace = runContext.workingDir().path().resolve("with space.txt");
|
||||||
Path targetFileWithUnicode = runContext.workingDir().path().resolve("สวัสดี.txt");
|
Path targetFileWithUnicode = runContext.workingDir().path().resolve("สวัสดี&.txt");
|
||||||
|
|
||||||
Files.copy(fileWithSpace, targetFileWithSpace);
|
Files.copy(fileWithSpace, targetFileWithSpace);
|
||||||
Files.copy(fileWithUnicode, targetFileWithUnicode);
|
Files.copy(fileWithUnicode, targetFileWithUnicode);
|
||||||
|
|
||||||
Map<String, URI> outputFiles = FilesService.outputFiles(
|
Map<String, URI> outputFiles = FilesService.outputFiles(
|
||||||
runContext,
|
runContext,
|
||||||
List.of("with space.txt", "สวัสดี.txt")
|
List.of("with space.txt", "สวัสดี&.txt")
|
||||||
);
|
);
|
||||||
|
|
||||||
assertThat(outputFiles).hasSize(2);
|
assertThat(outputFiles).hasSize(2);
|
||||||
assertThat(outputFiles).containsKey("with space.txt");
|
assertThat(outputFiles).containsKey("with space.txt");
|
||||||
assertThat(outputFiles).containsKey("สวัสดี.txt");
|
assertThat(outputFiles).containsKey("สวัสดี&.txt");
|
||||||
|
|
||||||
assertThat(runContext.storage().getFile(outputFiles.get("with space.txt"))).isNotNull();
|
assertThat(runContext.storage().getFile(outputFiles.get("with space.txt"))).isNotNull();
|
||||||
assertThat(runContext.storage().getFile(outputFiles.get("สวัสดี.txt"))).isNotNull();
|
assertThat(runContext.storage().getFile(outputFiles.get("สวัสดี&.txt"))).isNotNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
private URI createFile() throws IOException {
|
private URI createFile() throws IOException {
|
||||||
|
|||||||
@@ -212,4 +212,44 @@ public class MultipleConditionTriggerCaseTest {
|
|||||||
e -> e.getState().getCurrent().equals(Type.SUCCESS),
|
e -> e.getState().getCurrent().equals(Type.SUCCESS),
|
||||||
MAIN_TENANT, "io.kestra.tests.trigger.multiple.preconditions", "flow-trigger-multiple-preconditions-flow-listen", Duration.ofSeconds(1)));
|
MAIN_TENANT, "io.kestra.tests.trigger.multiple.preconditions", "flow-trigger-multiple-preconditions-flow-listen", Duration.ofSeconds(1)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void flowTriggerMultipleConditions() throws TimeoutException, QueueException {
|
||||||
|
Execution execution = runnerUtils.runOne(MAIN_TENANT, "io.kestra.tests.trigger.multiple.conditions",
|
||||||
|
"flow-trigger-multiple-conditions-flow-a");
|
||||||
|
assertThat(execution.getTaskRunList().size()).isEqualTo(1);
|
||||||
|
assertThat(execution.getState().getCurrent()).isEqualTo(State.Type.SUCCESS);
|
||||||
|
|
||||||
|
// trigger is done
|
||||||
|
Execution triggerExecution = runnerUtils.awaitFlowExecution(
|
||||||
|
e -> e.getState().getCurrent().equals(Type.SUCCESS),
|
||||||
|
MAIN_TENANT, "io.kestra.tests.trigger.multiple.conditions", "flow-trigger-multiple-conditions-flow-listen");
|
||||||
|
executionRepository.delete(triggerExecution);
|
||||||
|
assertThat(triggerExecution.getTaskRunList().size()).isEqualTo(1);
|
||||||
|
assertThat(triggerExecution.getState().getCurrent()).isEqualTo(State.Type.SUCCESS);
|
||||||
|
|
||||||
|
// we assert that we didn't have any other flow triggered
|
||||||
|
assertThrows(RuntimeException.class, () -> runnerUtils.awaitFlowExecution(
|
||||||
|
e -> e.getState().getCurrent().equals(Type.SUCCESS),
|
||||||
|
MAIN_TENANT, "io.kestra.tests.trigger.multiple.conditions", "flow-trigger-multiple-conditions-flow-listen", Duration.ofSeconds(1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void flowTriggerMixedConditions() throws TimeoutException, QueueException {
|
||||||
|
Execution execution = runnerUtils.runOne(MAIN_TENANT, "io.kestra.tests.trigger.mixed.conditions",
|
||||||
|
"flow-trigger-mixed-conditions-flow-a");
|
||||||
|
assertThat(execution.getTaskRunList().size()).isEqualTo(1);
|
||||||
|
assertThat(execution.getState().getCurrent()).isEqualTo(State.Type.SUCCESS);
|
||||||
|
|
||||||
|
// trigger is done
|
||||||
|
Execution triggerExecution = runnerUtils.awaitFlowExecution(
|
||||||
|
e -> e.getState().getCurrent().equals(Type.SUCCESS),
|
||||||
|
MAIN_TENANT, "io.kestra.tests.trigger.mixed.conditions", "flow-trigger-mixed-conditions-flow-listen");
|
||||||
|
executionRepository.delete(triggerExecution);
|
||||||
|
assertThat(triggerExecution.getTaskRunList().size()).isEqualTo(1);
|
||||||
|
assertThat(triggerExecution.getState().getCurrent()).isEqualTo(State.Type.SUCCESS);
|
||||||
|
|
||||||
|
// we assert that we didn't have any other flow triggered
|
||||||
|
assertThrows(RuntimeException.class, () -> runnerUtils.awaitFlowExecution(
|
||||||
|
e -> e.getState().getCurrent().equals(Type.SUCCESS),
|
||||||
|
MAIN_TENANT, "io.kestra.tests.trigger.mixed.conditions", "flow-trigger-mixed-conditions-flow-listen", Duration.ofSeconds(1)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,6 +101,19 @@ class RunContextPropertyTest {
|
|||||||
var runContextProperty = new RunContextProperty<>(Property.<String>builder().expression("{{ variable }}").build(), runContext);
|
var runContextProperty = new RunContextProperty<>(Property.<String>builder().expression("{{ variable }}").build(), runContext);
|
||||||
|
|
||||||
assertThat(runContextProperty.as(String.class, Map.of("variable", "value1"))).isEqualTo(Optional.of("value1"));
|
assertThat(runContextProperty.as(String.class, Map.of("variable", "value1"))).isEqualTo(Optional.of("value1"));
|
||||||
assertThat(runContextProperty.skipCache().as(String.class, Map.of("variable", "value2"))).isEqualTo(Optional.of("value2"));
|
var skippedCache = runContextProperty.skipCache();
|
||||||
|
assertThat(skippedCache.as(String.class, Map.of("variable", "value2"))).isEqualTo(Optional.of("value2"));
|
||||||
|
// assure skipCache is preserved across calls
|
||||||
|
assertThat(skippedCache.as(String.class, Map.of("variable", "value3"))).isEqualTo(Optional.of("value3"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void asShouldNotReturnCachedRenderedPropertyWithOfExpression() throws IllegalVariableEvaluationException {
|
||||||
|
var runContext = runContextFactory.of();
|
||||||
|
|
||||||
|
var runContextProperty = new RunContextProperty<String>(Property.ofExpression("{{ variable }}"), runContext);
|
||||||
|
|
||||||
|
assertThat(runContextProperty.as(String.class, Map.of("variable", "value1"))).isEqualTo(Optional.of("value1"));
|
||||||
|
assertThat(runContextProperty.as(String.class, Map.of("variable", "value2"))).isEqualTo(Optional.of("value2"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -112,33 +112,6 @@ public class FileSizeFunctionTest {
|
|||||||
assertThat(size).isEqualTo(FILE_SIZE);
|
assertThat(size).isEqualTo(FILE_SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldThrowIllegalArgumentException_givenTrigger_andParentExecution_andMissingNamespace() throws IOException {
|
|
||||||
String executionId = IdUtils.create();
|
|
||||||
URI internalStorageURI = getInternalStorageURI(executionId);
|
|
||||||
URI internalStorageFile = getInternalStorageFile(internalStorageURI);
|
|
||||||
|
|
||||||
Map<String, Object> variables = Map.of(
|
|
||||||
"flow", Map.of(
|
|
||||||
"id", "subflow",
|
|
||||||
"namespace", NAMESPACE,
|
|
||||||
"tenantId", MAIN_TENANT),
|
|
||||||
"execution", Map.of("id", IdUtils.create()),
|
|
||||||
"trigger", Map.of(
|
|
||||||
"flowId", FLOW,
|
|
||||||
"executionId", executionId,
|
|
||||||
"tenantId", MAIN_TENANT
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
Exception ex = assertThrows(
|
|
||||||
IllegalArgumentException.class,
|
|
||||||
() -> variableRenderer.render("{{ fileSize('" + internalStorageFile + "') }}", variables)
|
|
||||||
);
|
|
||||||
|
|
||||||
assertTrue(ex.getMessage().startsWith("Unable to read the file"), "Exception message doesn't match expected one");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void returnsCorrectSize_givenUri_andCurrentExecution() throws IOException, IllegalVariableEvaluationException {
|
void returnsCorrectSize_givenUri_andCurrentExecution() throws IOException, IllegalVariableEvaluationException {
|
||||||
String executionId = IdUtils.create();
|
String executionId = IdUtils.create();
|
||||||
|
|||||||
@@ -259,6 +259,27 @@ class ReadFileFunctionTest {
|
|||||||
assertThat(variableRenderer.render("{{ read(nsfile) }}", variables)).isEqualTo("Hello World");
|
assertThat(variableRenderer.render("{{ read(nsfile) }}", variables)).isEqualTo("Hello World");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReadChildFileEvenIfTrigger() throws IOException, IllegalVariableEvaluationException {
|
||||||
|
String namespace = "my.namespace";
|
||||||
|
String flowId = "flow";
|
||||||
|
String executionId = IdUtils.create();
|
||||||
|
URI internalStorageURI = URI.create("/" + namespace.replace(".", "/") + "/" + flowId + "/executions/" + executionId + "/tasks/task/" + IdUtils.create() + "/123456.ion");
|
||||||
|
URI internalStorageFile = storageInterface.put(MAIN_TENANT, namespace, internalStorageURI, new ByteArrayInputStream("Hello from a task output".getBytes()));
|
||||||
|
|
||||||
|
Map<String, Object> variables = Map.of(
|
||||||
|
"flow", Map.of(
|
||||||
|
"id", "flow",
|
||||||
|
"namespace", "notme",
|
||||||
|
"tenantId", MAIN_TENANT),
|
||||||
|
"execution", Map.of("id", "notme"),
|
||||||
|
"trigger", Map.of("namespace", "notme", "flowId", "parent", "executionId", "parent")
|
||||||
|
);
|
||||||
|
|
||||||
|
String render = variableRenderer.render("{{ read('" + internalStorageFile + "') }}", variables);
|
||||||
|
assertThat(render).isEqualTo("Hello from a task output");
|
||||||
|
}
|
||||||
|
|
||||||
private URI createFile() throws IOException {
|
private URI createFile() throws IOException {
|
||||||
File tempFile = File.createTempFile("file", ".txt");
|
File tempFile = File.createTempFile("file", ".txt");
|
||||||
Files.write(tempFile.toPath(), "Hello World".getBytes());
|
Files.write(tempFile.toPath(), "Hello World".getBytes());
|
||||||
|
|||||||
@@ -12,20 +12,24 @@ import io.kestra.core.queues.QueueInterface;
|
|||||||
import io.kestra.core.repositories.FlowRepositoryInterface;
|
import io.kestra.core.repositories.FlowRepositoryInterface;
|
||||||
import io.kestra.core.runners.ConcurrencyLimit;
|
import io.kestra.core.runners.ConcurrencyLimit;
|
||||||
import io.kestra.core.runners.RunnerUtils;
|
import io.kestra.core.runners.RunnerUtils;
|
||||||
|
import io.kestra.core.utils.TestsUtils;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
import jakarta.inject.Named;
|
import jakarta.inject.Named;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
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 reactor.core.publisher.Flux;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
|
|
||||||
import static io.kestra.core.utils.Rethrow.throwRunnable;
|
import static io.kestra.core.utils.Rethrow.throwRunnable;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
@KestraTest(startRunner = true)
|
@KestraTest(startRunner = true)
|
||||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
@@ -54,14 +58,29 @@ class ConcurrencyLimitServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
@LoadFlows("flows/valids/flow-concurrency-queue.yml")
|
@LoadFlows("flows/valids/flow-concurrency-queue.yml")
|
||||||
void unqueueExecution() throws QueueException, TimeoutException {
|
void unqueueExecution() throws QueueException, TimeoutException, InterruptedException {
|
||||||
// run a first flow so the second is queued
|
// run a first flow so the second is queued
|
||||||
runnerUtils.runOneUntilRunning(TENANT_ID, TESTS_FLOW_NS, "flow-concurrency-queue");
|
Execution first = runnerUtils.runOneUntilRunning(TENANT_ID, TESTS_FLOW_NS, "flow-concurrency-queue");
|
||||||
Execution result = runUntilQueued(TESTS_FLOW_NS, "flow-concurrency-queue");
|
Execution result = runUntilQueued(TESTS_FLOW_NS, "flow-concurrency-queue");
|
||||||
assertThat(result.getState().isQueued()).isTrue();
|
assertThat(result.getState().isQueued()).isTrue();
|
||||||
|
|
||||||
|
// await for the execution to be terminated
|
||||||
|
CountDownLatch terminated = new CountDownLatch(2);
|
||||||
|
Flux<Execution> receive = TestsUtils.receive(executionQueue, (either) -> {
|
||||||
|
if (either.getLeft().getId().equals(first.getId()) && either.getLeft().getState().isTerminated()) {
|
||||||
|
terminated.countDown();
|
||||||
|
}
|
||||||
|
if (either.getLeft().getId().equals(result.getId()) && either.getLeft().getState().isTerminated()) {
|
||||||
|
terminated.countDown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
Execution unqueued = concurrencyLimitService.unqueue(result, State.Type.RUNNING);
|
Execution unqueued = concurrencyLimitService.unqueue(result, State.Type.RUNNING);
|
||||||
assertThat(unqueued.getState().isRunning()).isTrue();
|
assertThat(unqueued.getState().isRunning()).isTrue();
|
||||||
|
executionQueue.emit(unqueued);
|
||||||
|
|
||||||
|
assertTrue(terminated.await(10, TimeUnit.SECONDS));
|
||||||
|
receive.blockLast();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -73,7 +92,6 @@ class ConcurrencyLimitServiceTest {
|
|||||||
assertThat(limit.get().getTenantId()).isEqualTo(execution.getTenantId());
|
assertThat(limit.get().getTenantId()).isEqualTo(execution.getTenantId());
|
||||||
assertThat(limit.get().getNamespace()).isEqualTo(execution.getNamespace());
|
assertThat(limit.get().getNamespace()).isEqualTo(execution.getNamespace());
|
||||||
assertThat(limit.get().getFlowId()).isEqualTo(execution.getFlowId());
|
assertThat(limit.get().getFlowId()).isEqualTo(execution.getFlowId());
|
||||||
assertThat(limit.get().getRunning()).isEqualTo(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -156,6 +156,26 @@ class DownloadTest {
|
|||||||
assertThat(output.getUri().toString()).endsWith("filename.jpg");
|
assertThat(output.getUri().toString()).endsWith("filename.jpg");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void fileNameShouldOverrideContentDisposition() throws Exception {
|
||||||
|
EmbeddedServer embeddedServer = applicationContext.getBean(EmbeddedServer.class);
|
||||||
|
embeddedServer.start();
|
||||||
|
|
||||||
|
Download task = Download.builder()
|
||||||
|
.id(DownloadTest.class.getSimpleName())
|
||||||
|
.type(DownloadTest.class.getName())
|
||||||
|
.uri(Property.ofValue(embeddedServer.getURI() + "/content-disposition"))
|
||||||
|
.saveAs(Property.ofValue("hardcoded-filename.jpg"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
RunContext runContext = TestsUtils.mockRunContext(this.runContextFactory, task, ImmutableMap.of());
|
||||||
|
|
||||||
|
Download.Output output = task.run(runContext);
|
||||||
|
|
||||||
|
assertThat(output.getUri().toString()).endsWith("hardcoded-filename.jpg");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void contentDispositionWithPath() throws Exception {
|
void contentDispositionWithPath() throws Exception {
|
||||||
EmbeddedServer embeddedServer = applicationContext.getBean(EmbeddedServer.class);
|
EmbeddedServer embeddedServer = applicationContext.getBean(EmbeddedServer.class);
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
id: flow-trigger-mixed-conditions-flow-a
|
||||||
|
namespace: io.kestra.tests.trigger.mixed.conditions
|
||||||
|
|
||||||
|
labels:
|
||||||
|
some: label
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- id: only
|
||||||
|
type: io.kestra.plugin.core.debug.Return
|
||||||
|
format: "from parents: {{execution.id}}"
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
id: flow-trigger-mixed-conditions-flow-listen
|
||||||
|
namespace: io.kestra.tests.trigger.mixed.conditions
|
||||||
|
|
||||||
|
triggers:
|
||||||
|
- id: on_completion
|
||||||
|
type: io.kestra.plugin.core.trigger.Flow
|
||||||
|
states: [ SUCCESS ]
|
||||||
|
conditions:
|
||||||
|
- type: io.kestra.plugin.core.condition.ExecutionFlow
|
||||||
|
namespace: io.kestra.tests.trigger.mixed.conditions
|
||||||
|
flowId: flow-trigger-mixed-conditions-flow-a
|
||||||
|
- id: on_failure
|
||||||
|
type: io.kestra.plugin.core.trigger.Flow
|
||||||
|
states: [ FAILED ]
|
||||||
|
preconditions:
|
||||||
|
id: flowsFailure
|
||||||
|
flows:
|
||||||
|
- namespace: io.kestra.tests.trigger.multiple.conditions
|
||||||
|
flowId: flow-trigger-multiple-conditions-flow-a
|
||||||
|
states: [FAILED]
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- id: only
|
||||||
|
type: io.kestra.plugin.core.debug.Return
|
||||||
|
format: "It works"
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
id: flow-trigger-multiple-conditions-flow-a
|
||||||
|
namespace: io.kestra.tests.trigger.multiple.conditions
|
||||||
|
|
||||||
|
labels:
|
||||||
|
some: label
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- id: only
|
||||||
|
type: io.kestra.plugin.core.debug.Return
|
||||||
|
format: "from parents: {{execution.id}}"
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
id: flow-trigger-multiple-conditions-flow-listen
|
||||||
|
namespace: io.kestra.tests.trigger.multiple.conditions
|
||||||
|
|
||||||
|
triggers:
|
||||||
|
- id: on_completion
|
||||||
|
type: io.kestra.plugin.core.trigger.Flow
|
||||||
|
states: [ SUCCESS ]
|
||||||
|
conditions:
|
||||||
|
- type: io.kestra.plugin.core.condition.ExecutionFlow
|
||||||
|
namespace: io.kestra.tests.trigger.multiple.conditions
|
||||||
|
flowId: flow-trigger-multiple-conditions-flow-a
|
||||||
|
- id: on_failure
|
||||||
|
type: io.kestra.plugin.core.trigger.Flow
|
||||||
|
states: [ FAILED ]
|
||||||
|
conditions:
|
||||||
|
- type: io.kestra.plugin.core.condition.ExecutionFlow
|
||||||
|
namespace: io.kestra.tests.trigger.multiple.conditions
|
||||||
|
flowId: flow-trigger-multiple-conditions-flow-a
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- id: only
|
||||||
|
type: io.kestra.plugin.core.debug.Return
|
||||||
|
format: "It works"
|
||||||
@@ -50,16 +50,147 @@ public class FlowTriggerService {
|
|||||||
.map(io.kestra.plugin.core.trigger.Flow.class::cast);
|
.map(io.kestra.plugin.core.trigger.Flow.class::cast);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Execution> computeExecutionsFromFlowTriggers(Execution execution, List<? extends Flow> allFlows, Optional<MultipleConditionStorageInterface> multipleConditionStorage) {
|
/**
|
||||||
List<FlowWithFlowTrigger> validTriggersBeforeMultipleConditionEval = allFlows.stream()
|
* This method computes executions to trigger from flow triggers from a given execution.
|
||||||
|
* It only computes those depending on standard (non-multiple / non-preconditions) conditions, so it must be used
|
||||||
|
* in conjunction with {@link #computeExecutionsFromFlowTriggerPreconditions(Execution, Flow, MultipleConditionStorageInterface)}.
|
||||||
|
*/
|
||||||
|
public List<Execution> computeExecutionsFromFlowTriggerConditions(Execution execution, Flow flow) {
|
||||||
|
List<FlowWithFlowTrigger> flowWithFlowTriggers = computeFlowTriggers(execution, flow)
|
||||||
|
.stream()
|
||||||
|
// we must filter on no multiple conditions and no preconditions to avoid evaluating two times triggers that have standard conditions and multiple conditions
|
||||||
|
.filter(it -> it.getTrigger().getPreconditions() == null && ListUtils.emptyOnNull(it.getTrigger().getConditions()).stream().noneMatch(MultipleCondition.class::isInstance))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
// short-circuit empty triggers to evaluate
|
||||||
|
if (flowWithFlowTriggers.isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// compute all executions to create from flow triggers without taken into account multiple conditions
|
||||||
|
return flowWithFlowTriggers.stream()
|
||||||
|
.map(f -> f.getTrigger().evaluate(
|
||||||
|
Optional.empty(),
|
||||||
|
runContextFactory.of(f.getFlow(), execution),
|
||||||
|
f.getFlow(),
|
||||||
|
execution
|
||||||
|
))
|
||||||
|
.filter(Optional::isPresent)
|
||||||
|
.map(Optional::get)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method computes executions to trigger from flow triggers from a given execution.
|
||||||
|
* It only computes those depending on multiple conditions and preconditions, so it must be used
|
||||||
|
* in conjunction with {@link #computeExecutionsFromFlowTriggerConditions(Execution, Flow)}.
|
||||||
|
*/
|
||||||
|
public List<Execution> computeExecutionsFromFlowTriggerPreconditions(Execution execution, Flow flow, MultipleConditionStorageInterface multipleConditionStorage) {
|
||||||
|
List<FlowWithFlowTrigger> flowWithFlowTriggers = computeFlowTriggers(execution, flow)
|
||||||
|
.stream()
|
||||||
|
// we must filter on multiple conditions or preconditions to avoid evaluating two times triggers that only have standard conditions
|
||||||
|
.filter(flowWithFlowTrigger -> flowWithFlowTrigger.getTrigger().getPreconditions() != null || ListUtils.emptyOnNull(flowWithFlowTrigger.getTrigger().getConditions()).stream().anyMatch(MultipleCondition.class::isInstance))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
// short-circuit empty triggers to evaluate
|
||||||
|
if (flowWithFlowTriggers.isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<FlowWithFlowTriggerAndMultipleCondition> flowWithMultipleConditionsToEvaluate = flowWithFlowTriggers.stream()
|
||||||
|
.flatMap(flowWithFlowTrigger -> flowTriggerMultipleConditions(flowWithFlowTrigger)
|
||||||
|
.map(multipleCondition -> new FlowWithFlowTriggerAndMultipleCondition(
|
||||||
|
flowWithFlowTrigger.getFlow(),
|
||||||
|
multipleConditionStorage.getOrCreate(flowWithFlowTrigger.getFlow(), multipleCondition, execution.getOutputs()),
|
||||||
|
flowWithFlowTrigger.getTrigger(),
|
||||||
|
multipleCondition
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
// avoid evaluating expired windows (for ex for daily time window or deadline)
|
||||||
|
.filter(flowWithFlowTriggerAndMultipleCondition -> flowWithFlowTriggerAndMultipleCondition.getMultipleConditionWindow().isValid(ZonedDateTime.now()))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
// evaluate multiple conditions
|
||||||
|
Map<FlowWithFlowTriggerAndMultipleCondition, MultipleConditionWindow> multipleConditionWindowsByFlow = flowWithMultipleConditionsToEvaluate.stream().map(f -> {
|
||||||
|
Map<String, Boolean> results = f.getMultipleCondition()
|
||||||
|
.getConditions()
|
||||||
|
.entrySet()
|
||||||
|
.stream()
|
||||||
|
.map(e -> new AbstractMap.SimpleEntry<>(
|
||||||
|
e.getKey(),
|
||||||
|
conditionService.isValid(e.getValue(), f.getFlow(), execution)
|
||||||
|
))
|
||||||
|
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||||
|
|
||||||
|
return Map.entry(f, f.getMultipleConditionWindow().with(results));
|
||||||
|
})
|
||||||
|
.filter(e -> !e.getValue().getResults().isEmpty())
|
||||||
|
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||||
|
|
||||||
|
// persist results
|
||||||
|
multipleConditionStorage.save(new ArrayList<>(multipleConditionWindowsByFlow.values()));
|
||||||
|
|
||||||
|
// compute all executions to create from flow triggers now that multiple conditions storage is populated
|
||||||
|
List<Execution> executions = flowWithFlowTriggers.stream()
|
||||||
|
// will evaluate conditions
|
||||||
|
.filter(flowWithFlowTrigger ->
|
||||||
|
conditionService.isValid(
|
||||||
|
flowWithFlowTrigger.getTrigger(),
|
||||||
|
flowWithFlowTrigger.getFlow(),
|
||||||
|
execution,
|
||||||
|
multipleConditionStorage
|
||||||
|
)
|
||||||
|
)
|
||||||
|
// will evaluate preconditions
|
||||||
|
.filter(flowWithFlowTrigger ->
|
||||||
|
conditionService.isValid(
|
||||||
|
flowWithFlowTrigger.getTrigger().getPreconditions(),
|
||||||
|
flowWithFlowTrigger.getFlow(),
|
||||||
|
execution,
|
||||||
|
multipleConditionStorage
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.map(f -> f.getTrigger().evaluate(
|
||||||
|
Optional.of(multipleConditionStorage),
|
||||||
|
runContextFactory.of(f.getFlow(), execution),
|
||||||
|
f.getFlow(),
|
||||||
|
execution
|
||||||
|
))
|
||||||
|
.filter(Optional::isPresent)
|
||||||
|
.map(Optional::get)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
// purge fulfilled or expired multiple condition windows
|
||||||
|
Stream.concat(
|
||||||
|
multipleConditionWindowsByFlow.entrySet().stream()
|
||||||
|
.map(e -> Map.entry(
|
||||||
|
e.getKey().getMultipleCondition(),
|
||||||
|
e.getValue()
|
||||||
|
))
|
||||||
|
.filter(e -> !Boolean.FALSE.equals(e.getKey().getResetOnSuccess()) &&
|
||||||
|
e.getKey().getConditions().size() == Optional.ofNullable(e.getValue().getResults()).map(Map::size).orElse(0)
|
||||||
|
)
|
||||||
|
.map(Map.Entry::getValue),
|
||||||
|
multipleConditionStorage.expired(execution.getTenantId()).stream()
|
||||||
|
).forEach(multipleConditionStorage::delete);
|
||||||
|
|
||||||
|
return executions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<FlowWithFlowTrigger> computeFlowTriggers(Execution execution, Flow flow) {
|
||||||
|
if (
|
||||||
// prevent recursive flow triggers
|
// prevent recursive flow triggers
|
||||||
.filter(flow -> flowService.removeUnwanted(flow, execution))
|
!flowService.removeUnwanted(flow, execution) ||
|
||||||
// filter out Test Executions
|
// filter out Test Executions
|
||||||
.filter(flow -> execution.getKind() == null)
|
execution.getKind() != null ||
|
||||||
// ensure flow & triggers are enabled
|
// ensure flow & triggers are enabled
|
||||||
.filter(flow -> !flow.isDisabled() && !(flow instanceof FlowWithException))
|
flow.isDisabled() || flow instanceof FlowWithException ||
|
||||||
.filter(flow -> flow.getTriggers() != null && !flow.getTriggers().isEmpty())
|
flow.getTriggers() == null || flow.getTriggers().isEmpty()) {
|
||||||
.flatMap(flow -> flowTriggers(flow).map(trigger -> new FlowWithFlowTrigger(flow, trigger)))
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return flowTriggers(flow).map(trigger -> new FlowWithFlowTrigger(flow, trigger))
|
||||||
// filter on the execution state the flow listen to
|
// filter on the execution state the flow listen to
|
||||||
.filter(flowWithFlowTrigger -> flowWithFlowTrigger.getTrigger().getStates().contains(execution.getState().getCurrent()))
|
.filter(flowWithFlowTrigger -> flowWithFlowTrigger.getTrigger().getStates().contains(execution.getState().getCurrent()))
|
||||||
// validate flow triggers conditions excluding multiple conditions
|
// validate flow triggers conditions excluding multiple conditions
|
||||||
@@ -74,96 +205,6 @@ public class FlowTriggerService {
|
|||||||
execution
|
execution
|
||||||
)
|
)
|
||||||
)).toList();
|
)).toList();
|
||||||
|
|
||||||
// short-circuit empty triggers to evaluate
|
|
||||||
if (validTriggersBeforeMultipleConditionEval.isEmpty()) {
|
|
||||||
return Collections.emptyList();
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<FlowWithFlowTriggerAndMultipleCondition, MultipleConditionWindow> multipleConditionWindowsByFlow = null;
|
|
||||||
if (multipleConditionStorage.isPresent()) {
|
|
||||||
List<FlowWithFlowTriggerAndMultipleCondition> flowWithMultipleConditionsToEvaluate = validTriggersBeforeMultipleConditionEval.stream()
|
|
||||||
.flatMap(flowWithFlowTrigger -> flowTriggerMultipleConditions(flowWithFlowTrigger)
|
|
||||||
.map(multipleCondition -> new FlowWithFlowTriggerAndMultipleCondition(
|
|
||||||
flowWithFlowTrigger.getFlow(),
|
|
||||||
multipleConditionStorage.get().getOrCreate(flowWithFlowTrigger.getFlow(), multipleCondition, execution.getOutputs()),
|
|
||||||
flowWithFlowTrigger.getTrigger(),
|
|
||||||
multipleCondition
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
// avoid evaluating expired windows (for ex for daily time window or deadline)
|
|
||||||
.filter(flowWithFlowTriggerAndMultipleCondition -> flowWithFlowTriggerAndMultipleCondition.getMultipleConditionWindow().isValid(ZonedDateTime.now()))
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
// evaluate multiple conditions
|
|
||||||
multipleConditionWindowsByFlow = flowWithMultipleConditionsToEvaluate.stream().map(f -> {
|
|
||||||
Map<String, Boolean> results = f.getMultipleCondition()
|
|
||||||
.getConditions()
|
|
||||||
.entrySet()
|
|
||||||
.stream()
|
|
||||||
.map(e -> new AbstractMap.SimpleEntry<>(
|
|
||||||
e.getKey(),
|
|
||||||
conditionService.isValid(e.getValue(), f.getFlow(), execution)
|
|
||||||
))
|
|
||||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
|
||||||
|
|
||||||
return Map.entry(f, f.getMultipleConditionWindow().with(results));
|
|
||||||
})
|
|
||||||
.filter(e -> !e.getValue().getResults().isEmpty())
|
|
||||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
|
||||||
|
|
||||||
// persist results
|
|
||||||
multipleConditionStorage.get().save(new ArrayList<>(multipleConditionWindowsByFlow.values()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// compute all executions to create from flow triggers now that multiple conditions storage is populated
|
|
||||||
List<Execution> executions = validTriggersBeforeMultipleConditionEval.stream()
|
|
||||||
// will evaluate conditions
|
|
||||||
.filter(flowWithFlowTrigger ->
|
|
||||||
conditionService.isValid(
|
|
||||||
flowWithFlowTrigger.getTrigger(),
|
|
||||||
flowWithFlowTrigger.getFlow(),
|
|
||||||
execution,
|
|
||||||
multipleConditionStorage.orElse(null)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
// will evaluate preconditions
|
|
||||||
.filter(flowWithFlowTrigger ->
|
|
||||||
conditionService.isValid(
|
|
||||||
flowWithFlowTrigger.getTrigger().getPreconditions(),
|
|
||||||
flowWithFlowTrigger.getFlow(),
|
|
||||||
execution,
|
|
||||||
multipleConditionStorage.orElse(null)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.map(f -> f.getTrigger().evaluate(
|
|
||||||
multipleConditionStorage,
|
|
||||||
runContextFactory.of(f.getFlow(), execution),
|
|
||||||
f.getFlow(),
|
|
||||||
execution
|
|
||||||
))
|
|
||||||
.filter(Optional::isPresent)
|
|
||||||
.map(Optional::get)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
if (multipleConditionStorage.isPresent()) {
|
|
||||||
// purge fulfilled or expired multiple condition windows
|
|
||||||
Stream.concat(
|
|
||||||
multipleConditionWindowsByFlow.entrySet().stream()
|
|
||||||
.map(e -> Map.entry(
|
|
||||||
e.getKey().getMultipleCondition(),
|
|
||||||
e.getValue()
|
|
||||||
))
|
|
||||||
.filter(e -> !Boolean.FALSE.equals(e.getKey().getResetOnSuccess()) &&
|
|
||||||
e.getKey().getConditions().size() == Optional.ofNullable(e.getValue().getResults()).map(Map::size).orElse(0)
|
|
||||||
)
|
|
||||||
.map(Map.Entry::getValue),
|
|
||||||
multipleConditionStorage.get().expired(execution.getTenantId()).stream()
|
|
||||||
).forEach(multipleConditionStorage.get()::delete);
|
|
||||||
}
|
|
||||||
|
|
||||||
return executions;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Stream<MultipleCondition> flowTriggerMultipleConditions(FlowWithFlowTrigger flowWithFlowTrigger) {
|
private Stream<MultipleCondition> flowTriggerMultipleConditions(FlowWithFlowTrigger flowWithFlowTrigger) {
|
||||||
|
|||||||
@@ -25,8 +25,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||||||
|
|
||||||
@KestraTest
|
@KestraTest
|
||||||
class FlowTriggerServiceTest {
|
class FlowTriggerServiceTest {
|
||||||
public static final List<Label> EMPTY_LABELS = List.of();
|
private static final List<Label> EMPTY_LABELS = List.of();
|
||||||
public static final Optional<MultipleConditionStorageInterface> EMPTY_MULTIPLE_CONDITION_STORAGE = Optional.empty();
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
private TestRunContextFactory runContextFactory;
|
private TestRunContextFactory runContextFactory;
|
||||||
@@ -56,14 +55,27 @@ class FlowTriggerServiceTest {
|
|||||||
|
|
||||||
var simpleFlowExecution = Execution.newExecution(simpleFlow, EMPTY_LABELS).withState(State.Type.SUCCESS);
|
var simpleFlowExecution = Execution.newExecution(simpleFlow, EMPTY_LABELS).withState(State.Type.SUCCESS);
|
||||||
|
|
||||||
var resultingExecutionsToRun = flowTriggerService.computeExecutionsFromFlowTriggers(
|
var resultingExecutionsToRun = flowTriggerService.computeExecutionsFromFlowTriggerConditions(
|
||||||
simpleFlowExecution,
|
simpleFlowExecution,
|
||||||
List.of(simpleFlow, flowWithFlowTrigger),
|
flowWithFlowTrigger
|
||||||
EMPTY_MULTIPLE_CONDITION_STORAGE
|
|
||||||
);
|
);
|
||||||
|
|
||||||
assertThat(resultingExecutionsToRun).size().isEqualTo(1);
|
assertThat(resultingExecutionsToRun).size().isEqualTo(1);
|
||||||
assertThat(resultingExecutionsToRun.get(0).getFlowId()).isEqualTo(flowWithFlowTrigger.getId());
|
assertThat(resultingExecutionsToRun.getFirst().getFlowId()).isEqualTo(flowWithFlowTrigger.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void computeExecutionsFromFlowTriggers_none() {
|
||||||
|
var simpleFlow = aSimpleFlow();
|
||||||
|
|
||||||
|
var simpleFlowExecution = Execution.newExecution(simpleFlow, EMPTY_LABELS).withState(State.Type.SUCCESS);
|
||||||
|
|
||||||
|
var resultingExecutionsToRun = flowTriggerService.computeExecutionsFromFlowTriggerConditions(
|
||||||
|
simpleFlowExecution,
|
||||||
|
simpleFlow
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThat(resultingExecutionsToRun).isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -81,10 +93,9 @@ class FlowTriggerServiceTest {
|
|||||||
|
|
||||||
var simpleFlowExecution = Execution.newExecution(simpleFlow, EMPTY_LABELS).withState(State.Type.CREATED);
|
var simpleFlowExecution = Execution.newExecution(simpleFlow, EMPTY_LABELS).withState(State.Type.CREATED);
|
||||||
|
|
||||||
var resultingExecutionsToRun = flowTriggerService.computeExecutionsFromFlowTriggers(
|
var resultingExecutionsToRun = flowTriggerService.computeExecutionsFromFlowTriggerConditions(
|
||||||
simpleFlowExecution,
|
simpleFlowExecution,
|
||||||
List.of(simpleFlow, flowWithFlowTrigger),
|
flowWithFlowTrigger
|
||||||
EMPTY_MULTIPLE_CONDITION_STORAGE
|
|
||||||
);
|
);
|
||||||
|
|
||||||
assertThat(resultingExecutionsToRun).size().isEqualTo(0);
|
assertThat(resultingExecutionsToRun).size().isEqualTo(0);
|
||||||
@@ -109,10 +120,9 @@ class FlowTriggerServiceTest {
|
|||||||
.kind(ExecutionKind.TEST)
|
.kind(ExecutionKind.TEST)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
var resultingExecutionsToRun = flowTriggerService.computeExecutionsFromFlowTriggers(
|
var resultingExecutionsToRun = flowTriggerService.computeExecutionsFromFlowTriggerConditions(
|
||||||
simpleFlowExecutionComingFromATest,
|
simpleFlowExecutionComingFromATest,
|
||||||
List.of(simpleFlow, flowWithFlowTrigger),
|
flowWithFlowTrigger
|
||||||
EMPTY_MULTIPLE_CONDITION_STORAGE
|
|
||||||
);
|
);
|
||||||
|
|
||||||
assertThat(resultingExecutionsToRun).size().isEqualTo(0);
|
assertThat(resultingExecutionsToRun).size().isEqualTo(0);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
version=1.1.0-SNAPSHOT
|
version=1.1.5
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package io.kestra.repository.mysql;
|
package io.kestra.repository.mysql;
|
||||||
|
|
||||||
import io.kestra.core.models.triggers.Trigger;
|
import io.kestra.core.models.triggers.Trigger;
|
||||||
|
import io.kestra.core.runners.ScheduleContextInterface;
|
||||||
import io.kestra.core.utils.DateUtils;
|
import io.kestra.core.utils.DateUtils;
|
||||||
import io.kestra.jdbc.repository.AbstractJdbcTriggerRepository;
|
import io.kestra.jdbc.repository.AbstractJdbcTriggerRepository;
|
||||||
|
import io.kestra.jdbc.runner.JdbcSchedulerContext;
|
||||||
import io.kestra.jdbc.services.JdbcFilterService;
|
import io.kestra.jdbc.services.JdbcFilterService;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
import jakarta.inject.Named;
|
import jakarta.inject.Named;
|
||||||
@@ -11,6 +13,10 @@ import org.jooq.Condition;
|
|||||||
import org.jooq.Field;
|
import org.jooq.Field;
|
||||||
import org.jooq.impl.DSL;
|
import org.jooq.impl.DSL;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneOffset;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.time.temporal.Temporal;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -45,4 +51,11 @@ public class MysqlTriggerRepository extends AbstractJdbcTriggerRepository {
|
|||||||
throw new IllegalArgumentException("Unsupported GroupType: " + groupType);
|
throw new IllegalArgumentException("Unsupported GroupType: " + groupType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Temporal toNextExecutionTime(ZonedDateTime now) {
|
||||||
|
// next_execution_date in the table is stored in UTC
|
||||||
|
// convert 'now' to UTC LocalDateTime to avoid any timezone/offset interpretation by the database.
|
||||||
|
return now.withZoneSameInstant(ZoneOffset.UTC).toLocalDateTime();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import reactor.core.publisher.Flux;
|
|||||||
import reactor.core.publisher.FluxSink;
|
import reactor.core.publisher.FluxSink;
|
||||||
|
|
||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
|
import java.time.temporal.Temporal;
|
||||||
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;
|
||||||
@@ -151,7 +152,7 @@ public abstract class AbstractJdbcTriggerRepository extends AbstractJdbcReposito
|
|||||||
.select(field("value"))
|
.select(field("value"))
|
||||||
.from(this.jdbcRepository.getTable())
|
.from(this.jdbcRepository.getTable())
|
||||||
.where(
|
.where(
|
||||||
(field("next_execution_date").lessThan(now.toOffsetDateTime())
|
(field("next_execution_date").lessThan(toNextExecutionTime(now))
|
||||||
// we check for null for backwards compatibility
|
// we check for null for backwards compatibility
|
||||||
.or(field("next_execution_date").isNull()))
|
.or(field("next_execution_date").isNull()))
|
||||||
.and(field("execution_id").isNull())
|
.and(field("execution_id").isNull())
|
||||||
@@ -169,7 +170,7 @@ public abstract class AbstractJdbcTriggerRepository extends AbstractJdbcReposito
|
|||||||
.select(field("value"))
|
.select(field("value"))
|
||||||
.from(this.jdbcRepository.getTable())
|
.from(this.jdbcRepository.getTable())
|
||||||
.where(
|
.where(
|
||||||
(field("next_execution_date").lessThan(now.toOffsetDateTime())
|
(field("next_execution_date").lessThan(toNextExecutionTime(now))
|
||||||
// we check for null for backwards compatibility
|
// we check for null for backwards compatibility
|
||||||
.or(field("next_execution_date").isNull()))
|
.or(field("next_execution_date").isNull()))
|
||||||
.and(field("execution_id").isNotNull())
|
.and(field("execution_id").isNotNull())
|
||||||
@@ -179,6 +180,10 @@ public abstract class AbstractJdbcTriggerRepository extends AbstractJdbcReposito
|
|||||||
.map(r -> this.jdbcRepository.deserialize(r.get("value", String.class))));
|
.map(r -> this.jdbcRepository.deserialize(r.get("value", String.class))));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected Temporal toNextExecutionTime(ZonedDateTime now) {
|
||||||
|
return now.toOffsetDateTime();
|
||||||
|
}
|
||||||
|
|
||||||
public Trigger save(Trigger trigger, ScheduleContextInterface scheduleContextInterface) {
|
public Trigger save(Trigger trigger, ScheduleContextInterface scheduleContextInterface) {
|
||||||
JdbcSchedulerContext jdbcSchedulerContext = (JdbcSchedulerContext) scheduleContextInterface;
|
JdbcSchedulerContext jdbcSchedulerContext = (JdbcSchedulerContext) scheduleContextInterface;
|
||||||
|
|
||||||
|
|||||||
@@ -22,10 +22,10 @@ public class AbstractJdbcConcurrencyLimitStorage extends AbstractJdbcRepository
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch the concurrency limit counter then process the count using the consumer function.
|
* Fetch the concurrency limit counter, then process the count using the consumer function.
|
||||||
* It locked the raw and is wrapped in a transaction so the consumer should use the provided dslContext for any database access.
|
* It locked the raw and is wrapped in a transaction, so the consumer should use the provided dslContext for any database access.
|
||||||
* <p>
|
* <p>
|
||||||
* Note that to avoid a race when no concurrency limit counter exists, it first always try to insert a 0 counter.
|
* Note that to avoid a race when no concurrency limit counter exists, it first always tries to insert a 0 counter.
|
||||||
*/
|
*/
|
||||||
public ExecutionRunning countThenProcess(FlowInterface flow, BiFunction<DSLContext, ConcurrencyLimit, Pair<ExecutionRunning, ConcurrencyLimit>> consumer) {
|
public ExecutionRunning countThenProcess(FlowInterface flow, BiFunction<DSLContext, ConcurrencyLimit, Pair<ExecutionRunning, ConcurrencyLimit>> consumer) {
|
||||||
return this.jdbcRepository
|
return this.jdbcRepository
|
||||||
@@ -97,7 +97,7 @@ public class AbstractJdbcConcurrencyLimitStorage extends AbstractJdbcRepository
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns all concurrency limit from the database
|
* Returns all concurrency limits from the database
|
||||||
*/
|
*/
|
||||||
public List<ConcurrencyLimit> find(String tenantId) {
|
public List<ConcurrencyLimit> find(String tenantId) {
|
||||||
return this.jdbcRepository
|
return this.jdbcRepository
|
||||||
@@ -132,8 +132,7 @@ public class AbstractJdbcConcurrencyLimitStorage extends AbstractJdbcRepository
|
|||||||
.and(field("namespace").eq(flow.getNamespace()))
|
.and(field("namespace").eq(flow.getNamespace()))
|
||||||
.and(field("flow_id").eq(flow.getId()));
|
.and(field("flow_id").eq(flow.getId()));
|
||||||
|
|
||||||
return Optional.ofNullable(select.forUpdate().fetchOne())
|
return this.jdbcRepository.fetchOne(select.forUpdate());
|
||||||
.map(record -> this.jdbcRepository.map(record));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void update(DSLContext dslContext, ConcurrencyLimit concurrencyLimit) {
|
private void update(DSLContext dslContext, ConcurrencyLimit concurrencyLimit) {
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.function.BiConsumer;
|
import java.util.function.BiConsumer;
|
||||||
import java.util.function.Consumer;
|
|
||||||
|
|
||||||
public abstract class AbstractJdbcExecutionQueuedStorage extends AbstractJdbcRepository {
|
public abstract class AbstractJdbcExecutionQueuedStorage extends AbstractJdbcRepository {
|
||||||
protected io.kestra.jdbc.AbstractJdbcRepository<ExecutionQueued> jdbcRepository;
|
protected io.kestra.jdbc.AbstractJdbcRepository<ExecutionQueued> jdbcRepository;
|
||||||
@@ -70,18 +69,12 @@ public abstract class AbstractJdbcExecutionQueuedStorage extends AbstractJdbcRep
|
|||||||
this.jdbcRepository
|
this.jdbcRepository
|
||||||
.getDslContextWrapper()
|
.getDslContextWrapper()
|
||||||
.transaction(configuration -> {
|
.transaction(configuration -> {
|
||||||
var select = DSL
|
DSL
|
||||||
.using(configuration)
|
.using(configuration)
|
||||||
.select(AbstractJdbcRepository.field("value"))
|
.deleteFrom(this.jdbcRepository.getTable())
|
||||||
.from(this.jdbcRepository.getTable())
|
.where(buildTenantCondition(execution.getTenantId()))
|
||||||
.where(buildTenantCondition(execution.getTenantId()))
|
.and(field("key").eq(IdUtils.fromParts(execution.getTenantId(), execution.getNamespace(), execution.getFlowId(), execution.getId())))
|
||||||
.and(field("key").eq(IdUtils.fromParts(execution.getTenantId(), execution.getNamespace(), execution.getFlowId(), execution.getId())))
|
.execute();
|
||||||
.forUpdate();
|
|
||||||
|
|
||||||
Optional<ExecutionQueued> maybeExecution = this.jdbcRepository.fetchOne(select);
|
|
||||||
if (maybeExecution.isPresent()) {
|
|
||||||
this.jdbcRepository.delete(maybeExecution.get());
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -424,7 +424,7 @@ public class JdbcExecutor implements ExecutorInterface {
|
|||||||
|
|
||||||
MultipleConditionEvent multipleConditionEvent = either.getLeft();
|
MultipleConditionEvent multipleConditionEvent = either.getLeft();
|
||||||
|
|
||||||
flowTriggerService.computeExecutionsFromFlowTriggers(multipleConditionEvent.execution(), List.of(multipleConditionEvent.flow()), Optional.of(multipleConditionStorage))
|
flowTriggerService.computeExecutionsFromFlowTriggerPreconditions(multipleConditionEvent.execution(), multipleConditionEvent.flow(), multipleConditionStorage)
|
||||||
.forEach(exec -> {
|
.forEach(exec -> {
|
||||||
try {
|
try {
|
||||||
executionQueue.emit(exec);
|
executionQueue.emit(exec);
|
||||||
@@ -1230,8 +1230,10 @@ public class JdbcExecutor implements ExecutorInterface {
|
|||||||
private void processFlowTriggers(Execution execution) throws QueueException {
|
private void processFlowTriggers(Execution execution) throws QueueException {
|
||||||
// directly process simple conditions
|
// directly process simple conditions
|
||||||
flowTriggerService.withFlowTriggersOnly(allFlows.stream())
|
flowTriggerService.withFlowTriggersOnly(allFlows.stream())
|
||||||
.filter(f ->ListUtils.emptyOnNull(f.getTrigger().getConditions()).stream().noneMatch(c -> c instanceof MultipleCondition) && f.getTrigger().getPreconditions() == null)
|
.filter(f -> ListUtils.emptyOnNull(f.getTrigger().getConditions()).stream().noneMatch(c -> c instanceof MultipleCondition) && f.getTrigger().getPreconditions() == null)
|
||||||
.flatMap(f -> flowTriggerService.computeExecutionsFromFlowTriggers(execution, List.of(f.getFlow()), Optional.empty()).stream())
|
.map(f -> f.getFlow())
|
||||||
|
.distinct() // as computeExecutionsFromFlowTriggers is based on flow, we must map FlowWithFlowTrigger to a flow and distinct to avoid multiple execution for the same flow
|
||||||
|
.flatMap(f -> flowTriggerService.computeExecutionsFromFlowTriggerConditions(execution, f).stream())
|
||||||
.forEach(throwConsumer(exec -> executionQueue.emit(exec)));
|
.forEach(throwConsumer(exec -> executionQueue.emit(exec)));
|
||||||
|
|
||||||
// send multiple conditions to the multiple condition queue for later processing
|
// send multiple conditions to the multiple condition queue for later processing
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import io.kestra.core.models.flows.FlowWithSource;
|
|||||||
import io.kestra.core.models.triggers.Trigger;
|
import io.kestra.core.models.triggers.Trigger;
|
||||||
import io.kestra.core.repositories.TriggerRepositoryInterface;
|
import io.kestra.core.repositories.TriggerRepositoryInterface;
|
||||||
import io.kestra.core.runners.ScheduleContextInterface;
|
import io.kestra.core.runners.ScheduleContextInterface;
|
||||||
|
import io.kestra.core.runners.Scheduler;
|
||||||
import io.kestra.core.runners.SchedulerTriggerStateInterface;
|
import io.kestra.core.runners.SchedulerTriggerStateInterface;
|
||||||
import io.kestra.core.services.FlowListenersInterface;
|
import io.kestra.core.services.FlowListenersInterface;
|
||||||
import io.kestra.core.services.FlowService;
|
import io.kestra.core.services.FlowService;
|
||||||
@@ -56,6 +57,9 @@ public class JdbcScheduler extends AbstractScheduler {
|
|||||||
.forEach(abstractTrigger -> triggerRepository.delete(Trigger.of(flow, abstractTrigger)));
|
.forEach(abstractTrigger -> triggerRepository.delete(Trigger.of(flow, abstractTrigger)));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// No-op consumption of the trigger queue, so the events are purged from the queue
|
||||||
|
this.triggerQueue.receive(Scheduler.class, trigger -> { });
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -115,6 +115,10 @@ public abstract class JdbcServiceLivenessCoordinatorTest {
|
|||||||
if (either.getLeft().getTaskRun().getState().getCurrent() == Type.RUNNING) {
|
if (either.getLeft().getTaskRun().getState().getCurrent() == Type.RUNNING) {
|
||||||
runningLatch.countDown();
|
runningLatch.countDown();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (either.getLeft().getTaskRun().getState().getCurrent() == Type.FAILED) {
|
||||||
|
fail("Worker task result should not be in FAILED state as it should be resubmitted");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
workerJobQueue.emit(workerTask(Duration.ofSeconds(5)));
|
workerJobQueue.emit(workerTask(Duration.ofSeconds(5)));
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
@Singleton
|
@Singleton
|
||||||
public class TestRunner implements Runnable, AutoCloseable {
|
public class TestRunner implements Runnable, AutoCloseable {
|
||||||
@Setter private int workerThread = Math.max(3, Runtime.getRuntime().availableProcessors());
|
@Setter private int workerThread = Math.max(3, Runtime.getRuntime().availableProcessors()) * 16;
|
||||||
@Setter private boolean schedulerEnabled = true;
|
@Setter private boolean schedulerEnabled = true;
|
||||||
@Setter private boolean workerEnabled = true;
|
@Setter private boolean workerEnabled = true;
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,26 @@
|
|||||||
document.getElementsByTagName("html")[0].classList.add(localStorage.getItem("theme"));
|
document.getElementsByTagName("html")[0].classList.add(localStorage.getItem("theme"));
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- Optional but recommended for faster connection -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
|
||||||
|
<!-- Load Google Fonts non-blocking -->
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Public+Sans:wght@300;400;700;800&family=Source+Code+Pro:wght@400;700;800&display=swap"
|
||||||
|
media="print"
|
||||||
|
onload="this.media='all'"
|
||||||
|
>
|
||||||
|
|
||||||
|
<!-- Fallback for when JavaScript is disabled -->
|
||||||
|
<noscript>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Public+Sans:wght@300;400;700;800&family=Source+Code+Pro:wght@400;700;800&display=swap"
|
||||||
|
>
|
||||||
|
</noscript>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>
|
<noscript>
|
||||||
|
|||||||
12
ui/package-lock.json
generated
12
ui/package-lock.json
generated
@@ -24,7 +24,6 @@
|
|||||||
"cronstrue": "^3.9.0",
|
"cronstrue": "^3.9.0",
|
||||||
"cytoscape": "^3.33.0",
|
"cytoscape": "^3.33.0",
|
||||||
"dagre": "^0.8.5",
|
"dagre": "^0.8.5",
|
||||||
"el-table-infinite-scroll": "^3.0.7",
|
|
||||||
"element-plus": "2.11.5",
|
"element-plus": "2.11.5",
|
||||||
"humanize-duration": "^3.33.1",
|
"humanize-duration": "^3.33.1",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
@@ -9941,17 +9940,6 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/el-table-infinite-scroll": {
|
|
||||||
"version": "3.0.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/el-table-infinite-scroll/-/el-table-infinite-scroll-3.0.7.tgz",
|
|
||||||
"integrity": "sha512-at7f8GjNzvkf16i5kCBb1MOq6wI65k+TuaSt5wgiOLAKvdTr36+wAvnOnPYVIPhEpGeM8mRgLZQr2b5YV0lQaw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"core-js": "^3.x",
|
|
||||||
"element-plus": "^2.x",
|
|
||||||
"vue": "^3.x"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.207",
|
"version": "1.5.207",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.207.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.207.tgz",
|
||||||
|
|||||||
@@ -38,7 +38,6 @@
|
|||||||
"cronstrue": "^3.9.0",
|
"cronstrue": "^3.9.0",
|
||||||
"cytoscape": "^3.33.0",
|
"cytoscape": "^3.33.0",
|
||||||
"dagre": "^0.8.5",
|
"dagre": "^0.8.5",
|
||||||
"el-table-infinite-scroll": "^3.0.7",
|
|
||||||
"element-plus": "2.11.5",
|
"element-plus": "2.11.5",
|
||||||
"humanize-duration": "^3.33.1",
|
"humanize-duration": "^3.33.1",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
|
|||||||
@@ -35,16 +35,18 @@
|
|||||||
<WeatherSunny v-else />
|
<WeatherSunny v-else />
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="panelWrapper" :class="{panelTabResizing: resizing}" :style="{width: activeTab?.length ? `${panelWidth}px` : 0}">
|
<div class="panelWrapper" ref="panelWrapper" :class="{panelTabResizing: resizing}" :style="{width: activeTab?.length ? `${panelWidth}px` : 0}">
|
||||||
<div :style="{overflow: 'hidden'}">
|
<div :style="{overflow: 'hidden'}">
|
||||||
<button v-if="activeTab.length" class="closeButton" @click="setActiveTab('')">
|
<button v-if="activeTab.length" class="closeButton" @click="setActiveTab('')">
|
||||||
<Close />
|
<Close />
|
||||||
</button>
|
</button>
|
||||||
<ContextDocs v-if="activeTab === 'docs'" />
|
<KeepAlive>
|
||||||
<ContextNews v-else-if="activeTab === 'news'" />
|
<ContextDocs v-if="activeTab === 'docs'" />
|
||||||
<template v-else>
|
<ContextNews v-else-if="activeTab === 'news'" />
|
||||||
{{ activeTab }}
|
<template v-else>
|
||||||
</template>
|
{{ activeTab }}
|
||||||
|
</template>
|
||||||
|
</KeepAlive>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -96,6 +98,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
const panelWidth = ref(640)
|
const panelWidth = ref(640)
|
||||||
|
const panelWrapper = ref<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
const {startResizing, resizing} = useResizablePanel(activeTab)
|
const {startResizing, resizing} = useResizablePanel(activeTab)
|
||||||
|
|
||||||
|
|||||||
@@ -4,14 +4,22 @@
|
|||||||
<slot name="back-button" />
|
<slot name="back-button" />
|
||||||
<h2>{{ title }}</h2>
|
<h2>{{ title }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content" ref="contentRef">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import {ref} from "vue";
|
||||||
|
|
||||||
defineProps<{title:string}>();
|
defineProps<{title:string}>();
|
||||||
|
|
||||||
|
const contentRef = ref<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
contentRef
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
@@ -197,7 +197,6 @@
|
|||||||
|
|
||||||
import {trackTabOpen, trackTabClose} from "../utils/tabTracking";
|
import {trackTabOpen, trackTabClose} from "../utils/tabTracking";
|
||||||
import {Panel, Tab, TabLive} from "../utils/multiPanelTypes";
|
import {Panel, Tab, TabLive} from "../utils/multiPanelTypes";
|
||||||
import {usePanelDefaultSize} from "../composables/usePanelDefaultSize";
|
|
||||||
|
|
||||||
const {t} = useI18n();
|
const {t} = useI18n();
|
||||||
const {showKeyShortcuts} = useKeyShortcuts();
|
const {showKeyShortcuts} = useKeyShortcuts();
|
||||||
@@ -449,7 +448,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultSize = usePanelDefaultSize(panels);
|
const defaultSize = computed(() => panels.value.length === 0 ? 1 : (panels.value.reduce((acc, panel) => acc + panel.size, 0) / panels.value.length));
|
||||||
|
|
||||||
function newPanelDrop(_e: DragEvent, direction: "left" | "right") {
|
function newPanelDrop(_e: DragEvent, direction: "left" | "right") {
|
||||||
if (!movedTabInfo.value) return;
|
if (!movedTabInfo.value) return;
|
||||||
|
|||||||
@@ -22,6 +22,8 @@
|
|||||||
columns: optionalColumns,
|
columns: optionalColumns,
|
||||||
storageKey: storageKey
|
storageKey: storageKey
|
||||||
}"
|
}"
|
||||||
|
:defaultScope="false"
|
||||||
|
:defaultTimeRange="false"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #table>
|
<template #table>
|
||||||
@@ -298,6 +300,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import _merge from "lodash/merge";
|
import _merge from "lodash/merge";
|
||||||
import {ref, computed, watch} from "vue";
|
import {ref, computed, watch} from "vue";
|
||||||
|
import moment from "moment";
|
||||||
import {useI18n} from "vue-i18n";
|
import {useI18n} from "vue-i18n";
|
||||||
import {useRoute} from "vue-router";
|
import {useRoute} from "vue-router";
|
||||||
import {ElMessage} from "element-plus";
|
import {ElMessage} from "element-plus";
|
||||||
@@ -333,11 +336,9 @@
|
|||||||
import TopNavBar from "../layout/TopNavBar.vue";
|
import TopNavBar from "../layout/TopNavBar.vue";
|
||||||
import BulkSelect from "../layout/BulkSelect.vue";
|
import BulkSelect from "../layout/BulkSelect.vue";
|
||||||
import LogsWrapper from "../logs/LogsWrapper.vue";
|
import LogsWrapper from "../logs/LogsWrapper.vue";
|
||||||
//@ts-expect-error No declaration file
|
|
||||||
import SelectTable from "../layout/SelectTable.vue";
|
import SelectTable from "../layout/SelectTable.vue";
|
||||||
import TriggerAvatar from "../flows/TriggerAvatar.vue";
|
import TriggerAvatar from "../flows/TriggerAvatar.vue";
|
||||||
import KSFilter from "../filter/components/KSFilter.vue";
|
import KSFilter from "../filter/components/KSFilter.vue";
|
||||||
import useRestoreUrl from "../../composables/useRestoreUrl";
|
|
||||||
import MarkdownTooltip from "../layout/MarkdownTooltip.vue";
|
import MarkdownTooltip from "../layout/MarkdownTooltip.vue";
|
||||||
import useRouteContext from "../../composables/useRouteContext";
|
import useRouteContext from "../../composables/useRouteContext";
|
||||||
|
|
||||||
@@ -436,8 +437,6 @@
|
|||||||
.filter(Boolean) as ColumnConfig[]
|
.filter(Boolean) as ColumnConfig[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const {saveRestoreUrl} = useRestoreUrl();
|
|
||||||
|
|
||||||
const loadData = (callback?: () => void) => {
|
const loadData = (callback?: () => void) => {
|
||||||
const query = loadQuery({
|
const query = loadQuery({
|
||||||
size: parseInt(String(route.query?.size ?? "25")),
|
size: parseInt(String(route.query?.size ?? "25")),
|
||||||
@@ -463,8 +462,7 @@
|
|||||||
|
|
||||||
const {ready, onSort, onPageChanged, queryWithFilter, load} = useDataTableActions({
|
const {ready, onSort, onPageChanged, queryWithFilter, load} = useDataTableActions({
|
||||||
dataTableRef: dataTable,
|
dataTableRef: dataTable,
|
||||||
loadData,
|
loadData
|
||||||
saveRestoreUrl
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -696,7 +694,16 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const loadQuery = (base: any) => {
|
const loadQuery = (base: any) => {
|
||||||
let queryFilter = queryWithFilter();
|
const queryFilter = queryWithFilter();
|
||||||
|
|
||||||
|
const timeRange = queryFilter["filters[timeRange][EQUALS]"];
|
||||||
|
if (timeRange) {
|
||||||
|
const end = new Date();
|
||||||
|
const start = new Date(end.getTime() - moment.duration(timeRange).asMilliseconds());
|
||||||
|
queryFilter["filters[startDate][GREATER_THAN_OR_EQUAL_TO]"] = start.toISOString();
|
||||||
|
queryFilter["filters[endDate][LESS_THAN_OR_EQUAL_TO]"] = end.toISOString();
|
||||||
|
delete queryFilter["filters[timeRange][EQUALS]"];
|
||||||
|
}
|
||||||
|
|
||||||
return _merge(base, queryFilter);
|
return _merge(base, queryFilter);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {computed, onBeforeMount, ref, useTemplateRef} from "vue";
|
import {computed, onBeforeMount, ref, useTemplateRef, watch} from "vue";
|
||||||
import {stringify, parse} from "@kestra-io/ui-libs/flow-yaml-utils";
|
import {stringify, parse} from "@kestra-io/ui-libs/flow-yaml-utils";
|
||||||
|
|
||||||
import type {Dashboard, Chart} from "./composables/useDashboards";
|
import type {Dashboard, Chart} from "./composables/useDashboards";
|
||||||
@@ -89,9 +89,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!props.isFlow && !props.isNamespace) {
|
if (!props.isFlow && !props.isNamespace) {
|
||||||
|
// Preserve timeRange filter when switching dashboards
|
||||||
|
const preservedQuery = Object.fromEntries(
|
||||||
|
Object.entries(route.query).filter(([key]) =>
|
||||||
|
key.includes("timeRange")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
router.replace({
|
router.replace({
|
||||||
params: {...route.params, dashboard: id},
|
params: {...route.params, dashboard: id},
|
||||||
query: route.params.dashboard !== id ? {} : {...route.query},
|
query: route.params.dashboard !== id ? preservedQuery : {...route.query},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,8 +109,22 @@
|
|||||||
onBeforeMount(() => {
|
onBeforeMount(() => {
|
||||||
const ID = getDashboard(route, "id");
|
const ID = getDashboard(route, "id");
|
||||||
|
|
||||||
if (props.isFlow && ID === "default") load("default", processFlowYaml(YAML_FLOW, route.params.namespace as string, route.params.id as string));
|
if (props.isFlow) {
|
||||||
else if (props.isNamespace && ID === "default") load("default", YAML_NAMESPACE);
|
load(ID, processFlowYaml(YAML_FLOW, route.params.namespace as string, route.params.id as string));
|
||||||
|
} else if (props.isNamespace) {
|
||||||
|
load(ID, YAML_NAMESPACE);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => getDashboard(route, "id"), (newId, oldId) => {
|
||||||
|
if (newId !== oldId) {
|
||||||
|
const defaultYAML = props.isFlow
|
||||||
|
? processFlowYaml(YAML_FLOW, route.params.namespace as string, route.params.id as string)
|
||||||
|
: props.isNamespace
|
||||||
|
? YAML_NAMESPACE
|
||||||
|
: YAML_MAIN;
|
||||||
|
load(newId, defaultYAML);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -28,33 +28,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadChart(chart: any) {
|
|
||||||
const yamlChart = YAML_UTILS.stringify(chart);
|
|
||||||
const result: { error: string | null; data: null | {
|
|
||||||
id?: string;
|
|
||||||
name?: string;
|
|
||||||
type?: string;
|
|
||||||
chartOptions?: Record<string, any>;
|
|
||||||
dataFilters?: any[];
|
|
||||||
charts?: any[];
|
|
||||||
}; raw: any } = {
|
|
||||||
error: null,
|
|
||||||
data: null,
|
|
||||||
raw: {}
|
|
||||||
};
|
|
||||||
const errors = await dashboardStore.validateChart(yamlChart);
|
|
||||||
if (errors.constraints) {
|
|
||||||
result.error = errors.constraints;
|
|
||||||
} else {
|
|
||||||
result.data = {...chart, content: yamlChart, raw: chart};
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateChartPreview(event: any) {
|
async function updateChartPreview(event: any) {
|
||||||
const chart = YAML_UTILS.getChartAtPosition(event.model.getValue(), event.position);
|
const chart = YAML_UTILS.getChartAtPosition(event.model.getValue(), event.position);
|
||||||
if (chart) {
|
if (chart) {
|
||||||
const result = await loadChart(chart);
|
const result = await dashboardStore.loadChart(chart);
|
||||||
dashboardStore.selectedChart = typeof result.data === "object"
|
dashboardStore.selectedChart = typeof result.data === "object"
|
||||||
? {
|
? {
|
||||||
...result.data,
|
...result.data,
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="button-top">
|
<div class="button-top">
|
||||||
<ValidationError class="mx-3" tooltipPlacement="bottom-start" :errors="errors" />
|
<ValidationError
|
||||||
|
class="mx-3"
|
||||||
|
tooltipPlacement="bottom-start"
|
||||||
|
:errors="dashboardStore.errors"
|
||||||
|
:warnings="dashboardStore.warnings"
|
||||||
|
/>
|
||||||
|
|
||||||
<el-button
|
<el-button
|
||||||
:icon="ContentSave"
|
:icon="ContentSave"
|
||||||
@@ -17,6 +22,7 @@
|
|||||||
import {useI18n} from "vue-i18n";
|
import {useI18n} from "vue-i18n";
|
||||||
import ContentSave from "vue-material-design-icons/ContentSave.vue";
|
import ContentSave from "vue-material-design-icons/ContentSave.vue";
|
||||||
import ValidationError from "../../flows/ValidationError.vue";
|
import ValidationError from "../../flows/ValidationError.vue";
|
||||||
|
import {useDashboardStore} from "../../../stores/dashboard";
|
||||||
|
|
||||||
const {t} = useI18n();
|
const {t} = useI18n();
|
||||||
|
|
||||||
@@ -24,15 +30,11 @@
|
|||||||
(e: "save"): void;
|
(e: "save"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const props = defineProps<{
|
const dashboardStore = useDashboardStore();
|
||||||
warnings?: string[];
|
|
||||||
errors?: string[];
|
|
||||||
disabled?: boolean;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const saveButtonType = computed(() => {
|
const saveButtonType = computed(() => {
|
||||||
if (props.errors) return "danger";
|
if (dashboardStore.errors) return "danger";
|
||||||
return props.warnings ? "warning" : "primary";
|
return dashboardStore.warnings ? "warning" : "primary";
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
FIELDNAME_INJECTION_KEY,
|
FIELDNAME_INJECTION_KEY,
|
||||||
FULL_SCHEMA_INJECTION_KEY,
|
FULL_SCHEMA_INJECTION_KEY,
|
||||||
FULL_SOURCE_INJECTION_KEY,
|
FULL_SOURCE_INJECTION_KEY,
|
||||||
|
ON_TASK_EDITOR_CLICK_INJECTION_KEY,
|
||||||
PARENT_PATH_INJECTION_KEY,
|
PARENT_PATH_INJECTION_KEY,
|
||||||
POSITION_INJECTION_KEY,
|
POSITION_INJECTION_KEY,
|
||||||
REF_PATH_INJECTION_KEY,
|
REF_PATH_INJECTION_KEY,
|
||||||
@@ -111,6 +112,15 @@
|
|||||||
provide(BLOCK_SCHEMA_PATH_INJECTION_KEY, computed(() => props.blockSchemaPath ?? dashboardStore.schema.$ref ?? ""));
|
provide(BLOCK_SCHEMA_PATH_INJECTION_KEY, computed(() => props.blockSchemaPath ?? dashboardStore.schema.$ref ?? ""));
|
||||||
provide(FULL_SOURCE_INJECTION_KEY, computed(() => dashboardStore.sourceCode ?? ""));
|
provide(FULL_SOURCE_INJECTION_KEY, computed(() => dashboardStore.sourceCode ?? ""));
|
||||||
provide(POSITION_INJECTION_KEY, props.position ?? "after");
|
provide(POSITION_INJECTION_KEY, props.position ?? "after");
|
||||||
|
provide(ON_TASK_EDITOR_CLICK_INJECTION_KEY, (elt) => {
|
||||||
|
const type = elt?.type;
|
||||||
|
dashboardStore.loadChart(elt);
|
||||||
|
if(type){
|
||||||
|
pluginsStore.updateDocumentation({type});
|
||||||
|
}else{
|
||||||
|
pluginsStore.updateDocumentation();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const pluginsStore = usePluginsStore();
|
const pluginsStore = usePluginsStore();
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-100 p-4">
|
<div class="w-100 p-4">
|
||||||
<Sections
|
<Sections
|
||||||
|
:key="dashboardStore.sourceCode"
|
||||||
:dashboard="{id: 'default', charts: []}"
|
:dashboard="{id: 'default', charts: []}"
|
||||||
:charts="charts.map(chart => chart.data).filter(chart => chart !== null)"
|
:charts="charts.map(chart => chart.data).filter(chart => chart !== null)"
|
||||||
showDefault
|
showDefault
|
||||||
@@ -9,11 +10,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {onMounted, ref} from "vue";
|
import {ref, watch} from "vue";
|
||||||
import Sections from "../sections/Sections.vue";
|
import Sections from "../sections/Sections.vue";
|
||||||
import {Chart} from "../composables/useDashboards";
|
import {Chart} from "../composables/useDashboards";
|
||||||
import {useDashboardStore} from "../../../stores/dashboard";
|
import {useDashboardStore} from "../../../stores/dashboard";
|
||||||
import * as YAML_UTILS from "@kestra-io/ui-libs/flow-yaml-utils";
|
import * as YAML_UTILS from "@kestra-io/ui-libs/flow-yaml-utils";
|
||||||
|
import throttle from "lodash/throttle";
|
||||||
|
|
||||||
interface Result {
|
interface Result {
|
||||||
error: string[] | null;
|
error: string[] | null;
|
||||||
@@ -23,21 +25,27 @@
|
|||||||
|
|
||||||
const charts = ref<Result[]>([])
|
const charts = ref<Result[]>([])
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
validateAndLoadAllCharts();
|
|
||||||
});
|
|
||||||
|
|
||||||
const dashboardStore = useDashboardStore();
|
const dashboardStore = useDashboardStore();
|
||||||
|
|
||||||
function validateAndLoadAllCharts() {
|
const validateAndLoadAllChartsThrottled = throttle(validateAndLoadAllCharts, 500);
|
||||||
charts.value = [];
|
|
||||||
|
async function validateAndLoadAllCharts() {
|
||||||
const allCharts = YAML_UTILS.getAllCharts(dashboardStore.sourceCode) ?? [];
|
const allCharts = YAML_UTILS.getAllCharts(dashboardStore.sourceCode) ?? [];
|
||||||
allCharts.forEach(async (chart: any) => {
|
charts.value = await Promise.all(allCharts.map(async (chart: any) => {
|
||||||
const loadedChart = await loadChart(chart);
|
return loadChart(chart);
|
||||||
charts.value.push(loadedChart);
|
}));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => dashboardStore.sourceCode,
|
||||||
|
() => {
|
||||||
|
validateAndLoadAllChartsThrottled();
|
||||||
|
}
|
||||||
|
, {immediate: true}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async function loadChart(chart: any) {
|
async function loadChart(chart: any) {
|
||||||
const yamlChart = YAML_UTILS.stringify(chart);
|
const yamlChart = YAML_UTILS.stringify(chart);
|
||||||
const result: Result = {
|
const result: Result = {
|
||||||
|
|||||||
@@ -96,14 +96,19 @@
|
|||||||
return [DEFAULT, ...dashboards.value].filter((d) => !search.value || d.title.toLowerCase().includes(search.value.toLowerCase()));
|
return [DEFAULT, ...dashboards.value].filter((d) => !search.value || d.title.toLowerCase().includes(search.value.toLowerCase()));
|
||||||
});
|
});
|
||||||
|
|
||||||
const ID = getDashboard(route, "id") as string;
|
const STORAGE_KEY = getDashboard(route, "key");
|
||||||
|
|
||||||
const selected = ref(null);
|
const selected = ref<string | null>(null);
|
||||||
const select = (dashboard: any) => {
|
const select = (dashboard: any) => {
|
||||||
selected.value = dashboard?.title;
|
selected.value = dashboard?.title;
|
||||||
|
|
||||||
if (dashboard?.id) localStorage.setItem(ID, dashboard.id)
|
if (STORAGE_KEY) {
|
||||||
else localStorage.removeItem(ID);
|
if (dashboard?.id) {
|
||||||
|
localStorage.setItem(STORAGE_KEY, dashboard.id);
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem(STORAGE_KEY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
emits("dashboard", dashboard.id);
|
emits("dashboard", dashboard.id);
|
||||||
};
|
};
|
||||||
@@ -121,7 +126,7 @@
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchLast = () => localStorage.getItem(ID);
|
const getStoredDashboard = () => STORAGE_KEY ? localStorage.getItem(STORAGE_KEY) : null;
|
||||||
const fetchDashboards = () => {
|
const fetchDashboards = () => {
|
||||||
dashboardStore
|
dashboardStore
|
||||||
.list({})
|
.list({})
|
||||||
@@ -129,13 +134,17 @@
|
|||||||
dashboards.value = response.results;
|
dashboards.value = response.results;
|
||||||
|
|
||||||
const creation = Boolean(route.query.created);
|
const creation = Boolean(route.query.created);
|
||||||
const lastSelected = creation ? (route.params?.dashboard ?? fetchLast()) : (fetchLast() ?? route.params?.dashboard);
|
const lastSelected = creation
|
||||||
|
? (route.params?.dashboard ?? getStoredDashboard())
|
||||||
|
: (getStoredDashboard() ?? route.params?.dashboard);
|
||||||
|
|
||||||
if (lastSelected) {
|
if (lastSelected) {
|
||||||
const dashboard = dashboards.value.find((d) => d.id === lastSelected);
|
const dashboard = dashboards.value.find((d) => d.id === lastSelected);
|
||||||
|
|
||||||
if (dashboard) select(dashboard);
|
if (dashboard) {
|
||||||
else {
|
selected.value = dashboard.title;
|
||||||
|
emits("dashboard", dashboard.id);
|
||||||
|
} else {
|
||||||
selected.value = null;
|
selected.value = null;
|
||||||
emits("dashboard", "default");
|
emits("dashboard", "default");
|
||||||
}
|
}
|
||||||
@@ -145,15 +154,19 @@
|
|||||||
|
|
||||||
onBeforeMount(() => fetchDashboards());
|
onBeforeMount(() => fetchDashboards());
|
||||||
|
|
||||||
const tenant = ref(route.params.tenant);
|
const tenant = ref();
|
||||||
watch(route, (r) => {
|
watch(() => route.params.tenant, (t) => {
|
||||||
if (tenant.value !== r.params.tenant) {
|
if (tenant.value !== t) {
|
||||||
fetchDashboards();
|
fetchDashboards();
|
||||||
tenant.value = r.params.tenant;
|
tenant.value = t;
|
||||||
}
|
}
|
||||||
},
|
}, {immediate: true});
|
||||||
{deep: true},
|
|
||||||
);
|
watch(() => route.params?.dashboard, (val) => {
|
||||||
|
if(route.name === "home" && STORAGE_KEY) {
|
||||||
|
localStorage.setItem(STORAGE_KEY, val as string);
|
||||||
|
}
|
||||||
|
}, {immediate: true});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@@ -161,14 +174,6 @@
|
|||||||
span{
|
span{
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(svg){
|
|
||||||
color: var(--ks-content-tertiary);
|
|
||||||
font-size: 1.10rem;
|
|
||||||
position: absolute;
|
|
||||||
bottom: -0.10rem;
|
|
||||||
right: 0.08rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.dropdown {
|
.dropdown {
|
||||||
width: 300px;
|
width: 300px;
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<section id="charts" :class="{padding}">
|
<div class="dashboard-sections-container">
|
||||||
<el-row :gutter="16">
|
<section id="charts" :class="{padding}">
|
||||||
<el-col
|
<div
|
||||||
v-for="chart in props.charts"
|
v-for="chart in props.charts"
|
||||||
:key="`chart__${chart.id}`"
|
:key="`chart__${chart.id}`"
|
||||||
:xs="24"
|
class="dashboard-block"
|
||||||
:sm="(chart.chartOptions?.width || 6) * 4"
|
:class="{
|
||||||
:md="(chart.chartOptions?.width || 6) * 2"
|
[`dash-width-${chart.chartOptions?.width || 6}`]: true
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<div class="d-flex flex-column">
|
<div class="d-flex flex-column">
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
@@ -64,9 +65,9 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-col>
|
</div>
|
||||||
</el-row>
|
</section>
|
||||||
</section>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -133,14 +134,28 @@
|
|||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@import "@kestra-io/ui-libs/src/scss/variables";
|
@import "@kestra-io/ui-libs/src/scss/variables";
|
||||||
|
|
||||||
|
.dashboard-sections-container{
|
||||||
|
container-type: inline-size;
|
||||||
|
}
|
||||||
|
|
||||||
|
$smallMobile: 375px;
|
||||||
|
$tablet: 768px;
|
||||||
|
|
||||||
section#charts {
|
section#charts {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
@container (min-width: #{$smallMobile}) {
|
||||||
|
grid-template-columns: repeat(6, 1fr);
|
||||||
|
}
|
||||||
|
@container (min-width: #{$tablet}) {
|
||||||
|
grid-template-columns: repeat(12, 1fr);
|
||||||
|
}
|
||||||
&.padding {
|
&.padding {
|
||||||
padding: 0 2rem 1rem;
|
padding: 0 2rem 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .el-row .el-col {
|
.dashboard-block {
|
||||||
margin-bottom: 1rem;
|
|
||||||
|
|
||||||
& > div {
|
& > div {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
@@ -159,5 +174,24 @@ section#charts {
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dash-width-3, .dash-width-6, .dash-width-9, .dash-width-12 {
|
||||||
|
grid-column: span 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
@container (min-width: #{$smallMobile}) {
|
||||||
|
.dash-width-6, .dash-width-9, .dash-width-12 {
|
||||||
|
grid-column: span 6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@container (min-width: #{$tablet}) {
|
||||||
|
.dash-width-9 {
|
||||||
|
grid-column: span 9;
|
||||||
|
}
|
||||||
|
.dash-width-12 {
|
||||||
|
grid-column: span 12;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<ContextInfoContent :title="routeInfo.title">
|
<ContextInfoContent :title="routeInfo.title" ref="contextInfoRef">
|
||||||
<template v-if="isOnline" #back-button>
|
<template v-if="isOnline" #back-button>
|
||||||
<button
|
<button
|
||||||
class="back-button"
|
class="back-button"
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
<OpenInNew class="blank" />
|
<OpenInNew class="blank" />
|
||||||
</router-link>
|
</router-link>
|
||||||
</template>
|
</template>
|
||||||
<div ref="docWrapper" class="docs-controls">
|
<div class="docs-controls">
|
||||||
<template v-if="isOnline">
|
<template v-if="isOnline">
|
||||||
<ContextDocsSearch />
|
<ContextDocsSearch />
|
||||||
<DocsMenu />
|
<DocsMenu />
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {ref, watch, computed, getCurrentInstance, onUnmounted, onMounted, nextTick} from "vue";
|
import {ref, watch, computed, getCurrentInstance, onUnmounted, onMounted} from "vue";
|
||||||
import {useDocStore} from "../../stores/doc";
|
import {useDocStore} from "../../stores/doc";
|
||||||
import {useI18n} from "vue-i18n";
|
import {useI18n} from "vue-i18n";
|
||||||
import OpenInNew from "vue-material-design-icons/OpenInNew.vue";
|
import OpenInNew from "vue-material-design-icons/OpenInNew.vue";
|
||||||
@@ -55,7 +55,9 @@
|
|||||||
import ContextInfoContent from "../ContextInfoContent.vue";
|
import ContextInfoContent from "../ContextInfoContent.vue";
|
||||||
import ContextChildTableOfContents from "./ContextChildTableOfContents.vue";
|
import ContextChildTableOfContents from "./ContextChildTableOfContents.vue";
|
||||||
|
|
||||||
|
|
||||||
import {useNetwork} from "@vueuse/core"
|
import {useNetwork} from "@vueuse/core"
|
||||||
|
import {useScrollMemory} from "../../composables/useScrollMemory"
|
||||||
const {isOnline} = useNetwork()
|
const {isOnline} = useNetwork()
|
||||||
|
|
||||||
import Markdown from "../../components/layout/Markdown.vue";
|
import Markdown from "../../components/layout/Markdown.vue";
|
||||||
@@ -64,19 +66,18 @@
|
|||||||
const docStore = useDocStore();
|
const docStore = useDocStore();
|
||||||
const {t} = useI18n({useScope: "global"});
|
const {t} = useI18n({useScope: "global"});
|
||||||
|
|
||||||
const docWrapper = ref<HTMLDivElement | null>(null);
|
const contextInfoRef = ref<InstanceType<typeof ContextInfoContent> | null>(null);
|
||||||
const docHistory = ref<string[]>([]);
|
const docHistory = ref<string[]>([]);
|
||||||
const currentHistoryIndex = ref(-1);
|
const currentHistoryIndex = ref(-1);
|
||||||
const ast = ref<any>(undefined);
|
const ast = ref<any>(undefined);
|
||||||
|
|
||||||
const pageMetadata = computed(() => docStore.pageMetadata);
|
const pageMetadata = computed(() => docStore.pageMetadata);
|
||||||
const docPath = computed(() => docStore.docPath);
|
const docPath = computed(() => docStore.docPath);
|
||||||
|
|
||||||
const routeInfo = computed(() => ({
|
const routeInfo = computed(() => ({
|
||||||
title: pageMetadata.value?.title ?? t("docs"),
|
title: pageMetadata.value?.title ?? t("docs"),
|
||||||
}));
|
}));
|
||||||
const canGoBack = computed(() => docHistory.value.length > 1 && currentHistoryIndex.value > 0);
|
const canGoBack = computed(() => docHistory.value.length > 1 && currentHistoryIndex.value > 0);
|
||||||
|
|
||||||
|
|
||||||
const addToHistory = (path: string) => {
|
const addToHistory = (path: string) => {
|
||||||
// Always store the path, even empty ones
|
// Always store the path, even empty ones
|
||||||
const pathToAdd = path || "";
|
const pathToAdd = path || "";
|
||||||
@@ -179,8 +180,10 @@
|
|||||||
|
|
||||||
addToHistory(val);
|
addToHistory(val);
|
||||||
refreshPage(val);
|
refreshPage(val);
|
||||||
nextTick(() => docWrapper.value?.scrollTo(0, 0));
|
|
||||||
}, {immediate: true});
|
}, {immediate: true});
|
||||||
|
|
||||||
|
const scrollableElement = computed(() => contextInfoRef.value?.contentRef ?? null)
|
||||||
|
useScrollMemory(ref("context-panel-docs"), scrollableElement as any)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
@@ -23,9 +23,15 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {ref} from "vue"
|
import {ref, computed} from "vue"
|
||||||
|
import {useRoute} from "vue-router";
|
||||||
|
import {useScrollMemory} from "../../composables/useScrollMemory";
|
||||||
|
|
||||||
const collapsed = ref(false);
|
const collapsed = ref(false);
|
||||||
|
const route = useRoute();
|
||||||
|
const scrollKey = computed(() => `docs:${route.fullPath}`);
|
||||||
|
|
||||||
|
useScrollMemory(scrollKey, undefined, true);
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -84,7 +84,7 @@
|
|||||||
import {useExecutionsStore} from "../../stores/executions";
|
import {useExecutionsStore} from "../../stores/executions";
|
||||||
import {useAuthStore} from "override/stores/auth";
|
import {useAuthStore} from "override/stores/auth";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
component: string;
|
component: string;
|
||||||
execution: {
|
execution: {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -95,7 +95,10 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
tooltipPosition: string;
|
tooltipPosition: string;
|
||||||
}>();
|
}>(), {
|
||||||
|
component: "el-button",
|
||||||
|
tooltipPosition: "bottom"
|
||||||
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
follow: [];
|
follow: [];
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
refresh: {shown: true, callback: refresh}
|
refresh: {shown: true, callback: refresh}
|
||||||
}"
|
}"
|
||||||
@update-properties="updateDisplayColumns"
|
@update-properties="updateDisplayColumns"
|
||||||
|
:defaultScope="defaultScopeFilter"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -70,7 +71,7 @@
|
|||||||
@selection-change="handleSelectionChange"
|
@selection-change="handleSelectionChange"
|
||||||
:selectable="!hidden?.includes('selection') && canCheck"
|
:selectable="!hidden?.includes('selection') && canCheck"
|
||||||
:no-data-text="$t('no_results.executions')"
|
:no-data-text="$t('no_results.executions')"
|
||||||
:rowKey="(row: any) => `${row.namespace}-${row.id}`"
|
:rowKey="(row: any) => row.id"
|
||||||
>
|
>
|
||||||
<template #select-actions>
|
<template #select-actions>
|
||||||
<BulkSelect
|
<BulkSelect
|
||||||
@@ -144,10 +145,7 @@
|
|||||||
|
|
||||||
<el-form>
|
<el-form>
|
||||||
<ElFormItem :label="$t('execution labels')">
|
<ElFormItem :label="$t('execution labels')">
|
||||||
<LabelInput
|
<LabelInput v-model:labels="executionLabels" />
|
||||||
:key="executionLabels.map((l) => l.key).join('-')"
|
|
||||||
v-model:labels="executionLabels"
|
|
||||||
/>
|
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
@@ -384,7 +382,7 @@
|
|||||||
import _merge from "lodash/merge";
|
import _merge from "lodash/merge";
|
||||||
import {useI18n} from "vue-i18n";
|
import {useI18n} from "vue-i18n";
|
||||||
import {useRoute, useRouter} from "vue-router";
|
import {useRoute, useRouter} from "vue-router";
|
||||||
import {ref, computed, onMounted, watch, h, useTemplateRef} from "vue";
|
import {ref, computed, watch, h, useTemplateRef} from "vue";
|
||||||
import * as YAML_UTILS from "@kestra-io/ui-libs/flow-yaml-utils";
|
import * as YAML_UTILS from "@kestra-io/ui-libs/flow-yaml-utils";
|
||||||
import {ElMessageBox, ElSwitch, ElFormItem, ElAlert, ElCheckbox} from "element-plus";
|
import {ElMessageBox, ElSwitch, ElFormItem, ElAlert, ElCheckbox} from "element-plus";
|
||||||
|
|
||||||
@@ -411,7 +409,6 @@
|
|||||||
import DateAgo from "../layout/DateAgo.vue";
|
import DateAgo from "../layout/DateAgo.vue";
|
||||||
import DataTable from "../layout/DataTable.vue";
|
import DataTable from "../layout/DataTable.vue";
|
||||||
import BulkSelect from "../layout/BulkSelect.vue";
|
import BulkSelect from "../layout/BulkSelect.vue";
|
||||||
//@ts-expect-error no declaration file
|
|
||||||
import SelectTable from "../layout/SelectTable.vue";
|
import SelectTable from "../layout/SelectTable.vue";
|
||||||
import KSFilter from "../filter/components/KSFilter.vue";
|
import KSFilter from "../filter/components/KSFilter.vue";
|
||||||
import Sections from "../dashboard/sections/Sections.vue";
|
import Sections from "../dashboard/sections/Sections.vue";
|
||||||
@@ -424,14 +421,12 @@
|
|||||||
import {filterValidLabels} from "./utils";
|
import {filterValidLabels} from "./utils";
|
||||||
import {useToast} from "../../utils/toast";
|
import {useToast} from "../../utils/toast";
|
||||||
import {storageKeys} from "../../utils/constants";
|
import {storageKeys} from "../../utils/constants";
|
||||||
import {defaultNamespace} from "../../composables/useNamespaces";
|
|
||||||
import {humanizeDuration, invisibleSpace} from "../../utils/filters";
|
import {humanizeDuration, invisibleSpace} from "../../utils/filters";
|
||||||
import Utils from "../../utils/utils";
|
import Utils from "../../utils/utils";
|
||||||
|
|
||||||
import action from "../../models/action";
|
import action from "../../models/action";
|
||||||
import permission from "../../models/permission";
|
import permission from "../../models/permission";
|
||||||
|
|
||||||
import useRestoreUrl from "../../composables/useRestoreUrl";
|
|
||||||
import useRouteContext from "../../composables/useRouteContext";
|
import useRouteContext from "../../composables/useRouteContext";
|
||||||
import {useTableColumns} from "../../composables/useTableColumns";
|
import {useTableColumns} from "../../composables/useTableColumns";
|
||||||
import {useDataTableActions} from "../../composables/useDataTableActions";
|
import {useDataTableActions} from "../../composables/useDataTableActions";
|
||||||
@@ -463,6 +458,7 @@
|
|||||||
hidden?: string[] | null;
|
hidden?: string[] | null;
|
||||||
flowId?: string | undefined;
|
flowId?: string | undefined;
|
||||||
namespace?: string | undefined;
|
namespace?: string | undefined;
|
||||||
|
defaultScopeFilter?: boolean;
|
||||||
}>(), {
|
}>(), {
|
||||||
embed: false,
|
embed: false,
|
||||||
filter: true,
|
filter: true,
|
||||||
@@ -475,6 +471,7 @@
|
|||||||
hidden: null,
|
hidden: null,
|
||||||
flowId: undefined,
|
flowId: undefined,
|
||||||
namespace: undefined,
|
namespace: undefined,
|
||||||
|
defaultScopeFilter: undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -496,7 +493,6 @@
|
|||||||
const selectedStatus = ref(undefined);
|
const selectedStatus = ref(undefined);
|
||||||
const lastRefreshDate = ref(new Date());
|
const lastRefreshDate = ref(new Date());
|
||||||
const unqueueDialogVisible = ref(false);
|
const unqueueDialogVisible = ref(false);
|
||||||
const isDefaultNamespaceAllow = ref(true);
|
|
||||||
const changeStatusDialogVisible = ref(false);
|
const changeStatusDialogVisible = ref(false);
|
||||||
const actionOptions = ref<Record<string, any>>({});
|
const actionOptions = ref<Record<string, any>>({});
|
||||||
const dblClickRouteName = ref("executions/update");
|
const dblClickRouteName = ref("executions/update");
|
||||||
@@ -614,11 +610,6 @@
|
|||||||
const routeInfo = computed(() => ({title: t("executions")}));
|
const routeInfo = computed(() => ({title: t("executions")}));
|
||||||
useRouteContext(routeInfo, props.embed);
|
useRouteContext(routeInfo, props.embed);
|
||||||
|
|
||||||
const {saveRestoreUrl} = useRestoreUrl({
|
|
||||||
restoreUrl: true,
|
|
||||||
isDefaultNamespaceAllow: isDefaultNamespaceAllow.value
|
|
||||||
});
|
|
||||||
|
|
||||||
const dataTableRef = ref(null);
|
const dataTableRef = ref(null);
|
||||||
const selectTableRef = useTemplateRef<typeof SelectTable>("selectTable");
|
const selectTableRef = useTemplateRef<typeof SelectTable>("selectTable");
|
||||||
|
|
||||||
@@ -634,8 +625,7 @@
|
|||||||
dblClickRouteName: dblClickRouteName.value,
|
dblClickRouteName: dblClickRouteName.value,
|
||||||
embed: props.embed,
|
embed: props.embed,
|
||||||
dataTableRef,
|
dataTableRef,
|
||||||
loadData: loadData,
|
loadData: loadData
|
||||||
saveRestoreUrl
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -1043,31 +1033,6 @@
|
|||||||
emit("state-count", {runningCount, totalCount});
|
emit("state-count", {runningCount, totalCount});
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
const query = {...route.query};
|
|
||||||
let queryHasChanged = false;
|
|
||||||
|
|
||||||
const queryKeys = Object.keys(query);
|
|
||||||
if (props.namespace === undefined && defaultNamespace() && !queryKeys.some(key => key.startsWith("filters[namespace]"))) {
|
|
||||||
query["filters[namespace][PREFIX]"] = defaultNamespace();
|
|
||||||
queryHasChanged = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!queryKeys.some(key => key.startsWith("filters[scope]"))) {
|
|
||||||
query["filters[scope][EQUALS]"] = "USER";
|
|
||||||
queryHasChanged = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (queryHasChanged) {
|
|
||||||
router.replace({query});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (route.name === "flows/update") {
|
|
||||||
optionalColumns.value = optionalColumns.value.
|
|
||||||
filter(col => col.prop !== "namespace" && col.prop !== "flowId");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(isOpenLabelsModal, (opening) => {
|
watch(isOpenLabelsModal, (opening) => {
|
||||||
if (opening) {
|
if (opening) {
|
||||||
executionLabels.value = [];
|
executionLabels.value = [];
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
v-if="!isExecutionStarted"
|
v-if="!isExecutionStarted"
|
||||||
:execution="execution"
|
:execution="execution"
|
||||||
/>
|
/>
|
||||||
<el-card id="gantt" shadow="never" v-else-if="execution && executionsStore.flow">
|
<el-card id="gantt" shadow="never" :class="{'no-border': !hasValidDate}" v-else-if="execution && executionsStore.flow">
|
||||||
<template #header>
|
<template #header v-if="hasValidDate">
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<Duration class="th text-end" :histories="execution.state.histories" />
|
<Duration class="th text-end" :histories="execution.state.histories" />
|
||||||
<span class="text-end" v-for="(date, i) in dates" :key="i">
|
<span class="text-end" v-for="(date, i) in dates" :key="i">
|
||||||
@@ -234,6 +234,9 @@
|
|||||||
isExecutionStarted() {
|
isExecutionStarted() {
|
||||||
return this.execution?.state?.current && !["CREATED", "QUEUED"].includes(this.execution.state.current);
|
return this.execution?.state?.current && !["CREATED", "QUEUED"].includes(this.execution.state.current);
|
||||||
},
|
},
|
||||||
|
hasValidDate() {
|
||||||
|
return isFinite(this.delta());
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
forwardEvent(type, event) {
|
forwardEvent(type, event) {
|
||||||
@@ -443,6 +446,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.no-border {
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
// To Separate through Line
|
// To Separate through Line
|
||||||
:deep(.vue-recycle-scroller__item-view) {
|
:deep(.vue-recycle-scroller__item-view) {
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ export function useExecutionRoot() {
|
|||||||
follow();
|
follow();
|
||||||
window.addEventListener("popstate", follow);
|
window.addEventListener("popstate", follow);
|
||||||
|
|
||||||
dependenciesCount.value = (await flowStore.loadDependencies({namespace: route.params.namespace as string, id: route.params.flowId as string, subtype: "FLOW"})).count;
|
dependenciesCount.value = (await flowStore.loadDependencies({namespace: route.params.namespace as string, id: route.params.flowId as string, subtype: "FLOW"}, true)).count;
|
||||||
previousExecutionId.value = route.params.id as string;
|
previousExecutionId.value = route.params.id as string;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {ref, computed, watch, PropType} from "vue";
|
import {ref, computed, watch, PropType} from "vue";
|
||||||
import DateSelect from "./DateSelect.vue";
|
import DateSelect from "./DateSelect.vue";
|
||||||
|
import {useI18n} from "vue-i18n";
|
||||||
|
|
||||||
interface TimePreset {
|
interface TimePreset {
|
||||||
value?: string;
|
value?: string;
|
||||||
@@ -64,9 +65,11 @@
|
|||||||
timeFilterPresets.value.map(preset => preset.value)
|
timeFilterPresets.value.map(preset => preset.value)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const {t} = useI18n();
|
||||||
|
|
||||||
const customAwarePlaceholder = computed<string | undefined>(() => {
|
const customAwarePlaceholder = computed<string | undefined>(() => {
|
||||||
if (props.placeholder) return props.placeholder;
|
if (props.placeholder) return props.placeholder;
|
||||||
return props.allowCustom ? "datepicker.custom" : undefined;
|
return props.allowCustom ? t("datepicker.custom") : undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
const onTimeRangeSelect = (range: string | undefined) => {
|
const onTimeRangeSelect = (range: string | undefined) => {
|
||||||
|
|||||||
@@ -531,8 +531,9 @@
|
|||||||
}
|
}
|
||||||
.content-container {
|
.content-container {
|
||||||
height: calc(100vh - 0px);
|
height: calc(100vh - 0px);
|
||||||
overflow-y: auto !important;
|
overflow-y: scroll;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
scrollbar-gutter: stable;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -541,19 +542,16 @@
|
|||||||
|
|
||||||
:deep(.el-collapse) {
|
:deep(.el-collapse) {
|
||||||
.el-collapse-item__wrap {
|
.el-collapse-item__wrap {
|
||||||
overflow-y: auto !important;
|
|
||||||
max-height: none !important;
|
max-height: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-collapse-item__content {
|
.el-collapse-item__content {
|
||||||
overflow-y: auto !important;
|
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.var-value) {
|
:deep(.var-value) {
|
||||||
overflow-y: auto !important;
|
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,8 @@
|
|||||||
searchInputFullWidth?: boolean;
|
searchInputFullWidth?: boolean;
|
||||||
legacyQuery?: boolean;
|
legacyQuery?: boolean;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
|
defaultScope?: boolean;
|
||||||
|
defaultTimeRange?: boolean;
|
||||||
}>(), {
|
}>(), {
|
||||||
buttons: () => ({}),
|
buttons: () => ({}),
|
||||||
tableOptions: () => ({}),
|
tableOptions: () => ({}),
|
||||||
@@ -53,7 +55,9 @@
|
|||||||
showSearchInput: true,
|
showSearchInput: true,
|
||||||
searchInputFullWidth: false,
|
searchInputFullWidth: false,
|
||||||
legacyQuery: false,
|
legacyQuery: false,
|
||||||
readOnly: false
|
readOnly: false,
|
||||||
|
defaultScope: undefined,
|
||||||
|
defaultTimeRange: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const emits = defineEmits<{
|
const emits = defineEmits<{
|
||||||
@@ -75,7 +79,9 @@
|
|||||||
} = useFilters(
|
} = useFilters(
|
||||||
props.configuration,
|
props.configuration,
|
||||||
props.showSearchInput,
|
props.showSearchInput,
|
||||||
props.legacyQuery
|
props.legacyQuery,
|
||||||
|
props.defaultScope,
|
||||||
|
props.defaultTimeRange,
|
||||||
);
|
);
|
||||||
|
|
||||||
const {savedFilters, saveFilter, updateSavedFilter, deleteSavedFilter} = useSavedFilters(
|
const {savedFilters, saveFilter, updateSavedFilter, deleteSavedFilter} = useSavedFilters(
|
||||||
@@ -166,6 +172,7 @@
|
|||||||
watch(appliedFilters, (newFilters) => {
|
watch(appliedFilters, (newFilters) => {
|
||||||
emits("filter", newFilters);
|
emits("filter", newFilters);
|
||||||
}, {deep: true});
|
}, {deep: true});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
84
ui/src/components/filter/composables/useDefaultFilter.ts
Normal file
84
ui/src/components/filter/composables/useDefaultFilter.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import {nextTick, onMounted} from "vue";
|
||||||
|
import {LocationQuery, useRoute, useRouter} from "vue-router";
|
||||||
|
import {useMiscStore} from "override/stores/misc";
|
||||||
|
import {defaultNamespace} from "../../../composables/useNamespaces";
|
||||||
|
|
||||||
|
interface DefaultFilterOptions {
|
||||||
|
namespace?: string;
|
||||||
|
includeTimeRange?: boolean;
|
||||||
|
includeScope?: boolean;
|
||||||
|
legacyQuery?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NAMESPACE_FILTER_PREFIX = "filters[namespace]";
|
||||||
|
const SCOPE_FILTER_PREFIX = "filters[scope]";
|
||||||
|
const TIME_RANGE_FILTER_PREFIX = "filters[timeRange]";
|
||||||
|
|
||||||
|
const hasFilterKey = (query: LocationQuery, prefix: string): boolean =>
|
||||||
|
Object.keys(query).some(key => key.startsWith(prefix));
|
||||||
|
|
||||||
|
export function applyDefaultFilters(
|
||||||
|
currentQuery: LocationQuery,
|
||||||
|
{
|
||||||
|
namespace,
|
||||||
|
includeTimeRange,
|
||||||
|
includeScope,
|
||||||
|
legacyQuery,
|
||||||
|
}: DefaultFilterOptions = {}): { query: LocationQuery, change: boolean } {
|
||||||
|
|
||||||
|
if(currentQuery && Object.keys(currentQuery).length > 0) {
|
||||||
|
return {
|
||||||
|
query: currentQuery,
|
||||||
|
change: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = {...currentQuery};
|
||||||
|
|
||||||
|
if (namespace === undefined && defaultNamespace() && !hasFilterKey(query, NAMESPACE_FILTER_PREFIX)) {
|
||||||
|
query[legacyQuery ? "namespace" : `${NAMESPACE_FILTER_PREFIX}[PREFIX]`] = defaultNamespace();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeScope && !hasFilterKey(query, SCOPE_FILTER_PREFIX)) {
|
||||||
|
query[legacyQuery ? "scope" : `${SCOPE_FILTER_PREFIX}[EQUALS]`] = "USER";
|
||||||
|
}
|
||||||
|
|
||||||
|
const TIME_FILTER_KEYS = /startDate|endDate|timeRange/;
|
||||||
|
|
||||||
|
if (includeTimeRange && !Object.keys(query).some(key => TIME_FILTER_KEYS.test(key))) {
|
||||||
|
const defaultDuration = useMiscStore().configs?.chartDefaultDuration ?? "P30D";
|
||||||
|
query[legacyQuery ? "timeRange" : `${TIME_RANGE_FILTER_PREFIX}[EQUALS]`] = defaultDuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {query, change: true};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDefaultFilter(
|
||||||
|
defaultOptions?: DefaultFilterOptions,
|
||||||
|
) {
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
// wait for router to be ready
|
||||||
|
await nextTick()
|
||||||
|
// wait for the useRestoreUrl to apply its changes
|
||||||
|
await nextTick()
|
||||||
|
// finally add default filter if necessary
|
||||||
|
const {query, change} = applyDefaultFilters(route.query, defaultOptions)
|
||||||
|
if(change) {
|
||||||
|
router.replace({...route, query})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function resetDefaultFilter(){
|
||||||
|
router.replace({
|
||||||
|
...route,
|
||||||
|
query: applyDefaultFilters({}, defaultOptions).query
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
resetDefaultFilter
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,8 +17,16 @@ import {
|
|||||||
KV_COMPARATORS
|
KV_COMPARATORS
|
||||||
} from "../utils/filterTypes";
|
} from "../utils/filterTypes";
|
||||||
import {usePreAppliedFilters} from "./usePreAppliedFilters";
|
import {usePreAppliedFilters} from "./usePreAppliedFilters";
|
||||||
|
import {useDefaultFilter} from "./useDefaultFilter";
|
||||||
|
|
||||||
export function useFilters(configuration: FilterConfiguration, showSearchInput = true, legacyQuery = false) {
|
|
||||||
|
export function useFilters(
|
||||||
|
configuration: FilterConfiguration,
|
||||||
|
showSearchInput = true,
|
||||||
|
legacyQuery = false,
|
||||||
|
defaultScope?: boolean,
|
||||||
|
defaultTimeRange?: boolean
|
||||||
|
) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
@@ -28,8 +36,7 @@ export function useFilters(configuration: FilterConfiguration, showSearchInput =
|
|||||||
const {
|
const {
|
||||||
markAsPreApplied,
|
markAsPreApplied,
|
||||||
hasPreApplied,
|
hasPreApplied,
|
||||||
getPreApplied,
|
getPreApplied
|
||||||
getAllPreApplied
|
|
||||||
} = usePreAppliedFilters();
|
} = usePreAppliedFilters();
|
||||||
|
|
||||||
const appendQueryParam = (query: Record<string, any>, key: string, value: string) => {
|
const appendQueryParam = (query: Record<string, any>, key: string, value: string) => {
|
||||||
@@ -367,24 +374,24 @@ export function useFilters(configuration: FilterConfiguration, showSearchInput =
|
|||||||
updateRoute();
|
updateRoute();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
const {resetDefaultFilter} = useDefaultFilter({
|
||||||
* Resets all filters to their pre-applied state and clears the search query
|
legacyQuery,
|
||||||
*/
|
includeScope: defaultScope ?? configuration.keys?.some((k) => k.key === "scope"),
|
||||||
|
includeTimeRange: defaultTimeRange ?? configuration.keys?.some((k) => k.key === "timeRange"),
|
||||||
|
});
|
||||||
|
|
||||||
const resetToPreApplied = () => {
|
const resetToPreApplied = () => {
|
||||||
appliedFilters.value = getAllPreApplied();
|
|
||||||
searchQuery.value = "";
|
searchQuery.value = "";
|
||||||
updateRoute();
|
resetDefaultFilter();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
watch(searchQuery, () => {
|
||||||
|
updateRoute();
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
appliedFilters: computed(() => appliedFilters.value),
|
appliedFilters: computed(() => appliedFilters.value),
|
||||||
searchQuery: computed({
|
searchQuery,
|
||||||
get: () => searchQuery.value,
|
|
||||||
set: value => {
|
|
||||||
searchQuery.value = value;
|
|
||||||
updateRoute();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
addFilter,
|
addFilter,
|
||||||
removeFilter,
|
removeFilter,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export function useValues(label: string | undefined, t?: ReturnType<typeof useI1
|
|||||||
{label: t("datepicker.last24hours"), value: "PT24H"},
|
{label: t("datepicker.last24hours"), value: "PT24H"},
|
||||||
{label: t("datepicker.last48hours"), value: "PT48H"},
|
{label: t("datepicker.last48hours"), value: "PT48H"},
|
||||||
{label: t("datepicker.last7days"), value: "PT168H"},
|
{label: t("datepicker.last7days"), value: "PT168H"},
|
||||||
{label: t("datepicker.last30days"), value: "PT720H"},
|
{label: t("datepicker.last30days"), value: "P30D"},
|
||||||
{label: t("datepicker.last365days"), value: "PT8760H"},
|
{label: t("datepicker.last365days"), value: "PT8760H"},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,15 @@ import {computed, ComputedRef} from "vue";
|
|||||||
import {FilterConfiguration} from "../utils/filterTypes";
|
import {FilterConfiguration} from "../utils/filterTypes";
|
||||||
import {useI18n} from "vue-i18n";
|
import {useI18n} from "vue-i18n";
|
||||||
|
|
||||||
export const useBlueprintFilter = (): ComputedRef<FilterConfiguration> => computed(() => {
|
export const useBlueprintFilter = (): ComputedRef<FilterConfiguration> => {
|
||||||
const {t} = useI18n();
|
const {t} = useI18n();
|
||||||
|
|
||||||
return {
|
return computed(() => {
|
||||||
title: t("filter.titles.blueprint_filters"),
|
return {
|
||||||
searchPlaceholder: t("filter.search_placeholders.search_blueprints"),
|
title: t("filter.titles.blueprint_filters"),
|
||||||
keys: [
|
searchPlaceholder: t("filter.search_placeholders.search_blueprints"),
|
||||||
]
|
keys: [
|
||||||
};
|
]
|
||||||
});
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -7,152 +7,160 @@ import {useAuthStore} from "override/stores/auth";
|
|||||||
import {useValues} from "../composables/useValues";
|
import {useValues} from "../composables/useValues";
|
||||||
import {useI18n} from "vue-i18n";
|
import {useI18n} from "vue-i18n";
|
||||||
|
|
||||||
export const useDashboardFilter = (): ComputedRef<FilterConfiguration> => computed(() => {
|
export const useDashboardFilter = (): ComputedRef<FilterConfiguration> => {
|
||||||
const {t} = useI18n();
|
const {t} = useI18n();
|
||||||
|
|
||||||
return {
|
return computed(() => {
|
||||||
title: t("filter.titles.dashboard_filters"),
|
return {
|
||||||
searchPlaceholder: t("filter.search_placeholders.search_dashboards"),
|
title: t("filter.titles.dashboard_filters"),
|
||||||
keys: [
|
searchPlaceholder: t("filter.search_placeholders.search_dashboards"),
|
||||||
{
|
keys: [
|
||||||
key: "namespace",
|
{
|
||||||
label: t("filter.namespace.label"),
|
key: "namespace",
|
||||||
description: t("filter.namespace.description"),
|
label: t("filter.namespace.label"),
|
||||||
comparators: [
|
description: t("filter.namespace.description"),
|
||||||
Comparators.IN,
|
comparators: [
|
||||||
Comparators.NOT_IN,
|
Comparators.IN,
|
||||||
Comparators.CONTAINS,
|
Comparators.NOT_IN,
|
||||||
Comparators.PREFIX,
|
Comparators.CONTAINS,
|
||||||
],
|
Comparators.PREFIX,
|
||||||
valueType: "multi-select",
|
],
|
||||||
valueProvider: async () => {
|
valueType: "multi-select",
|
||||||
const user = useAuthStore().user;
|
valueProvider: async () => {
|
||||||
if (user && user.hasAnyActionOnAnyNamespace(permission.NAMESPACE, action.READ)) {
|
const user = useAuthStore().user;
|
||||||
const namespacesStore = useNamespacesStore();
|
if (user && user.hasAnyActionOnAnyNamespace(permission.NAMESPACE, action.READ)) {
|
||||||
const namespaces = (await namespacesStore.loadAutocomplete()) as string[];
|
const namespacesStore = useNamespacesStore();
|
||||||
return [...new Set(namespaces
|
const namespaces = (await namespacesStore.loadAutocomplete()) as string[];
|
||||||
.flatMap(namespace => {
|
return [...new Set(namespaces
|
||||||
return namespace.split(".").reduce((current: string[], part: string) => {
|
.flatMap(namespace => {
|
||||||
const previousCombination = current?.[current.length - 1];
|
return namespace.split(".").reduce((current: string[], part: string) => {
|
||||||
return [...current, `${(previousCombination ? previousCombination + "." : "")}${part}`];
|
const previousCombination = current?.[current.length - 1];
|
||||||
}, []);
|
return [...current, `${(previousCombination ? previousCombination + "." : "")}${part}`];
|
||||||
}))].map(namespace => ({
|
}, []);
|
||||||
label: namespace,
|
}))].map(namespace => ({
|
||||||
value: namespace
|
label: namespace,
|
||||||
}));
|
value: namespace
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
searchable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "timeRange",
|
||||||
|
label: t("filter.timeRange_dashboard.label"),
|
||||||
|
description: t("filter.timeRange_dashboard.description"),
|
||||||
|
comparators: [Comparators.EQUALS],
|
||||||
|
valueType: "select",
|
||||||
|
valueProvider: async () => {
|
||||||
|
const {VALUES} = useValues("dashboard");
|
||||||
|
return VALUES.RELATIVE_DATE;
|
||||||
}
|
}
|
||||||
return [];
|
|
||||||
},
|
},
|
||||||
searchable: true
|
{
|
||||||
},
|
key: "state",
|
||||||
{
|
label: t("filter.state.label"),
|
||||||
key: "timeRange",
|
description: t("filter.state.description"),
|
||||||
label: t("filter.timeRange_dashboard.label"),
|
comparators: [Comparators.IN, Comparators.NOT_IN],
|
||||||
description: t("filter.timeRange_dashboard.description"),
|
valueType: "multi-select",
|
||||||
comparators: [Comparators.EQUALS],
|
valueProvider: async () => {
|
||||||
valueType: "select",
|
const {VALUES} = useValues("executions");
|
||||||
valueProvider: async () => {
|
return VALUES.EXECUTION_STATES;
|
||||||
const {VALUES} = useValues("dashboard");
|
},
|
||||||
return VALUES.RELATIVE_DATE;
|
showComparatorSelection: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "labels",
|
||||||
|
label: t("filter.labels.label"),
|
||||||
|
description: t("filter.labels.description"),
|
||||||
|
comparators: [Comparators.EQUALS, Comparators.NOT_EQUALS],
|
||||||
|
valueType: "text",
|
||||||
}
|
}
|
||||||
},
|
]
|
||||||
{
|
};
|
||||||
key: "state",
|
});
|
||||||
label: t("filter.state.label"),
|
};
|
||||||
description: t("filter.state.description"),
|
|
||||||
comparators: [Comparators.IN, Comparators.NOT_IN],
|
|
||||||
valueType: "multi-select",
|
|
||||||
valueProvider: async () => {
|
|
||||||
const {VALUES} = useValues("executions");
|
|
||||||
return VALUES.EXECUTION_STATES;
|
|
||||||
},
|
|
||||||
showComparatorSelection: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "labels",
|
|
||||||
label: t("filter.labels.label"),
|
|
||||||
description: t("filter.labels.description"),
|
|
||||||
comparators: [Comparators.EQUALS, Comparators.NOT_EQUALS],
|
|
||||||
valueType: "text",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
export const useNamespaceDashboardFilter = (): ComputedRef<FilterConfiguration> => computed(() => {
|
export const useNamespaceDashboardFilter = (): ComputedRef<FilterConfiguration> => {
|
||||||
const {t} = useI18n();
|
const {t} = useI18n();
|
||||||
|
|
||||||
return {
|
return computed(() => {
|
||||||
title: t("filter.titles.namespace_dashboard_filters"),
|
|
||||||
searchPlaceholder: t("filter.search_placeholders.search_dashboards"),
|
|
||||||
keys: [
|
|
||||||
{
|
|
||||||
key: "flowId",
|
|
||||||
label: t("filter.flowId.label"),
|
|
||||||
description: t("filter.flowId.description"),
|
|
||||||
comparators: [
|
|
||||||
Comparators.EQUALS,
|
|
||||||
Comparators.NOT_EQUALS,
|
|
||||||
Comparators.CONTAINS,
|
|
||||||
Comparators.STARTS_WITH,
|
|
||||||
Comparators.ENDS_WITH,
|
|
||||||
],
|
|
||||||
valueType: "text",
|
|
||||||
// valueProvider: async () => {
|
|
||||||
// const flowStore = useFlowStore();
|
|
||||||
|
|
||||||
// const flowIds = await flowStore.loadDistinctFlowIds();
|
return {
|
||||||
// return flowIds.map((flowId: string) => ({label: flowId, value: flowId}));
|
title: t("filter.titles.namespace_dashboard_filters"),
|
||||||
// },
|
searchPlaceholder: t("filter.search_placeholders.search_dashboards"),
|
||||||
searchable: true
|
keys: [
|
||||||
},
|
{
|
||||||
{
|
key: "flowId",
|
||||||
key: "timeRange",
|
label: t("filter.flowId.label"),
|
||||||
label: t("filter.timeRange_dashboard.label"),
|
description: t("filter.flowId.description"),
|
||||||
description: t("filter.timeRange_dashboard.description"),
|
comparators: [
|
||||||
comparators: [Comparators.EQUALS],
|
Comparators.EQUALS,
|
||||||
valueType: "select",
|
Comparators.NOT_EQUALS,
|
||||||
valueProvider: async () => {
|
Comparators.CONTAINS,
|
||||||
const {VALUES} = useValues("dashboard");
|
Comparators.STARTS_WITH,
|
||||||
return VALUES.RELATIVE_DATE;
|
Comparators.ENDS_WITH,
|
||||||
|
],
|
||||||
|
valueType: "text",
|
||||||
|
// valueProvider: async () => {
|
||||||
|
// const flowStore = useFlowStore();
|
||||||
|
|
||||||
|
// const flowIds = await flowStore.loadDistinctFlowIds();
|
||||||
|
// return flowIds.map((flowId: string) => ({label: flowId, value: flowId}));
|
||||||
|
// },
|
||||||
|
searchable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "timeRange",
|
||||||
|
label: t("filter.timeRange_dashboard.label"),
|
||||||
|
description: t("filter.timeRange_dashboard.description"),
|
||||||
|
comparators: [Comparators.EQUALS],
|
||||||
|
valueType: "select",
|
||||||
|
valueProvider: async () => {
|
||||||
|
const {VALUES} = useValues("dashboard");
|
||||||
|
return VALUES.RELATIVE_DATE;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "labels",
|
||||||
|
label: t("filter.labels.label"),
|
||||||
|
description: "Filter by labels",
|
||||||
|
comparators: [Comparators.EQUALS, Comparators.NOT_EQUALS],
|
||||||
|
valueType: "text",
|
||||||
}
|
}
|
||||||
},
|
]
|
||||||
{
|
};
|
||||||
key: "labels",
|
});
|
||||||
label: t("filter.labels.label"),
|
};
|
||||||
description: "Filter by labels",
|
|
||||||
comparators: [Comparators.EQUALS, Comparators.NOT_EQUALS],
|
|
||||||
valueType: "text",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
export const useFlowDashboardFilter = (): ComputedRef<FilterConfiguration> => computed(() => {
|
export const useFlowDashboardFilter = (): ComputedRef<FilterConfiguration> => {
|
||||||
const {t} = useI18n();
|
const {t} = useI18n();
|
||||||
|
|
||||||
return {
|
return computed(() => {
|
||||||
title: t("filter.titles.flow_dashboard_filters"),
|
|
||||||
searchPlaceholder: t("filter.search_placeholders.search_dashboards"),
|
return {
|
||||||
keys: [
|
title: t("filter.titles.flow_dashboard_filters"),
|
||||||
{
|
searchPlaceholder: t("filter.search_placeholders.search_dashboards"),
|
||||||
key: "timeRange",
|
keys: [
|
||||||
label: t("filter.timeRange_dashboard.label"),
|
{
|
||||||
description: t("filter.timeRange_dashboard.description"),
|
key: "timeRange",
|
||||||
comparators: [Comparators.EQUALS],
|
label: t("filter.timeRange_dashboard.label"),
|
||||||
valueType: "select",
|
description: t("filter.timeRange_dashboard.description"),
|
||||||
valueProvider: async () => {
|
comparators: [Comparators.EQUALS],
|
||||||
const {VALUES} = useValues("dashboard");
|
valueType: "select",
|
||||||
return VALUES.RELATIVE_DATE;
|
valueProvider: async () => {
|
||||||
|
const {VALUES} = useValues("dashboard");
|
||||||
|
return VALUES.RELATIVE_DATE;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "labels",
|
||||||
|
label: t("filter.labels.label"),
|
||||||
|
description: t("filter.labels.description"),
|
||||||
|
comparators: [Comparators.EQUALS, Comparators.NOT_EQUALS],
|
||||||
|
valueType: "text",
|
||||||
}
|
}
|
||||||
},
|
]
|
||||||
{
|
};
|
||||||
key: "labels",
|
});
|
||||||
label: t("filter.labels.label"),
|
};
|
||||||
description: t("filter.labels.description"),
|
|
||||||
comparators: [Comparators.EQUALS, Comparators.NOT_EQUALS],
|
|
||||||
valueType: "text",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
});
|
|
||||||
@@ -6,137 +6,143 @@ import {useNamespacesStore} from "override/stores/namespaces";
|
|||||||
import {useAuthStore} from "override/stores/auth";
|
import {useAuthStore} from "override/stores/auth";
|
||||||
import {useValues} from "../composables/useValues";
|
import {useValues} from "../composables/useValues";
|
||||||
import {useI18n} from "vue-i18n";
|
import {useI18n} from "vue-i18n";
|
||||||
|
import {useRoute} from "vue-router";
|
||||||
|
|
||||||
export const useExecutionFilter = (): ComputedRef<FilterConfiguration> => computed(() => {
|
export const useExecutionFilter = (): ComputedRef<FilterConfiguration> => {
|
||||||
const {t} = useI18n();
|
const {t} = useI18n();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
return {
|
return computed(() => {
|
||||||
title: t("filter.titles.execution_filters"),
|
return {
|
||||||
searchPlaceholder: t("filter.search_placeholders.search_executions"),
|
title: t("filter.titles.execution_filters"),
|
||||||
keys: [
|
searchPlaceholder: t("filter.search_placeholders.search_executions"),
|
||||||
{
|
keys: [
|
||||||
key: "namespace",
|
...(route.name !== "namespaces/update" ? [
|
||||||
label: t("filter.namespace.label"),
|
{
|
||||||
description: t("filter.namespace.description"),
|
key: "namespace",
|
||||||
comparators: [
|
label: t("filter.namespace.label"),
|
||||||
Comparators.IN,
|
description: t("filter.namespace.description"),
|
||||||
Comparators.NOT_IN,
|
comparators: [
|
||||||
Comparators.CONTAINS,
|
Comparators.IN,
|
||||||
Comparators.PREFIX,
|
Comparators.NOT_IN,
|
||||||
],
|
Comparators.CONTAINS,
|
||||||
valueType: "multi-select",
|
Comparators.PREFIX,
|
||||||
valueProvider: async () => {
|
],
|
||||||
const user = useAuthStore().user;
|
valueType: "multi-select" as const,
|
||||||
if (user && user.hasAnyActionOnAnyNamespace(permission.NAMESPACE, action.READ)) {
|
valueProvider: async () => {
|
||||||
const namespacesStore = useNamespacesStore();
|
const user = useAuthStore().user;
|
||||||
const namespaces = (await namespacesStore.loadAutocomplete()) as string[];
|
if (user && user.hasAnyActionOnAnyNamespace(permission.NAMESPACE, action.READ)) {
|
||||||
return [...new Set(namespaces
|
const namespacesStore = useNamespacesStore();
|
||||||
.flatMap(namespace => {
|
const namespaces = (await namespacesStore.loadAutocomplete()) as string[];
|
||||||
return namespace.split(".").reduce((current: string[], part: string) => {
|
return [...new Set(namespaces
|
||||||
const previousCombination = current?.[current.length - 1];
|
.flatMap(namespace => {
|
||||||
return [...current, `${(previousCombination ? previousCombination + "." : "")}${part}`];
|
return namespace.split(".").reduce((current: string[], part: string) => {
|
||||||
}, []);
|
const previousCombination = current?.[current.length - 1];
|
||||||
}))].map(namespace => ({
|
return [...current, `${(previousCombination ? previousCombination + "." : "")}${part}`];
|
||||||
label: namespace,
|
}, []);
|
||||||
value: namespace
|
}))].map(namespace => ({
|
||||||
}));
|
label: namespace,
|
||||||
|
value: namespace
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
searchable: true
|
||||||
|
},
|
||||||
|
] : []) as any,
|
||||||
|
...(route.name !== "flows/update" ? [{
|
||||||
|
key: "flowId",
|
||||||
|
label: t("filter.flowId.label"),
|
||||||
|
description: t("filter.flowId.description"),
|
||||||
|
comparators: [
|
||||||
|
Comparators.EQUALS,
|
||||||
|
Comparators.NOT_EQUALS,
|
||||||
|
Comparators.CONTAINS,
|
||||||
|
Comparators.STARTS_WITH,
|
||||||
|
Comparators.ENDS_WITH,
|
||||||
|
],
|
||||||
|
valueType: "text",
|
||||||
|
}] : []) as any,
|
||||||
|
{
|
||||||
|
key: "kind",
|
||||||
|
label: t("filter.kind.label"),
|
||||||
|
description: t("filter.kind.description"),
|
||||||
|
comparators: [Comparators.EQUALS],
|
||||||
|
valueType: "radio",
|
||||||
|
valueProvider: async () => {
|
||||||
|
const {VALUES} = useValues("executions");
|
||||||
|
return VALUES.KINDS;
|
||||||
}
|
}
|
||||||
return [];
|
|
||||||
},
|
},
|
||||||
searchable: true
|
{
|
||||||
},
|
key: "state",
|
||||||
{
|
label: t("filter.state.label"),
|
||||||
key: "flowId",
|
description: t("filter.state.description"),
|
||||||
label: t("filter.flowId.label"),
|
comparators: [Comparators.IN, Comparators.NOT_IN],
|
||||||
description: t("filter.flowId.description"),
|
valueType: "multi-select",
|
||||||
comparators: [
|
valueProvider: async () => {
|
||||||
Comparators.EQUALS,
|
const {VALUES} = useValues("executions");
|
||||||
Comparators.NOT_EQUALS,
|
return VALUES.EXECUTION_STATES;
|
||||||
Comparators.CONTAINS,
|
},
|
||||||
Comparators.STARTS_WITH,
|
showComparatorSelection: true,
|
||||||
Comparators.ENDS_WITH,
|
searchable: true
|
||||||
],
|
|
||||||
valueType: "text",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "kind",
|
|
||||||
label: t("filter.kind.label"),
|
|
||||||
description: t("filter.kind.description"),
|
|
||||||
comparators: [Comparators.EQUALS],
|
|
||||||
valueType: "radio",
|
|
||||||
valueProvider: async () => {
|
|
||||||
const {VALUES} = useValues("executions");
|
|
||||||
return VALUES.KINDS;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "state",
|
|
||||||
label: t("filter.state.label"),
|
|
||||||
description: t("filter.state.description"),
|
|
||||||
comparators: [Comparators.IN, Comparators.NOT_IN],
|
|
||||||
valueType: "multi-select",
|
|
||||||
valueProvider: async () => {
|
|
||||||
const {VALUES} = useValues("executions");
|
|
||||||
return VALUES.EXECUTION_STATES;
|
|
||||||
},
|
},
|
||||||
showComparatorSelection: true,
|
{
|
||||||
searchable: true
|
key: "scope",
|
||||||
},
|
label: t("filter.scope.label"),
|
||||||
{
|
description: t("filter.scope.description"),
|
||||||
key: "scope",
|
comparators: [Comparators.EQUALS, Comparators.NOT_EQUALS],
|
||||||
label: t("filter.scope.label"),
|
valueType: "radio",
|
||||||
description: t("filter.scope.description"),
|
valueProvider: async () => {
|
||||||
comparators: [Comparators.EQUALS, Comparators.NOT_EQUALS],
|
const {VALUES} = useValues("executions");
|
||||||
valueType: "radio",
|
return VALUES.SCOPES;
|
||||||
valueProvider: async () => {
|
},
|
||||||
const {VALUES} = useValues("executions");
|
showComparatorSelection: false
|
||||||
return VALUES.SCOPES;
|
|
||||||
},
|
},
|
||||||
showComparatorSelection: false
|
{
|
||||||
},
|
key: "childFilter",
|
||||||
{
|
label: t("filter.childFilter.label"),
|
||||||
key: "childFilter",
|
description: t("filter.childFilter.description"),
|
||||||
label: t("filter.childFilter.label"),
|
comparators: [Comparators.EQUALS],
|
||||||
description: t("filter.childFilter.description"),
|
valueType: "radio",
|
||||||
comparators: [Comparators.EQUALS],
|
valueProvider: async () => {
|
||||||
valueType: "radio",
|
const {VALUES} = useValues("executions");
|
||||||
valueProvider: async () => {
|
return VALUES.CHILDS;
|
||||||
const {VALUES} = useValues("executions");
|
}
|
||||||
return VALUES.CHILDS;
|
},
|
||||||
|
{
|
||||||
|
key: "timeRange",
|
||||||
|
label: t("filter.timeRange.label"),
|
||||||
|
description: t("filter.timeRange.description"),
|
||||||
|
comparators: [Comparators.EQUALS],
|
||||||
|
valueType: "select",
|
||||||
|
valueProvider: async () => {
|
||||||
|
const {VALUES} = useValues("executions");
|
||||||
|
return VALUES.RELATIVE_DATE;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "labels",
|
||||||
|
label: t("filter.labels_execution.label"),
|
||||||
|
description: t("filter.labels_execution.description"),
|
||||||
|
comparators: [Comparators.EQUALS, Comparators.NOT_EQUALS],
|
||||||
|
valueType: "text",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "triggerExecutionId",
|
||||||
|
label: t("filter.triggerExecutionId.label"),
|
||||||
|
description: t("filter.triggerExecutionId.description"),
|
||||||
|
comparators: [
|
||||||
|
Comparators.EQUALS,
|
||||||
|
Comparators.NOT_EQUALS,
|
||||||
|
Comparators.CONTAINS,
|
||||||
|
Comparators.STARTS_WITH,
|
||||||
|
Comparators.ENDS_WITH
|
||||||
|
],
|
||||||
|
valueType: "text",
|
||||||
|
searchable: true
|
||||||
}
|
}
|
||||||
},
|
]
|
||||||
{
|
};
|
||||||
key: "timeRange",
|
});
|
||||||
label: t("filter.timeRange.label"),
|
};
|
||||||
description: t("filter.timeRange.description"),
|
|
||||||
comparators: [Comparators.EQUALS],
|
|
||||||
valueType: "select",
|
|
||||||
valueProvider: async () => {
|
|
||||||
const {VALUES} = useValues("executions");
|
|
||||||
return VALUES.RELATIVE_DATE;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "labels",
|
|
||||||
label: t("filter.labels_execution.label"),
|
|
||||||
description: t("filter.labels_execution.description"),
|
|
||||||
comparators: [Comparators.EQUALS, Comparators.NOT_EQUALS],
|
|
||||||
valueType: "text",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "triggerExecutionId",
|
|
||||||
label: t("filter.triggerExecutionId.label"),
|
|
||||||
description: t("filter.triggerExecutionId.description"),
|
|
||||||
comparators: [
|
|
||||||
Comparators.EQUALS,
|
|
||||||
Comparators.NOT_EQUALS,
|
|
||||||
Comparators.CONTAINS,
|
|
||||||
Comparators.STARTS_WITH,
|
|
||||||
Comparators.ENDS_WITH
|
|
||||||
],
|
|
||||||
valueType: "text",
|
|
||||||
searchable: true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
});
|
|
||||||
@@ -3,90 +3,92 @@ import {FilterConfiguration, Comparators} from "../utils/filterTypes";
|
|||||||
import {useValues} from "../composables/useValues";
|
import {useValues} from "../composables/useValues";
|
||||||
import {useI18n} from "vue-i18n";
|
import {useI18n} from "vue-i18n";
|
||||||
|
|
||||||
export const useFlowExecutionFilter = (): ComputedRef<FilterConfiguration> => computed(() => {
|
export const useFlowExecutionFilter = (): ComputedRef<FilterConfiguration> => {
|
||||||
const {t} = useI18n();
|
const {t} = useI18n();
|
||||||
|
|
||||||
return {
|
return computed(() => {
|
||||||
title: t("filter.titles.flow_execution_filters"),
|
return {
|
||||||
searchPlaceholder: t("filter.search_placeholders.search_executions"),
|
title: t("filter.titles.flow_execution_filters"),
|
||||||
keys: [
|
searchPlaceholder: t("filter.search_placeholders.search_executions"),
|
||||||
{
|
keys: [
|
||||||
key: "state",
|
{
|
||||||
label: t("filter.state.label"),
|
key: "state",
|
||||||
description: t("filter.state.description"),
|
label: t("filter.state.label"),
|
||||||
comparators: [Comparators.IN, Comparators.NOT_IN],
|
description: t("filter.state.description"),
|
||||||
valueType: "multi-select",
|
comparators: [Comparators.IN, Comparators.NOT_IN],
|
||||||
valueProvider: async () => {
|
valueType: "multi-select",
|
||||||
const {VALUES} = useValues("executions");
|
valueProvider: async () => {
|
||||||
return VALUES.EXECUTION_STATES;
|
const {VALUES} = useValues("executions");
|
||||||
}
|
return VALUES.EXECUTION_STATES;
|
||||||
},
|
}
|
||||||
{
|
|
||||||
key: "scope",
|
|
||||||
label: t("filter.scope.label"),
|
|
||||||
description: t("filter.scope.description"),
|
|
||||||
comparators: [Comparators.EQUALS, Comparators.NOT_EQUALS],
|
|
||||||
valueType: "radio",
|
|
||||||
valueProvider: async () => {
|
|
||||||
const {VALUES} = useValues("executions");
|
|
||||||
return VALUES.SCOPES;
|
|
||||||
},
|
},
|
||||||
showComparatorSelection: false
|
{
|
||||||
},
|
key: "scope",
|
||||||
{
|
label: t("filter.scope.label"),
|
||||||
key: "childFilter",
|
description: t("filter.scope.description"),
|
||||||
label: t("filter.childFilter.label"),
|
comparators: [Comparators.EQUALS, Comparators.NOT_EQUALS],
|
||||||
description: t("filter.childFilter.description"),
|
valueType: "radio",
|
||||||
comparators: [Comparators.EQUALS],
|
valueProvider: async () => {
|
||||||
valueType: "radio",
|
const {VALUES} = useValues("executions");
|
||||||
valueProvider: async () => {
|
return VALUES.SCOPES;
|
||||||
const {VALUES} = useValues("executions");
|
},
|
||||||
return VALUES.CHILDS;
|
showComparatorSelection: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "childFilter",
|
||||||
|
label: t("filter.childFilter.label"),
|
||||||
|
description: t("filter.childFilter.description"),
|
||||||
|
comparators: [Comparators.EQUALS],
|
||||||
|
valueType: "radio",
|
||||||
|
valueProvider: async () => {
|
||||||
|
const {VALUES} = useValues("executions");
|
||||||
|
return VALUES.CHILDS;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "kind",
|
||||||
|
label: t("filter.kind.label"),
|
||||||
|
description: t("filter.kind.description"),
|
||||||
|
comparators: [Comparators.EQUALS],
|
||||||
|
valueType: "radio",
|
||||||
|
valueProvider: async () => {
|
||||||
|
const {VALUES} = useValues("executions");
|
||||||
|
return VALUES.KINDS;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "timeRange",
|
||||||
|
label: t("filter.timeRange.label"),
|
||||||
|
description: t("filter.timeRange.description"),
|
||||||
|
comparators: [Comparators.EQUALS],
|
||||||
|
valueType: "select",
|
||||||
|
valueProvider: async () => {
|
||||||
|
const {VALUES} = useValues("executions");
|
||||||
|
return VALUES.RELATIVE_DATE;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "labels",
|
||||||
|
label: t("filter.labels_execution.label"),
|
||||||
|
description: t("filter.labels_execution.description"),
|
||||||
|
comparators: [Comparators.EQUALS, Comparators.NOT_EQUALS],
|
||||||
|
valueType: "text",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "triggerExecutionId",
|
||||||
|
label: t("filter.triggerExecutionId.label"),
|
||||||
|
description: t("filter.triggerExecutionId.description"),
|
||||||
|
comparators: [
|
||||||
|
Comparators.EQUALS,
|
||||||
|
Comparators.NOT_EQUALS,
|
||||||
|
Comparators.CONTAINS,
|
||||||
|
Comparators.STARTS_WITH,
|
||||||
|
Comparators.ENDS_WITH
|
||||||
|
],
|
||||||
|
valueType: "text",
|
||||||
|
searchable: true
|
||||||
}
|
}
|
||||||
},
|
]
|
||||||
{
|
};
|
||||||
key: "kind",
|
});
|
||||||
label: t("filter.kind.label"),
|
};
|
||||||
description: t("filter.kind.description"),
|
|
||||||
comparators: [Comparators.EQUALS],
|
|
||||||
valueType: "radio",
|
|
||||||
valueProvider: async () => {
|
|
||||||
const {VALUES} = useValues("executions");
|
|
||||||
return VALUES.KINDS;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "timeRange",
|
|
||||||
label: t("filter.timeRange.label"),
|
|
||||||
description: t("filter.timeRange.description"),
|
|
||||||
comparators: [Comparators.EQUALS],
|
|
||||||
valueType: "select",
|
|
||||||
valueProvider: async () => {
|
|
||||||
const {VALUES} = useValues("executions");
|
|
||||||
return VALUES.RELATIVE_DATE;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "labels",
|
|
||||||
label: t("filter.labels_execution.label"),
|
|
||||||
description: t("filter.labels_execution.description"),
|
|
||||||
comparators: [Comparators.EQUALS, Comparators.NOT_EQUALS],
|
|
||||||
valueType: "text",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "triggerExecutionId",
|
|
||||||
label: t("filter.triggerExecutionId.label"),
|
|
||||||
description: t("filter.triggerExecutionId.description"),
|
|
||||||
comparators: [
|
|
||||||
Comparators.EQUALS,
|
|
||||||
Comparators.NOT_EQUALS,
|
|
||||||
Comparators.CONTAINS,
|
|
||||||
Comparators.STARTS_WITH,
|
|
||||||
Comparators.ENDS_WITH
|
|
||||||
],
|
|
||||||
valueType: "text",
|
|
||||||
searchable: true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
});
|
|
||||||
@@ -6,45 +6,49 @@ import {useNamespacesStore} from "override/stores/namespaces";
|
|||||||
import {useAuthStore} from "override/stores/auth";
|
import {useAuthStore} from "override/stores/auth";
|
||||||
import {useValues} from "../composables/useValues";
|
import {useValues} from "../composables/useValues";
|
||||||
import {useI18n} from "vue-i18n";
|
import {useI18n} from "vue-i18n";
|
||||||
|
import {useRoute} from "vue-router";
|
||||||
|
|
||||||
export const useFlowFilter = (): ComputedRef<FilterConfiguration> => computed(() => {
|
export const useFlowFilter = (): ComputedRef<FilterConfiguration> => {
|
||||||
const {t} = useI18n();
|
const {t} = useI18n();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
return {
|
return computed(() => ({
|
||||||
title: t("filter.titles.flow_filters"),
|
title: t("filter.titles.flow_filters"),
|
||||||
searchPlaceholder: t("filter.search_placeholders.search_flows"),
|
searchPlaceholder: t("filter.search_placeholders.search_flows"),
|
||||||
keys: [
|
keys: [
|
||||||
{
|
...(route.name !== "namespaces/update" ? [
|
||||||
key: "namespace",
|
{
|
||||||
label: t("filter.namespace.label"),
|
key: "namespace",
|
||||||
description: t("filter.namespace.description"),
|
label: t("filter.namespace.label"),
|
||||||
comparators: [
|
description: t("filter.namespace.description"),
|
||||||
Comparators.IN,
|
comparators: [
|
||||||
Comparators.NOT_IN,
|
Comparators.IN,
|
||||||
Comparators.CONTAINS,
|
Comparators.NOT_IN,
|
||||||
Comparators.PREFIX,
|
Comparators.CONTAINS,
|
||||||
],
|
Comparators.PREFIX,
|
||||||
valueType: "multi-select",
|
],
|
||||||
valueProvider: async () => {
|
valueType: "multi-select" as const,
|
||||||
const user = useAuthStore().user;
|
valueProvider: async () => {
|
||||||
if (user && user.hasAnyActionOnAnyNamespace(permission.NAMESPACE, action.READ)) {
|
const user = useAuthStore().user;
|
||||||
const namespacesStore = useNamespacesStore();
|
if (user && user.hasAnyActionOnAnyNamespace(permission.NAMESPACE, action.READ)) {
|
||||||
const namespaces = (await namespacesStore.loadAutocomplete()) as string[];
|
const namespacesStore = useNamespacesStore();
|
||||||
return [...new Set(namespaces
|
const namespaces = (await namespacesStore.loadAutocomplete()) as string[];
|
||||||
.flatMap(namespace => {
|
return [...new Set(namespaces
|
||||||
return namespace.split(".").reduce((current: string[], part: string) => {
|
.flatMap(namespace => {
|
||||||
const previousCombination = current?.[current.length - 1];
|
return namespace.split(".").reduce((current: string[], part: string) => {
|
||||||
return [...current, `${(previousCombination ? previousCombination + "." : "")}${part}`];
|
const previousCombination = current?.[current.length - 1];
|
||||||
}, []);
|
return [...current, `${(previousCombination ? previousCombination + "." : "")}${part}`];
|
||||||
}))].map(namespace => ({
|
}, []);
|
||||||
label: namespace,
|
}))].map(namespace => ({
|
||||||
value: namespace
|
label: namespace,
|
||||||
}));
|
value: namespace
|
||||||
}
|
}));
|
||||||
return [];
|
}
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
searchable: true
|
||||||
},
|
},
|
||||||
searchable: true
|
] : []) as any,
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: "scope",
|
key: "scope",
|
||||||
label: t("filter.scope_flow.label"),
|
label: t("filter.scope_flow.label"),
|
||||||
@@ -65,5 +69,5 @@ export const useFlowFilter = (): ComputedRef<FilterConfiguration> => computed(()
|
|||||||
valueType: "text",
|
valueType: "text",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
};
|
}));
|
||||||
});
|
};
|
||||||
@@ -3,47 +3,53 @@ import {Comparators, FilterConfiguration} from "../utils/filterTypes";
|
|||||||
import {useI18n} from "vue-i18n";
|
import {useI18n} from "vue-i18n";
|
||||||
import {useNamespacesStore} from "override/stores/namespaces";
|
import {useNamespacesStore} from "override/stores/namespaces";
|
||||||
import {useAuthStore} from "override/stores/auth";
|
import {useAuthStore} from "override/stores/auth";
|
||||||
|
import {useRoute} from "vue-router";
|
||||||
import permission from "../../../models/permission";
|
import permission from "../../../models/permission";
|
||||||
import action from "../../../models/action";
|
import action from "../../../models/action";
|
||||||
|
|
||||||
export const useKvFilter = (): ComputedRef<FilterConfiguration> => computed(() => {
|
export const useKvFilter = (): ComputedRef<FilterConfiguration> => {
|
||||||
const {t} = useI18n();
|
const {t} = useI18n();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
return {
|
return computed(() => {
|
||||||
title: t("filter.titles.kv_filters"),
|
return {
|
||||||
searchPlaceholder: t("filter.search_placeholders.search_kv"),
|
title: t("filter.titles.kv_filters"),
|
||||||
keys: [
|
searchPlaceholder: t("filter.search_placeholders.search_kv"),
|
||||||
{
|
keys: [
|
||||||
key: "namespace",
|
...(route.name !== "namespaces/update" ? [
|
||||||
label: t("filter.namespace.label"),
|
{
|
||||||
description: t("filter.namespace.description"),
|
key: "namespace",
|
||||||
comparators: [
|
label: t("filter.namespace.label"),
|
||||||
Comparators.IN,
|
description: t("filter.namespace.description"),
|
||||||
Comparators.NOT_IN,
|
comparators: [
|
||||||
Comparators.CONTAINS,
|
Comparators.IN,
|
||||||
Comparators.PREFIX,
|
Comparators.NOT_IN,
|
||||||
],
|
Comparators.CONTAINS,
|
||||||
valueType: "multi-select",
|
Comparators.PREFIX,
|
||||||
valueProvider: async () => {
|
],
|
||||||
const user = useAuthStore().user;
|
valueType: "multi-select" as const,
|
||||||
if (user && user.hasAnyActionOnAnyNamespace(permission.NAMESPACE, action.READ)) {
|
valueProvider: async () => {
|
||||||
const namespacesStore = useNamespacesStore();
|
const user = useAuthStore().user;
|
||||||
const namespaces = (await namespacesStore.loadAutocomplete()) as string[];
|
if (user && user.hasAnyActionOnAnyNamespace(permission.NAMESPACE, action.READ)) {
|
||||||
return [...new Set(namespaces
|
const namespacesStore = useNamespacesStore();
|
||||||
.flatMap(namespace => {
|
const namespaces = (await namespacesStore.loadAutocomplete()) as string[];
|
||||||
return namespace.split(".").reduce((current: string[], part: string) => {
|
return [...new Set(namespaces
|
||||||
const previousCombination = current?.[current.length - 1];
|
.flatMap(namespace => {
|
||||||
return [...current, `${(previousCombination ? previousCombination + "." : "")}${part}`];
|
return namespace.split(".").reduce((current: string[], part: string) => {
|
||||||
}, []);
|
const previousCombination = current?.[current.length - 1];
|
||||||
}))].map(namespace => ({
|
return [...current, `${(previousCombination ? previousCombination + "." : "")}${part}`];
|
||||||
label: namespace,
|
}, []);
|
||||||
value: namespace
|
}))].map(namespace => ({
|
||||||
}));
|
label: namespace,
|
||||||
|
value: namespace
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
searchable: true
|
||||||
}
|
}
|
||||||
return [];
|
] : []) as any,
|
||||||
},
|
],
|
||||||
searchable: true
|
};
|
||||||
}
|
});
|
||||||
],
|
};
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -3,24 +3,26 @@ import {FilterConfiguration, Comparators} from "../utils/filterTypes";
|
|||||||
import {useValues} from "../composables/useValues";
|
import {useValues} from "../composables/useValues";
|
||||||
import {useI18n} from "vue-i18n";
|
import {useI18n} from "vue-i18n";
|
||||||
|
|
||||||
export const useLogExecutionsFilter = (): ComputedRef<FilterConfiguration> => computed(() => {
|
export const useLogExecutionsFilter = (): ComputedRef<FilterConfiguration> => {
|
||||||
const {t} = useI18n();
|
const {t} = useI18n();
|
||||||
|
|
||||||
return {
|
return computed(() => {
|
||||||
title: t("filter.titles.log_filters"),
|
return {
|
||||||
searchPlaceholder: t("filter.search_placeholders.search_logs"),
|
title: t("filter.titles.log_filters"),
|
||||||
keys: [
|
searchPlaceholder: t("filter.search_placeholders.search_logs"),
|
||||||
{
|
keys: [
|
||||||
key: "level",
|
{
|
||||||
label: t("filter.level.label"),
|
key: "level",
|
||||||
description: t("filter.level.description"),
|
label: t("filter.level.label"),
|
||||||
comparators: [Comparators.EQUALS, Comparators.NOT_EQUALS],
|
description: t("filter.level.description"),
|
||||||
valueType: "select",
|
comparators: [Comparators.EQUALS, Comparators.NOT_EQUALS],
|
||||||
valueProvider: async () => {
|
valueType: "select",
|
||||||
const {VALUES} = useValues("logs");
|
valueProvider: async () => {
|
||||||
return VALUES.LEVELS;
|
const {VALUES} = useValues("logs");
|
||||||
},
|
return VALUES.LEVELS;
|
||||||
}
|
},
|
||||||
]
|
}
|
||||||
};
|
]
|
||||||
});
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -6,108 +6,114 @@ import {useNamespacesStore} from "override/stores/namespaces";
|
|||||||
import {useAuthStore} from "override/stores/auth";
|
import {useAuthStore} from "override/stores/auth";
|
||||||
import {useValues} from "../composables/useValues";
|
import {useValues} from "../composables/useValues";
|
||||||
import {useI18n} from "vue-i18n";
|
import {useI18n} from "vue-i18n";
|
||||||
|
import {useRoute} from "vue-router";
|
||||||
|
|
||||||
export const useLogFilter = (): ComputedRef<FilterConfiguration> => computed(() => {
|
export const useLogFilter = (): ComputedRef<FilterConfiguration> => {
|
||||||
const {t} = useI18n();
|
const {t} = useI18n();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
return {
|
return computed(() => {
|
||||||
title: t("filter.titles.log_filters"),
|
return {
|
||||||
searchPlaceholder: t("filter.search_placeholders.search_logs"),
|
title: t("filter.titles.log_filters"),
|
||||||
keys: [
|
searchPlaceholder: t("filter.search_placeholders.search_logs"),
|
||||||
{
|
keys: [
|
||||||
key: "namespace",
|
...(route.name !== "namespaces/update" && route.name !== "flows/update" ? [
|
||||||
label: t("filter.namespace.label"),
|
{
|
||||||
description: t("filter.namespace.description"),
|
key: "namespace",
|
||||||
comparators: [
|
label: t("filter.namespace.label"),
|
||||||
Comparators.IN,
|
description: t("filter.namespace.description"),
|
||||||
Comparators.NOT_IN,
|
comparators: [
|
||||||
Comparators.CONTAINS,
|
Comparators.IN,
|
||||||
Comparators.PREFIX,
|
Comparators.NOT_IN,
|
||||||
],
|
Comparators.CONTAINS,
|
||||||
valueType: "multi-select",
|
Comparators.PREFIX,
|
||||||
valueProvider: async () => {
|
],
|
||||||
const user = useAuthStore().user;
|
valueType: "multi-select" as const,
|
||||||
if (user && user.hasAnyActionOnAnyNamespace(permission.NAMESPACE, action.READ)) {
|
valueProvider: async () => {
|
||||||
const namespacesStore = useNamespacesStore();
|
const user = useAuthStore().user;
|
||||||
const namespaces = (await namespacesStore.loadAutocomplete()) as string[];
|
if (user && user.hasAnyActionOnAnyNamespace(permission.NAMESPACE, action.READ)) {
|
||||||
return [...new Set(namespaces
|
const namespacesStore = useNamespacesStore();
|
||||||
.flatMap(namespace => {
|
const namespaces = (await namespacesStore.loadAutocomplete()) as string[];
|
||||||
return namespace.split(".").reduce((current: string[], part: string) => {
|
return [...new Set(namespaces
|
||||||
const previousCombination = current?.[current.length - 1];
|
.flatMap(namespace => {
|
||||||
return [...current, `${(previousCombination ? previousCombination + "." : "")}${part}`];
|
return namespace.split(".").reduce((current: string[], part: string) => {
|
||||||
}, []);
|
const previousCombination = current?.[current.length - 1];
|
||||||
}))].map(namespace => ({
|
return [...current, `${(previousCombination ? previousCombination + "." : "")}${part}`];
|
||||||
label: namespace,
|
}, []);
|
||||||
value: namespace
|
}))].map(namespace => ({
|
||||||
}));
|
label: namespace,
|
||||||
}
|
value: namespace
|
||||||
return [];
|
}));
|
||||||
},
|
}
|
||||||
searchable: true
|
return [];
|
||||||
},
|
},
|
||||||
{
|
searchable: true
|
||||||
key: "level",
|
},
|
||||||
label: t("filter.level.label"),
|
] : []) as any,
|
||||||
description: t("filter.level.description"),
|
{
|
||||||
comparators: [Comparators.EQUALS, Comparators.NOT_EQUALS],
|
key: "level",
|
||||||
valueType: "select",
|
label: t("filter.level.label"),
|
||||||
valueProvider: async () => {
|
description: t("filter.level.description"),
|
||||||
const {VALUES} = useValues("logs");
|
comparators: [Comparators.EQUALS, Comparators.NOT_EQUALS],
|
||||||
return VALUES.LEVELS;
|
valueType: "select",
|
||||||
},
|
valueProvider: async () => {
|
||||||
showComparatorSelection: true
|
const {VALUES} = useValues("logs");
|
||||||
},
|
return VALUES.LEVELS;
|
||||||
{
|
},
|
||||||
key: "timeRange",
|
showComparatorSelection: true
|
||||||
label: t("filter.timeRange_log.label"),
|
},
|
||||||
description: t("filter.timeRange_log.description"),
|
{
|
||||||
comparators: [Comparators.EQUALS],
|
key: "timeRange",
|
||||||
valueType: "select",
|
label: t("filter.timeRange_log.label"),
|
||||||
valueProvider: async () => {
|
description: t("filter.timeRange_log.description"),
|
||||||
const {VALUES} = useValues("logs");
|
comparators: [Comparators.EQUALS],
|
||||||
return VALUES.RELATIVE_DATE;
|
valueType: "select",
|
||||||
}
|
valueProvider: async () => {
|
||||||
},
|
const {VALUES} = useValues("logs");
|
||||||
{
|
return VALUES.RELATIVE_DATE;
|
||||||
key: "scope",
|
}
|
||||||
label: t("filter.scope_log.label"),
|
},
|
||||||
description: t("filter.scope_log.description"),
|
{
|
||||||
comparators: [Comparators.EQUALS, Comparators.NOT_EQUALS],
|
key: "scope",
|
||||||
valueType: "radio",
|
label: t("filter.scope_log.label"),
|
||||||
valueProvider: async () => {
|
description: t("filter.scope_log.description"),
|
||||||
const {VALUES} = useValues("logs");
|
comparators: [Comparators.EQUALS, Comparators.NOT_EQUALS],
|
||||||
return VALUES.SCOPES;
|
valueType: "radio",
|
||||||
},
|
valueProvider: async () => {
|
||||||
showComparatorSelection: false
|
const {VALUES} = useValues("logs");
|
||||||
},
|
return VALUES.SCOPES;
|
||||||
{
|
},
|
||||||
key: "triggerId",
|
showComparatorSelection: false
|
||||||
label: t("filter.triggerId.label"),
|
},
|
||||||
description: t("filter.triggerId.description"),
|
{
|
||||||
comparators: [
|
key: "triggerId",
|
||||||
// Comparators.IN,
|
label: t("filter.triggerId.label"),
|
||||||
// Comparators.NOT_IN,
|
description: t("filter.triggerId.description"),
|
||||||
Comparators.EQUALS,
|
comparators: [
|
||||||
Comparators.NOT_EQUALS,
|
// Comparators.IN,
|
||||||
Comparators.CONTAINS,
|
// Comparators.NOT_IN,
|
||||||
Comparators.STARTS_WITH,
|
Comparators.EQUALS,
|
||||||
Comparators.ENDS_WITH
|
Comparators.NOT_EQUALS,
|
||||||
],
|
Comparators.CONTAINS,
|
||||||
valueType: "text",
|
Comparators.STARTS_WITH,
|
||||||
},
|
Comparators.ENDS_WITH
|
||||||
{
|
],
|
||||||
key: "flowId",
|
valueType: "text",
|
||||||
label: t("filter.flowId.label"),
|
},
|
||||||
description: t("filter.flowId.description"),
|
...(route.name !== "flows/update" ? [{
|
||||||
comparators: [
|
key: "flowId",
|
||||||
Comparators.EQUALS,
|
label: t("filter.flowId.label"),
|
||||||
Comparators.NOT_EQUALS,
|
description: t("filter.flowId.description"),
|
||||||
Comparators.CONTAINS,
|
comparators: [
|
||||||
Comparators.STARTS_WITH,
|
Comparators.EQUALS,
|
||||||
Comparators.ENDS_WITH,
|
Comparators.NOT_EQUALS,
|
||||||
],
|
Comparators.CONTAINS,
|
||||||
valueType: "text",
|
Comparators.STARTS_WITH,
|
||||||
},
|
Comparators.ENDS_WITH,
|
||||||
]
|
],
|
||||||
};
|
valueType: "text",
|
||||||
});
|
}] : []) as any,
|
||||||
|
]
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -5,94 +5,98 @@ import {useFlowStore} from "../../../stores/flow";
|
|||||||
import {useI18n} from "vue-i18n";
|
import {useI18n} from "vue-i18n";
|
||||||
import {useExecutionsStore} from "../../../stores/executions";
|
import {useExecutionsStore} from "../../../stores/executions";
|
||||||
|
|
||||||
export const useMetricFilter = (): ComputedRef<FilterConfiguration> => computed(() => {
|
export const useMetricFilter = (): ComputedRef<FilterConfiguration> => {
|
||||||
const {t} = useI18n();
|
const {t} = useI18n();
|
||||||
|
|
||||||
return {
|
return computed(() => {
|
||||||
title: t("filter.titles.metric_filters"),
|
return {
|
||||||
searchPlaceholder: t("filter.search_placeholders.search_metrics"),
|
title: t("filter.titles.metric_filters"),
|
||||||
keys: [
|
searchPlaceholder: t("filter.search_placeholders.search_metrics"),
|
||||||
{
|
keys: [
|
||||||
key: "metric",
|
{
|
||||||
label: t("filter.metric.label"),
|
key: "metric",
|
||||||
description: t("filter.metric.description"),
|
label: t("filter.metric.label"),
|
||||||
comparators: [Comparators.EQUALS],
|
description: t("filter.metric.description"),
|
||||||
valueType: "select",
|
comparators: [Comparators.EQUALS],
|
||||||
valueProvider: async () => {
|
valueType: "select",
|
||||||
const executionsStore = useExecutionsStore();
|
valueProvider: async () => {
|
||||||
const taskRuns = executionsStore.execution?.taskRunList ?? [];
|
const executionsStore = useExecutionsStore();
|
||||||
return taskRuns.map(taskRun => ({
|
const taskRuns = executionsStore.execution?.taskRunList ?? [];
|
||||||
label: taskRun.taskId + (taskRun.value ? ` - ${taskRun.value}` : ""),
|
return taskRuns.map(taskRun => ({
|
||||||
value: taskRun.id
|
label: taskRun.taskId + (taskRun.value ? ` - ${taskRun.value}` : ""),
|
||||||
}));
|
value: taskRun.id
|
||||||
},
|
}));
|
||||||
searchable: true
|
},
|
||||||
}
|
searchable: true
|
||||||
]
|
}
|
||||||
};
|
]
|
||||||
});
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const useFlowMetricFilter = (): ComputedRef<FilterConfiguration> => computed(() => {
|
export const useFlowMetricFilter = (): ComputedRef<FilterConfiguration> => {
|
||||||
const {t} = useI18n();
|
const {t} = useI18n();
|
||||||
|
|
||||||
return {
|
return computed(() => {
|
||||||
title: t("filter.titles.flow_metric_filters"),
|
return {
|
||||||
searchPlaceholder: t("filter.search_placeholders.search_metrics"),
|
title: t("filter.titles.flow_metric_filters"),
|
||||||
keys: [
|
searchPlaceholder: t("filter.search_placeholders.search_metrics"),
|
||||||
{
|
keys: [
|
||||||
key: "task",
|
{
|
||||||
label: t("filter.task.label"),
|
key: "task",
|
||||||
description: t("filter.task.description"),
|
label: t("filter.task.label"),
|
||||||
comparators: [
|
description: t("filter.task.description"),
|
||||||
Comparators.EQUALS,
|
comparators: [
|
||||||
],
|
Comparators.EQUALS,
|
||||||
valueType: "select",
|
],
|
||||||
valueProvider: async () => {
|
valueType: "select",
|
||||||
return (useFlowStore().tasksWithMetrics as string[]).map((value) => ({
|
valueProvider: async () => {
|
||||||
label: value,
|
return (useFlowStore().tasksWithMetrics as string[]).map((value) => ({
|
||||||
value
|
label: value,
|
||||||
}));
|
value
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
searchable: true
|
||||||
},
|
},
|
||||||
searchable: true
|
{
|
||||||
},
|
key: "metric",
|
||||||
{
|
label: t("filter.metric.label"),
|
||||||
key: "metric",
|
description: t("filter.metric.description"),
|
||||||
label: t("filter.metric.label"),
|
comparators: [
|
||||||
description: t("filter.metric.description"),
|
Comparators.EQUALS
|
||||||
comparators: [
|
],
|
||||||
Comparators.EQUALS
|
valueType: "select",
|
||||||
],
|
valueProvider: async () => {
|
||||||
valueType: "select",
|
return (useFlowStore().metrics as string[]).map((value) => ({
|
||||||
valueProvider: async () => {
|
label: value,
|
||||||
return (useFlowStore().metrics as string[]).map((value) => ({
|
value
|
||||||
label: value,
|
}));
|
||||||
value
|
},
|
||||||
}));
|
searchable: true
|
||||||
},
|
},
|
||||||
searchable: true
|
{
|
||||||
},
|
key: "aggregation",
|
||||||
{
|
label: t("filter.aggregation.label"),
|
||||||
key: "aggregation",
|
description: t("filter.aggregation.description"),
|
||||||
label: t("filter.aggregation.label"),
|
comparators: [Comparators.EQUALS],
|
||||||
description: t("filter.aggregation.description"),
|
valueType: "select",
|
||||||
comparators: [Comparators.EQUALS],
|
valueProvider: async () => {
|
||||||
valueType: "select",
|
const {VALUES} = useValues("metrics");
|
||||||
valueProvider: async () => {
|
return [...VALUES.AGGREGATIONS, {label: "Count", value: "COUNT"}];
|
||||||
const {VALUES} = useValues("metrics");
|
}
|
||||||
return [...VALUES.AGGREGATIONS, {label: "Count", value: "COUNT"}];
|
},
|
||||||
|
{
|
||||||
|
key: "timeRange",
|
||||||
|
label: t("filter.timeRange_metric.label"),
|
||||||
|
description: t("filter.timeRange_metric.description"),
|
||||||
|
comparators: [Comparators.EQUALS],
|
||||||
|
valueType: "select",
|
||||||
|
valueProvider: async () => {
|
||||||
|
const {VALUES} = useValues("metrics");
|
||||||
|
return VALUES.RELATIVE_DATE;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
]
|
||||||
{
|
};
|
||||||
key: "timeRange",
|
});
|
||||||
label: t("filter.timeRange_metric.label"),
|
};
|
||||||
description: t("filter.timeRange_metric.description"),
|
|
||||||
comparators: [Comparators.EQUALS],
|
|
||||||
valueType: "select",
|
|
||||||
valueProvider: async () => {
|
|
||||||
const {VALUES} = useValues("metrics");
|
|
||||||
return VALUES.RELATIVE_DATE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
});
|
|
||||||
@@ -2,12 +2,14 @@ import {computed, ComputedRef} from "vue";
|
|||||||
import {FilterConfiguration} from "../../../components/filter/utils/filterTypes";
|
import {FilterConfiguration} from "../../../components/filter/utils/filterTypes";
|
||||||
import {useI18n} from "vue-i18n";
|
import {useI18n} from "vue-i18n";
|
||||||
|
|
||||||
export const useNamespacesFilter = (): ComputedRef<FilterConfiguration> => computed(() => {
|
export const useNamespacesFilter = (): ComputedRef<FilterConfiguration> => {
|
||||||
const {t} = useI18n();
|
const {t} = useI18n();
|
||||||
|
|
||||||
return {
|
return computed(() => {
|
||||||
title: t("filter.titles.namespaces_filters"),
|
return {
|
||||||
searchPlaceholder: t("filter.search_placeholders.search_namespaces"),
|
title: t("filter.titles.namespace_filters"),
|
||||||
keys: [],
|
searchPlaceholder: t("filter.search_placeholders.search_namespaces"),
|
||||||
};
|
keys: [],
|
||||||
});
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ import {computed, ComputedRef} from "vue";
|
|||||||
import {FilterConfiguration} from "../../../components/filter/utils/filterTypes";
|
import {FilterConfiguration} from "../../../components/filter/utils/filterTypes";
|
||||||
import {useI18n} from "vue-i18n";
|
import {useI18n} from "vue-i18n";
|
||||||
|
|
||||||
export const usePluginFilter = (): ComputedRef<FilterConfiguration> => computed(() => {
|
export const usePluginFilter = (): ComputedRef<FilterConfiguration> => {
|
||||||
const {t} = useI18n();
|
const {t} = useI18n();
|
||||||
|
|
||||||
return {
|
return computed(() => {
|
||||||
title: t("filter.titles.plugin_filters"),
|
return {
|
||||||
searchPlaceholder: t("filter.search_placeholders.search_plugins", {count: 900}),
|
title: t("filter.titles.plugin_filters"),
|
||||||
keys: [],
|
searchPlaceholder: t("filter.search_placeholders.search_plugins", {count: 900}),
|
||||||
};
|
keys: [],
|
||||||
});
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -5,45 +5,51 @@ import action from "../../../models/action";
|
|||||||
import {useNamespacesStore} from "override/stores/namespaces";
|
import {useNamespacesStore} from "override/stores/namespaces";
|
||||||
import {useAuthStore} from "override/stores/auth";
|
import {useAuthStore} from "override/stores/auth";
|
||||||
import {useI18n} from "vue-i18n";
|
import {useI18n} from "vue-i18n";
|
||||||
|
import {useRoute} from "vue-router";
|
||||||
|
|
||||||
export const useSecretsFilter = (): ComputedRef<FilterConfiguration> => computed(() => {
|
export const useSecretsFilter = (): ComputedRef<FilterConfiguration> => {
|
||||||
const {t} = useI18n();
|
const {t} = useI18n();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
return {
|
return computed(() => {
|
||||||
title: t("filter.titles.secret_filters"),
|
return {
|
||||||
searchPlaceholder: t("filter.search_placeholders.search_secrets"),
|
title: t("filter.titles.secret_filters"),
|
||||||
keys: [
|
searchPlaceholder: t("filter.search_placeholders.search_secrets"),
|
||||||
{
|
keys: [
|
||||||
key: "namespace",
|
...(route.name !== "namespaces/update" ? [
|
||||||
label: t("filter.namespace.label"),
|
{
|
||||||
description: t("filter.namespace.description"),
|
key: "namespace",
|
||||||
comparators: [
|
label: t("filter.namespace.label"),
|
||||||
Comparators.IN,
|
description: t("filter.namespace.description"),
|
||||||
Comparators.NOT_IN,
|
comparators: [
|
||||||
Comparators.CONTAINS,
|
Comparators.IN,
|
||||||
Comparators.PREFIX,
|
Comparators.NOT_IN,
|
||||||
],
|
Comparators.CONTAINS,
|
||||||
valueType: "multi-select",
|
Comparators.PREFIX,
|
||||||
valueProvider: async () => {
|
],
|
||||||
const user = useAuthStore().user;
|
valueType: "multi-select",
|
||||||
if (user && user.hasAnyActionOnAnyNamespace(permission.NAMESPACE, action.READ)) {
|
valueProvider: async () => {
|
||||||
const namespacesStore = useNamespacesStore();
|
const user = useAuthStore().user;
|
||||||
const namespaces = (await namespacesStore.loadAutocomplete()) as string[];
|
if (user && user.hasAnyActionOnAnyNamespace(permission.NAMESPACE, action.READ)) {
|
||||||
return [...new Set(namespaces
|
const namespacesStore = useNamespacesStore();
|
||||||
.flatMap(namespace => {
|
const namespaces = (await namespacesStore.loadAutocomplete()) as string[];
|
||||||
return namespace.split(".").reduce((current: string[], part: string) => {
|
return [...new Set(namespaces
|
||||||
const previousCombination = current?.[current.length - 1];
|
.flatMap(namespace => {
|
||||||
return [...current, `${(previousCombination ? previousCombination + "." : "")}${part}`];
|
return namespace.split(".").reduce((current: string[], part: string) => {
|
||||||
}, []);
|
const previousCombination = current?.[current.length - 1];
|
||||||
}))].map(namespace => ({
|
return [...current, `${(previousCombination ? previousCombination + "." : "")}${part}`];
|
||||||
label: namespace,
|
}, []);
|
||||||
value: namespace
|
}))].map(namespace => ({
|
||||||
}));
|
label: namespace,
|
||||||
}
|
value: namespace
|
||||||
return [];
|
}));
|
||||||
},
|
}
|
||||||
searchable: true
|
return [];
|
||||||
},
|
},
|
||||||
],
|
searchable: true
|
||||||
};
|
},
|
||||||
});
|
] : []) as any,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -6,113 +6,118 @@ import {useNamespacesStore} from "override/stores/namespaces";
|
|||||||
import {useAuthStore} from "override/stores/auth";
|
import {useAuthStore} from "override/stores/auth";
|
||||||
import {useValues} from "../composables/useValues";
|
import {useValues} from "../composables/useValues";
|
||||||
import {useI18n} from "vue-i18n";
|
import {useI18n} from "vue-i18n";
|
||||||
|
import {useRoute} from "vue-router";
|
||||||
|
|
||||||
export const useTriggerFilter = (): ComputedRef<FilterConfiguration> => computed(() => {
|
export const useTriggerFilter = (): ComputedRef<FilterConfiguration> => {
|
||||||
const {t} = useI18n();
|
const {t} = useI18n();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
return {
|
return computed(() => {
|
||||||
title: t("filter.titles.trigger_filters"),
|
return {
|
||||||
searchPlaceholder: t("filter.search_placeholders.search_triggers"),
|
title: t("filter.titles.trigger_filters"),
|
||||||
keys: [
|
searchPlaceholder: t("filter.search_placeholders.search_triggers"),
|
||||||
{
|
keys: [
|
||||||
key: "namespace",
|
...(route.name !== "namespaces/update" ? [
|
||||||
label: t("filter.namespace.label"),
|
{
|
||||||
description: t("filter.namespace.description"),
|
key: "namespace",
|
||||||
comparators: [
|
label: t("filter.namespace.label"),
|
||||||
Comparators.IN,
|
description: t("filter.namespace.description"),
|
||||||
Comparators.NOT_IN,
|
comparators: [
|
||||||
Comparators.CONTAINS,
|
Comparators.IN,
|
||||||
Comparators.PREFIX,
|
Comparators.NOT_IN,
|
||||||
],
|
Comparators.CONTAINS,
|
||||||
valueType: "multi-select",
|
Comparators.PREFIX,
|
||||||
valueProvider: async () => {
|
],
|
||||||
const user = useAuthStore().user;
|
valueType: "multi-select" as const,
|
||||||
if (user && user.hasAnyActionOnAnyNamespace(permission.NAMESPACE, action.READ)) {
|
valueProvider: async () => {
|
||||||
const namespacesStore = useNamespacesStore();
|
const user = useAuthStore().user;
|
||||||
const namespaces = (await namespacesStore.loadAutocomplete()) as string[];
|
if (user && user.hasAnyActionOnAnyNamespace(permission.NAMESPACE, action.READ)) {
|
||||||
return [...new Set(namespaces
|
const namespacesStore = useNamespacesStore();
|
||||||
.flatMap(namespace => {
|
const namespaces = (await namespacesStore.loadAutocomplete()) as string[];
|
||||||
return namespace.split(".").reduce((current: string[], part: string) => {
|
return [...new Set(namespaces
|
||||||
const previousCombination = current?.[current.length - 1];
|
.flatMap(namespace => {
|
||||||
return [...current, `${(previousCombination ? previousCombination + "." : "")}${part}`];
|
return namespace.split(".").reduce((current: string[], part: string) => {
|
||||||
}, []);
|
const previousCombination = current?.[current.length - 1];
|
||||||
}))].map(namespace => ({
|
return [...current, `${(previousCombination ? previousCombination + "." : "")}${part}`];
|
||||||
label: namespace,
|
}, []);
|
||||||
value: namespace
|
}))].map(namespace => ({
|
||||||
}));
|
label: namespace,
|
||||||
|
value: namespace
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
searchable: true
|
||||||
|
},
|
||||||
|
] : []) as any,
|
||||||
|
...(route.name !== "flows/update" ? [{
|
||||||
|
key: "flowId",
|
||||||
|
label: t("filter.flowId.label"),
|
||||||
|
description: t("filter.flowId.description"),
|
||||||
|
comparators: [
|
||||||
|
Comparators.EQUALS,
|
||||||
|
Comparators.NOT_EQUALS,
|
||||||
|
Comparators.CONTAINS,
|
||||||
|
Comparators.STARTS_WITH,
|
||||||
|
Comparators.ENDS_WITH,
|
||||||
|
],
|
||||||
|
valueType: "text",
|
||||||
|
}] : []) as any,
|
||||||
|
{
|
||||||
|
key: "timeRange",
|
||||||
|
label: t("filter.timeRange_trigger.label"),
|
||||||
|
description: t("filter.timeRange_trigger.description"),
|
||||||
|
comparators: [Comparators.EQUALS],
|
||||||
|
valueType: "select",
|
||||||
|
valueProvider: async () => {
|
||||||
|
const {VALUES} = useValues("triggers");
|
||||||
|
return VALUES.RELATIVE_DATE;
|
||||||
}
|
}
|
||||||
return [];
|
|
||||||
},
|
},
|
||||||
searchable: true
|
{
|
||||||
},
|
key: "scope",
|
||||||
{
|
label: t("filter.scope_trigger.label"),
|
||||||
key: "flowId",
|
description: t("filter.scope_trigger.description"),
|
||||||
label: t("filter.flowId.label"),
|
comparators: [Comparators.EQUALS, Comparators.NOT_EQUALS],
|
||||||
description: t("filter.flowId.description"),
|
valueType: "radio",
|
||||||
comparators: [
|
valueProvider: async () => {
|
||||||
Comparators.EQUALS,
|
const {VALUES} = useValues("triggers");
|
||||||
Comparators.NOT_EQUALS,
|
return VALUES.SCOPES;
|
||||||
Comparators.CONTAINS,
|
},
|
||||||
Comparators.STARTS_WITH,
|
showComparatorSelection: false
|
||||||
Comparators.ENDS_WITH,
|
},
|
||||||
],
|
{
|
||||||
valueType: "text",
|
key: "triggerId",
|
||||||
},
|
label: t("filter.triggerId_trigger.label"),
|
||||||
{
|
description: t("filter.triggerId_trigger.description"),
|
||||||
key: "timeRange",
|
comparators: [
|
||||||
label: t("filter.timeRange_trigger.label"),
|
Comparators.IN,
|
||||||
description: t("filter.timeRange_trigger.description"),
|
Comparators.NOT_IN,
|
||||||
comparators: [Comparators.EQUALS],
|
Comparators.EQUALS,
|
||||||
valueType: "select",
|
Comparators.NOT_EQUALS,
|
||||||
valueProvider: async () => {
|
Comparators.CONTAINS,
|
||||||
const {VALUES} = useValues("triggers");
|
Comparators.STARTS_WITH,
|
||||||
return VALUES.RELATIVE_DATE;
|
Comparators.ENDS_WITH
|
||||||
|
],
|
||||||
|
valueType: "text",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "workerId",
|
||||||
|
label: t("filter.workerId.label"),
|
||||||
|
description: t("filter.workerId.description"),
|
||||||
|
comparators: [
|
||||||
|
Comparators.IN,
|
||||||
|
Comparators.NOT_IN,
|
||||||
|
Comparators.EQUALS,
|
||||||
|
Comparators.NOT_EQUALS,
|
||||||
|
Comparators.CONTAINS,
|
||||||
|
Comparators.STARTS_WITH,
|
||||||
|
Comparators.ENDS_WITH
|
||||||
|
],
|
||||||
|
valueType: "text",
|
||||||
|
searchable: true,
|
||||||
}
|
}
|
||||||
},
|
]
|
||||||
{
|
};
|
||||||
key: "scope",
|
});
|
||||||
label: t("filter.scope_trigger.label"),
|
};
|
||||||
description: t("filter.scope_trigger.description"),
|
|
||||||
comparators: [Comparators.EQUALS, Comparators.NOT_EQUALS],
|
|
||||||
valueType: "radio",
|
|
||||||
valueProvider: async () => {
|
|
||||||
const {VALUES} = useValues("triggers");
|
|
||||||
return VALUES.SCOPES;
|
|
||||||
},
|
|
||||||
showComparatorSelection: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "triggerId",
|
|
||||||
label: t("filter.triggerId_trigger.label"),
|
|
||||||
description: t("filter.triggerId_trigger.description"),
|
|
||||||
comparators: [
|
|
||||||
Comparators.IN,
|
|
||||||
Comparators.NOT_IN,
|
|
||||||
Comparators.EQUALS,
|
|
||||||
Comparators.NOT_EQUALS,
|
|
||||||
Comparators.CONTAINS,
|
|
||||||
Comparators.STARTS_WITH,
|
|
||||||
Comparators.ENDS_WITH
|
|
||||||
],
|
|
||||||
valueType: "text",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "workerId",
|
|
||||||
label: t("filter.workerId.label"),
|
|
||||||
description: t("filter.workerId.description"),
|
|
||||||
comparators: [
|
|
||||||
Comparators.IN,
|
|
||||||
Comparators.NOT_IN,
|
|
||||||
Comparators.EQUALS,
|
|
||||||
Comparators.NOT_EQUALS,
|
|
||||||
Comparators.CONTAINS,
|
|
||||||
Comparators.STARTS_WITH,
|
|
||||||
Comparators.ENDS_WITH
|
|
||||||
],
|
|
||||||
valueType: "text",
|
|
||||||
// valueProvider: async () => {},
|
|
||||||
searchable: true,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
});
|
|
||||||
@@ -39,7 +39,7 @@ export const decodeSearchParams = (query: LocationQuery) =>
|
|||||||
operation
|
operation
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter(Boolean);
|
.filter(v => v !== null);
|
||||||
|
|
||||||
type Filter = Pick<AppliedFilter, "key" | "comparator" | "value">;
|
type Filter = Pick<AppliedFilter, "key" | "comparator" | "value">;
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
:namespace="flowStore.flow?.namespace"
|
:namespace="flowStore.flow?.namespace"
|
||||||
:flowId="flowStore.flow?.id"
|
:flowId="flowStore.flow?.id"
|
||||||
:topbar="false"
|
:topbar="false"
|
||||||
:restoreUrl="false"
|
:defaultScopeFilter="false"
|
||||||
filter
|
filter
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
refresh: {shown: true, callback: load}
|
refresh: {shown: true, callback: load}
|
||||||
}"
|
}"
|
||||||
legacyQuery
|
legacyQuery
|
||||||
|
:defaultScope="false"
|
||||||
|
:defaultTimeRange="false"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div v-bind="$attrs" v-loading="isLoading">
|
<div v-bind="$attrs" v-loading="isLoading">
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
import FlowRootTopBar from "./FlowRootTopBar.vue";
|
import FlowRootTopBar from "./FlowRootTopBar.vue";
|
||||||
import FlowConcurrency from "./FlowConcurrency.vue";
|
import FlowConcurrency from "./FlowConcurrency.vue";
|
||||||
import DemoAuditLogs from "../demo/AuditLogs.vue";
|
import DemoAuditLogs from "../demo/AuditLogs.vue";
|
||||||
import {useAuthStore} from "override/stores/auth"
|
import {useAuthStore} from "override/stores/auth";
|
||||||
import {useMiscStore} from "override/stores/misc";
|
import {useMiscStore} from "override/stores/misc";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -59,13 +59,12 @@
|
|||||||
"$route.params.tab": {
|
"$route.params.tab": {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
handler: function (newTab) {
|
handler: function (newTab) {
|
||||||
if (newTab === "overview") {
|
if (newTab === "overview" || newTab === "executions") {
|
||||||
const dateTimeKeys = ["startDate", "endDate", "timeRange"];
|
const dateTimeKeys = ["startDate", "endDate", "timeRange"];
|
||||||
|
|
||||||
if (!Object.keys(this.$route.query).some((key) => dateTimeKeys.some((dateTimeKey) => key.includes(dateTimeKey)))) {
|
if (!Object.keys(this.$route.query).some((key) => dateTimeKeys.some((dateTimeKey) => key.includes(dateTimeKey)))) {
|
||||||
const miscStore = useMiscStore();
|
const DEFAULT_DURATION = this.miscStore.configs?.chartDefaultDuration ?? "P30D";
|
||||||
const defaultDuration = miscStore.configs?.chartDefaultDuration || "P30D";
|
const newQuery = {...this.$route.query, "filters[timeRange][EQUALS]": DEFAULT_DURATION};
|
||||||
const newQuery = {...this.$route.query, "filters[timeRange][EQUALS]": defaultDuration};
|
|
||||||
this.$router.replace({name: this.$route.name, params: this.$route.params, query: newQuery});
|
this.$router.replace({name: this.$route.name, params: this.$route.params, query: newQuery});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -314,7 +313,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapStores(useCoreStore, useFlowStore, useAuthStore),
|
...mapStores(useCoreStore, useFlowStore, useAuthStore, useMiscStore),
|
||||||
routeInfo() {
|
routeInfo() {
|
||||||
return {
|
return {
|
||||||
title: this.$route.params.id,
|
title: this.$route.params.id,
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
@update-properties="updateDisplayColumns"
|
@update-properties="updateDisplayColumns"
|
||||||
legacyQuery
|
legacyQuery
|
||||||
readOnly
|
readOnly
|
||||||
|
:defaultScope="false"
|
||||||
|
:defaultTimeRange="false"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<el-table
|
<el-table
|
||||||
@@ -472,7 +474,7 @@
|
|||||||
backfill: cleanBackfill.value
|
backfill: cleanBackfill.value
|
||||||
})
|
})
|
||||||
.then((newTrigger: any) => {
|
.then((newTrigger: any) => {
|
||||||
(window as any).$toast().saved(newTrigger.id);
|
toast.saved(newTrigger.triggerId);
|
||||||
triggers.value = triggers.value.map((t: any) => {
|
triggers.value = triggers.value.map((t: any) => {
|
||||||
if (t.id === newTrigger.id) {
|
if (t.id === newTrigger.id) {
|
||||||
return newTrigger
|
return newTrigger
|
||||||
@@ -493,7 +495,7 @@
|
|||||||
const pauseBackfill = (trigger: any) => {
|
const pauseBackfill = (trigger: any) => {
|
||||||
triggerStore.pauseBackfill(trigger)
|
triggerStore.pauseBackfill(trigger)
|
||||||
.then((newTrigger: any) => {
|
.then((newTrigger: any) => {
|
||||||
toast.saved(newTrigger.id);
|
toast.saved(newTrigger.triggerId);
|
||||||
triggers.value = triggers.value.map((t: any) => {
|
triggers.value = triggers.value.map((t: any) => {
|
||||||
if (t.id === newTrigger.id) {
|
if (t.id === newTrigger.id) {
|
||||||
return newTrigger
|
return newTrigger
|
||||||
@@ -506,7 +508,7 @@
|
|||||||
const unpauseBackfill = (trigger: any) => {
|
const unpauseBackfill = (trigger: any) => {
|
||||||
triggerStore.unpauseBackfill(trigger)
|
triggerStore.unpauseBackfill(trigger)
|
||||||
.then((newTrigger: any) => {
|
.then((newTrigger: any) => {
|
||||||
toast.saved(newTrigger.id);
|
toast.saved(newTrigger.triggerId);
|
||||||
triggers.value = triggers.value.map((t: any) => {
|
triggers.value = triggers.value.map((t: any) => {
|
||||||
if (t.id === newTrigger.id) {
|
if (t.id === newTrigger.id) {
|
||||||
return newTrigger
|
return newTrigger
|
||||||
@@ -519,7 +521,7 @@
|
|||||||
const deleteBackfill = (trigger: any) => {
|
const deleteBackfill = (trigger: any) => {
|
||||||
triggerStore.deleteBackfill(trigger)
|
triggerStore.deleteBackfill(trigger)
|
||||||
.then((newTrigger: any) => {
|
.then((newTrigger: any) => {
|
||||||
toast.saved(newTrigger.id);
|
toast.saved(newTrigger.triggerId);
|
||||||
triggers.value = triggers.value.map((t: any) => {
|
triggers.value = triggers.value.map((t: any) => {
|
||||||
if (t.id === newTrigger.id) {
|
if (t.id === newTrigger.id) {
|
||||||
return newTrigger
|
return newTrigger
|
||||||
@@ -532,7 +534,7 @@
|
|||||||
const setDisabled = (trigger: any, value: boolean) => {
|
const setDisabled = (trigger: any, value: boolean) => {
|
||||||
triggerStore.update({...trigger, disabled: !value})
|
triggerStore.update({...trigger, disabled: !value})
|
||||||
.then((newTrigger: any) => {
|
.then((newTrigger: any) => {
|
||||||
toast.saved(newTrigger.id);
|
toast.saved(newTrigger.triggerId);
|
||||||
triggers.value = triggers.value.map((t: any) => {
|
triggers.value = triggers.value.map((t: any) => {
|
||||||
if (t.id === newTrigger.id) {
|
if (t.id === newTrigger.id) {
|
||||||
return newTrigger
|
return newTrigger
|
||||||
@@ -548,7 +550,7 @@
|
|||||||
flowId: trigger.flowId,
|
flowId: trigger.flowId,
|
||||||
triggerId: trigger.triggerId
|
triggerId: trigger.triggerId
|
||||||
}).then((newTrigger: any) => {
|
}).then((newTrigger: any) => {
|
||||||
toast.saved(newTrigger.id);
|
toast.saved(newTrigger.triggerId);
|
||||||
triggers.value = triggers.value.map((t: any) => {
|
triggers.value = triggers.value.map((t: any) => {
|
||||||
if (t.id === newTrigger.id) {
|
if (t.id === newTrigger.id) {
|
||||||
return newTrigger
|
return newTrigger
|
||||||
@@ -564,7 +566,7 @@
|
|||||||
flowId: trigger.flowId,
|
flowId: trigger.flowId,
|
||||||
triggerId: trigger.triggerId
|
triggerId: trigger.triggerId
|
||||||
}).then((newTrigger: any) => {
|
}).then((newTrigger: any) => {
|
||||||
toast.saved(newTrigger.id);
|
toast.saved(newTrigger.triggerId);
|
||||||
triggers.value = triggers.value.map((t: any) => {
|
triggers.value = triggers.value.map((t: any) => {
|
||||||
if (t.id === newTrigger.id) {
|
if (t.id === newTrigger.id) {
|
||||||
return newTrigger
|
return newTrigger
|
||||||
|
|||||||
@@ -53,6 +53,7 @@
|
|||||||
refresh: {shown: true, callback: refresh}
|
refresh: {shown: true, callback: refresh}
|
||||||
}"
|
}"
|
||||||
@update-properties="updateDisplayColumns"
|
@update-properties="updateDisplayColumns"
|
||||||
|
:defaultScope="!route.name?.toString().startsWith('namespaces/')"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -204,6 +205,7 @@
|
|||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<TimeSeries
|
<TimeSeries
|
||||||
:chart="mappedChart(scope.row.id, scope.row.namespace)"
|
:chart="mappedChart(scope.row.id, scope.row.namespace)"
|
||||||
|
:filters="chartFilters()"
|
||||||
showDefault
|
showDefault
|
||||||
short
|
short
|
||||||
/>
|
/>
|
||||||
@@ -248,8 +250,8 @@
|
|||||||
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {ref, computed, onMounted, useTemplateRef} from "vue";
|
import {ref, computed, useTemplateRef} from "vue";
|
||||||
import {useRoute, useRouter} from "vue-router";
|
import {useRoute} from "vue-router";
|
||||||
import {useI18n} from "vue-i18n";
|
import {useI18n} from "vue-i18n";
|
||||||
import _merge from "lodash/merge";
|
import _merge from "lodash/merge";
|
||||||
import * as FILTERS from "../../utils/filters";
|
import * as FILTERS from "../../utils/filters";
|
||||||
@@ -272,7 +274,6 @@
|
|||||||
import TriggerAvatar from "./TriggerAvatar.vue";
|
import TriggerAvatar from "./TriggerAvatar.vue";
|
||||||
import DataTable from "../layout/DataTable.vue";
|
import DataTable from "../layout/DataTable.vue";
|
||||||
import BulkSelect from "../layout/BulkSelect.vue";
|
import BulkSelect from "../layout/BulkSelect.vue";
|
||||||
//@ts-expect-error no declaration file
|
|
||||||
import SelectTable from "../layout/SelectTable.vue";
|
import SelectTable from "../layout/SelectTable.vue";
|
||||||
import KSFilter from "../filter/components/KSFilter.vue";
|
import KSFilter from "../filter/components/KSFilter.vue";
|
||||||
import MarkdownTooltip from "../layout/MarkdownTooltip.vue";
|
import MarkdownTooltip from "../layout/MarkdownTooltip.vue";
|
||||||
@@ -283,17 +284,16 @@
|
|||||||
import permission from "../../models/permission";
|
import permission from "../../models/permission";
|
||||||
|
|
||||||
import {useToast} from "../../utils/toast";
|
import {useToast} from "../../utils/toast";
|
||||||
import {defaultNamespace} from "../../composables/useNamespaces";
|
|
||||||
|
|
||||||
import {useFlowStore} from "../../stores/flow";
|
import {useFlowStore} from "../../stores/flow";
|
||||||
import {useAuthStore} from "override/stores/auth";
|
import {useAuthStore} from "override/stores/auth";
|
||||||
|
import {useMiscStore} from "override/stores/misc";
|
||||||
import {useExecutionsStore} from "../../stores/executions";
|
import {useExecutionsStore} from "../../stores/executions";
|
||||||
|
|
||||||
import {useTableColumns} from "../../composables/useTableColumns";
|
import {useTableColumns} from "../../composables/useTableColumns";
|
||||||
import {DataTableRef, useDataTableActions} from "../../composables/useDataTableActions";
|
import {DataTableRef, useDataTableActions} from "../../composables/useDataTableActions";
|
||||||
import {useSelectTableActions} from "../../composables/useSelectTableActions";
|
import {useSelectTableActions} from "../../composables/useSelectTableActions";
|
||||||
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
topbar?: boolean;
|
topbar?: boolean;
|
||||||
namespace?: string;
|
namespace?: string;
|
||||||
@@ -307,9 +307,9 @@
|
|||||||
const flowStore = useFlowStore();
|
const flowStore = useFlowStore();
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
const executionsStore = useExecutionsStore();
|
const executionsStore = useExecutionsStore();
|
||||||
|
const miscStore = useMiscStore();
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const {t} = useI18n();
|
const {t} = useI18n();
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
@@ -622,24 +622,14 @@
|
|||||||
return MAPPED_CHARTS;
|
return MAPPED_CHARTS;
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
function chartFilters() {
|
||||||
const query = {...route.query};
|
const DEFAULT_DURATION = miscStore.configs?.chartDefaultDuration ?? "P30D";
|
||||||
const queryKeys = Object.keys(query);
|
return [{
|
||||||
let queryHasChanged = false;
|
field: "timeRange",
|
||||||
|
value: DEFAULT_DURATION,
|
||||||
if (props.namespace === undefined && defaultNamespace() && !queryKeys.some(key => key.startsWith("filters[namespace]"))) {
|
operation: "EQUALS"
|
||||||
query["filters[namespace][PREFIX]"] = defaultNamespace();
|
}];
|
||||||
queryHasChanged = true;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!queryKeys.some(key => key.startsWith("filters[scope]"))) {
|
|
||||||
query["filters[scope][EQUALS]"] = "USER";
|
|
||||||
queryHasChanged = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (queryHasChanged) router.replace({query});
|
|
||||||
});
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
@@ -56,7 +56,6 @@
|
|||||||
import DataTable from "../layout/DataTable.vue";
|
import DataTable from "../layout/DataTable.vue";
|
||||||
import SearchField from "../layout/SearchField.vue";
|
import SearchField from "../layout/SearchField.vue";
|
||||||
import NamespaceSelect from "../namespaces/components/NamespaceSelect.vue";
|
import NamespaceSelect from "../namespaces/components/NamespaceSelect.vue";
|
||||||
import useRestoreUrl from "../../composables/useRestoreUrl";
|
|
||||||
import useRouteContext from "../../composables/useRouteContext";
|
import useRouteContext from "../../composables/useRouteContext";
|
||||||
import {useDataTableActions} from "../../composables/useDataTableActions";
|
import {useDataTableActions} from "../../composables/useDataTableActions";
|
||||||
|
|
||||||
@@ -77,11 +76,9 @@
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
useRouteContext(routeInfo);
|
useRouteContext(routeInfo);
|
||||||
const {saveRestoreUrl} = useRestoreUrl({restoreUrl: true, isDefaultNamespaceAllow: true});
|
|
||||||
|
|
||||||
const {onPageChanged, onDataTableValue, queryWithFilter, ready} = useDataTableActions({
|
const {onPageChanged, onDataTableValue, queryWithFilter, ready} = useDataTableActions({
|
||||||
loadData,
|
loadData
|
||||||
saveRestoreUrl
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const namespace = computed({
|
const namespace = computed({
|
||||||
|
|||||||
@@ -83,6 +83,7 @@
|
|||||||
/* eslint-disable vue/enforce-style-attribute */
|
/* eslint-disable vue/enforce-style-attribute */
|
||||||
import {computed, onMounted, ref, shallowRef, watch} from "vue";
|
import {computed, onMounted, ref, shallowRef, watch} from "vue";
|
||||||
import {useI18n} from "vue-i18n";
|
import {useI18n} from "vue-i18n";
|
||||||
|
import {useThrottleFn} from "@vueuse/core";
|
||||||
import UnfoldLessHorizontal from "vue-material-design-icons/UnfoldLessHorizontal.vue";
|
import UnfoldLessHorizontal from "vue-material-design-icons/UnfoldLessHorizontal.vue";
|
||||||
import UnfoldMoreHorizontal from "vue-material-design-icons/UnfoldMoreHorizontal.vue";
|
import UnfoldMoreHorizontal from "vue-material-design-icons/UnfoldMoreHorizontal.vue";
|
||||||
import Help from "vue-material-design-icons/Help.vue";
|
import Help from "vue-material-design-icons/Help.vue";
|
||||||
@@ -94,6 +95,7 @@
|
|||||||
import {TabFocus} from "monaco-editor/esm/vs/editor/browser/config/tabFocus";
|
import {TabFocus} from "monaco-editor/esm/vs/editor/browser/config/tabFocus";
|
||||||
import MonacoEditor from "./MonacoEditor.vue";
|
import MonacoEditor from "./MonacoEditor.vue";
|
||||||
import type * as monaco from "monaco-editor/esm/vs/editor/editor.api";
|
import type * as monaco from "monaco-editor/esm/vs/editor/editor.api";
|
||||||
|
import {useScrollMemory} from "../../composables/useScrollMemory";
|
||||||
|
|
||||||
const {t} = useI18n()
|
const {t} = useI18n()
|
||||||
|
|
||||||
@@ -123,6 +125,7 @@
|
|||||||
shouldFocus: {type: Boolean, default: true},
|
shouldFocus: {type: Boolean, default: true},
|
||||||
showScroll: {type: Boolean, default: false},
|
showScroll: {type: Boolean, default: false},
|
||||||
diffOverviewBar: {type: Boolean, default: true},
|
diffOverviewBar: {type: Boolean, default: true},
|
||||||
|
scrollKey: {type: String, default: undefined},
|
||||||
})
|
})
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
@@ -312,6 +315,29 @@
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const codeEditor = editor as monaco.editor.IStandaloneCodeEditor;
|
||||||
|
const scrollMemory = props.scrollKey ? useScrollMemory(ref(props.scrollKey)) : null;
|
||||||
|
|
||||||
|
if (props.scrollKey && scrollMemory) {
|
||||||
|
const savedState = scrollMemory.loadData<monaco.editor.ICodeEditorViewState>("viewState");
|
||||||
|
if (savedState) {
|
||||||
|
codeEditor.restoreViewState(savedState);
|
||||||
|
codeEditor.revealLineInCenterIfOutsideViewport?.(codeEditor.getPosition()?.lineNumber ?? 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const top = scrollMemory.loadData<number>("scrollTop", 0);
|
||||||
|
if (typeof top === "number") {
|
||||||
|
codeEditor.setScrollTop(top);
|
||||||
|
}
|
||||||
|
|
||||||
|
const throttledSave = useThrottleFn(() => {
|
||||||
|
scrollMemory.saveData(codeEditor.saveViewState(), "viewState");
|
||||||
|
scrollMemory.saveData(codeEditor.getScrollTop(), "scrollTop");
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
codeEditor.onDidScrollChange?.(throttledSave);
|
||||||
|
}
|
||||||
|
|
||||||
if (!isDiff.value) {
|
if (!isDiff.value) {
|
||||||
editor.onDidBlurEditorWidget?.(() => {
|
editor.onDidBlurEditorWidget?.(() => {
|
||||||
emit("focusout", isCodeEditor(editor)
|
emit("focusout", isCodeEditor(editor)
|
||||||
@@ -468,6 +494,10 @@
|
|||||||
position: position,
|
position: position,
|
||||||
model: model,
|
model: model,
|
||||||
});
|
});
|
||||||
|
// Save view state when cursor changes
|
||||||
|
if (scrollMemory) {
|
||||||
|
scrollMemory.saveData(codeEditor.saveViewState(), "viewState");
|
||||||
|
}
|
||||||
}, 100) as unknown as number;
|
}, 100) as unknown as number;
|
||||||
highlightPebble();
|
highlightPebble();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -59,12 +59,12 @@
|
|||||||
const {t} = useI18n();
|
const {t} = useI18n();
|
||||||
|
|
||||||
const exportYaml = () => {
|
const exportYaml = () => {
|
||||||
const src = flowStore.flowYaml
|
if(!flowStore.flow || !flowStore.flowYaml) return;
|
||||||
if(!src) {
|
|
||||||
return;
|
const {id, namespace} = flowStore.flow;
|
||||||
}
|
const blob = new Blob([flowStore.flowYaml], {type: "text/yaml"});
|
||||||
const blob = new Blob([src], {type: "text/yaml"});
|
|
||||||
localUtils.downloadUrl(window.URL.createObjectURL(blob), "flow.yaml");
|
localUtils.downloadUrl(window.URL.createObjectURL(blob), `${namespace}.${id}.yaml`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const flowStore = useFlowStore();
|
const flowStore = useFlowStore();
|
||||||
@@ -109,24 +109,31 @@
|
|||||||
const onSaveAll = inject(FILES_SAVE_ALL_INJECTION_KEY);
|
const onSaveAll = inject(FILES_SAVE_ALL_INJECTION_KEY);
|
||||||
|
|
||||||
async function save(){
|
async function save(){
|
||||||
// Save the isCreating before saving.
|
try {
|
||||||
// saveAll can change its value.
|
// Save the isCreating before saving.
|
||||||
const isCreating = flowStore.isCreating
|
// saveAll can change its value.
|
||||||
await flowStore.saveAll()
|
const isCreating = flowStore.isCreating
|
||||||
|
await flowStore.saveAll()
|
||||||
|
|
||||||
if(isCreating){
|
if(isCreating){
|
||||||
await router.push({
|
await router.push({
|
||||||
name: "flows/update",
|
name: "flows/update",
|
||||||
params: {
|
params: {
|
||||||
id: flowStore.flow?.id,
|
id: flowStore.flow?.id,
|
||||||
namespace: flowStore.flow?.namespace,
|
namespace: flowStore.flow?.namespace,
|
||||||
tab: "edit",
|
tab: "edit",
|
||||||
tenant: routeParams.value.tenant,
|
tenant: routeParams.value.tenant,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onSaveAll?.();
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error?.status === 401) {
|
||||||
|
toast.error("401 Unauthorized", undefined, {duration: 2000});
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onSaveAll?.();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteFlow = () => {
|
const deleteFlow = () => {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
:creating="isCreating"
|
:creating="isCreating"
|
||||||
:path="path"
|
:path="path"
|
||||||
:diffOverviewBar="false"
|
:diffOverviewBar="false"
|
||||||
|
:scrollKey="editorScrollKey"
|
||||||
@update:model-value="editorUpdate"
|
@update:model-value="editorUpdate"
|
||||||
@cursor="updatePluginDocumentation"
|
@cursor="updatePluginDocumentation"
|
||||||
@save="flow ? saveFlowYaml(): saveFileContent()"
|
@save="flow ? saveFlowYaml(): saveFileContent()"
|
||||||
@@ -224,6 +225,19 @@
|
|||||||
const namespacesStore = useNamespacesStore();
|
const namespacesStore = useNamespacesStore();
|
||||||
const miscStore = useMiscStore();
|
const miscStore = useMiscStore();
|
||||||
|
|
||||||
|
const editorScrollKey = computed(() => {
|
||||||
|
if (props.flow) {
|
||||||
|
const ns = flowStore.flow?.namespace ?? "";
|
||||||
|
const id = flowStore.flow?.id ?? "";
|
||||||
|
return `flow:${ns}/${id}:code`;
|
||||||
|
}
|
||||||
|
const ns = namespace.value;
|
||||||
|
if (ns && props.path) {
|
||||||
|
return `file:${ns}:${props.path}`;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
|
||||||
function loadPluginsHash() {
|
function loadPluginsHash() {
|
||||||
miscStore.loadConfigs().then(config => {
|
miscStore.loadConfigs().then(config => {
|
||||||
hash.value = config.pluginsHash;
|
hash.value = config.pluginsHash;
|
||||||
|
|||||||
@@ -463,7 +463,7 @@
|
|||||||
for (const item of itemsArr) {
|
for (const item of itemsArr) {
|
||||||
const fullPath = `${parentPath}${item.fileName}`;
|
const fullPath = `${parentPath}${item.fileName}`;
|
||||||
result.push({path: fullPath, fileName: item.fileName, id: item.id});
|
result.push({path: fullPath, fileName: item.fileName, id: item.id});
|
||||||
if (isDirectory(item) && item.children.length > 0) {
|
if (isDirectory(item) && item.children?.length > 0) {
|
||||||
result.push(...flattenTree(item.children, `${fullPath}/`));
|
result.push(...flattenTree(item.children, `${fullPath}/`));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -688,21 +688,22 @@
|
|||||||
|
|
||||||
async function removeItems() {
|
async function removeItems() {
|
||||||
if(confirmation.value.nodes === undefined) return;
|
if(confirmation.value.nodes === undefined) return;
|
||||||
for (const node of confirmation.value.nodes) {
|
await Promise.all(confirmation.value.nodes.map(async (node, i) => {
|
||||||
|
const path = filesStore.getPath(node.id) ?? "";
|
||||||
try {
|
try {
|
||||||
await namespacesStore.deleteFileDirectory({
|
await namespacesStore.deleteFileDirectory({
|
||||||
namespace: props.currentNS ?? route.params.namespace as string,
|
namespace: props.currentNS ?? route.params.namespace as string,
|
||||||
path: filesStore.getPath(node) ?? "",
|
path,
|
||||||
});
|
});
|
||||||
tree.value.remove(node.id);
|
tree.value.remove(node.id);
|
||||||
closeTab?.({
|
closeTab?.({
|
||||||
path: filesStore.getPath(node) ?? "",
|
path,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to delete file: ${node.fileName}`, error);
|
console.error(`Failed to delete file: ${node.fileName}`, error);
|
||||||
toast.error(`Failed to delete file: ${node.fileName}`);
|
toast.error(`Failed to delete file: ${node.fileName}`);
|
||||||
}
|
}
|
||||||
}
|
}));
|
||||||
confirmation.value = {visible: false, nodes: []};
|
confirmation.value = {visible: false, nodes: []};
|
||||||
toast.success("Selected files deleted successfully.");
|
toast.success("Selected files deleted successfully.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -304,6 +304,7 @@
|
|||||||
const suggestWidgetResizeObserver = ref<MutationObserver>()
|
const suggestWidgetResizeObserver = ref<MutationObserver>()
|
||||||
const suggestWidgetObserver = ref<MutationObserver>()
|
const suggestWidgetObserver = ref<MutationObserver>()
|
||||||
const suggestWidget = ref<HTMLElement>()
|
const suggestWidget = ref<HTMLElement>()
|
||||||
|
const resizeObserver = ref<ResizeObserver>()
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
focus,
|
focus,
|
||||||
@@ -871,6 +872,20 @@
|
|||||||
setTimeout(() => monaco.editor.remeasureFonts(), 1)
|
setTimeout(() => monaco.editor.remeasureFonts(), 1)
|
||||||
emit("editorDidMount", editorResolved.value);
|
emit("editorDidMount", editorResolved.value);
|
||||||
|
|
||||||
|
/* Hhandle resizing. */
|
||||||
|
resizeObserver.value = new ResizeObserver(() => {
|
||||||
|
if (localEditor.value) {
|
||||||
|
localEditor.value.layout();
|
||||||
|
}
|
||||||
|
if (localDiffEditor.value) {
|
||||||
|
localDiffEditor.value.getModifiedEditor().layout();
|
||||||
|
localDiffEditor.value.getOriginalEditor().layout();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (editorRef.value) {
|
||||||
|
resizeObserver.value.observe(editorRef.value);
|
||||||
|
}
|
||||||
|
|
||||||
highlightLine();
|
highlightLine();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -928,6 +943,8 @@
|
|||||||
function destroy() {
|
function destroy() {
|
||||||
disposeObservers();
|
disposeObservers();
|
||||||
disposeCompletions.value?.();
|
disposeCompletions.value?.();
|
||||||
|
resizeObserver.value?.disconnect();
|
||||||
|
resizeObserver.value = undefined;
|
||||||
if (localDiffEditor.value !== undefined) {
|
if (localDiffEditor.value !== undefined) {
|
||||||
localDiffEditor.value?.dispose();
|
localDiffEditor.value?.dispose();
|
||||||
localDiffEditor.value?.getModel()?.modified?.dispose();
|
localDiffEditor.value?.getModel()?.modified?.dispose();
|
||||||
|
|||||||
@@ -235,7 +235,7 @@
|
|||||||
import {useI18n} from "vue-i18n";
|
import {useI18n} from "vue-i18n";
|
||||||
import {useRoute} from "vue-router";
|
import {useRoute} from "vue-router";
|
||||||
import _groupBy from "lodash/groupBy";
|
import _groupBy from "lodash/groupBy";
|
||||||
import {computed, ref, useTemplateRef, watch} from "vue";
|
import {computed, nextTick, ref, useTemplateRef, watch} from "vue";
|
||||||
|
|
||||||
import Check from "vue-material-design-icons/Check.vue";
|
import Check from "vue-material-design-icons/Check.vue";
|
||||||
import Delete from "vue-material-design-icons/Delete.vue";
|
import Delete from "vue-material-design-icons/Delete.vue";
|
||||||
@@ -272,7 +272,6 @@
|
|||||||
import DataTable from "../layout/DataTable.vue";
|
import DataTable from "../layout/DataTable.vue";
|
||||||
import _merge from "lodash/merge";
|
import _merge from "lodash/merge";
|
||||||
import {type DataTableRef, useDataTableActions} from "../../composables/useDataTableActions.ts";
|
import {type DataTableRef, useDataTableActions} from "../../composables/useDataTableActions.ts";
|
||||||
|
|
||||||
const dataTable = useTemplateRef<DataTableRef>("dataTable");
|
const dataTable = useTemplateRef<DataTableRef>("dataTable");
|
||||||
|
|
||||||
const loadData = async (callback?: () => void) => {
|
const loadData = async (callback?: () => void) => {
|
||||||
@@ -491,6 +490,8 @@
|
|||||||
kv.value.key = entry.key;
|
kv.value.key = entry.key;
|
||||||
const {type, value} = await namespacesStore.kv({namespace: entry.namespace, key: entry.key});
|
const {type, value} = await namespacesStore.kv({namespace: entry.namespace, key: entry.key});
|
||||||
kv.value.type = type;
|
kv.value.type = type;
|
||||||
|
// Force the type reset before setting the value
|
||||||
|
await nextTick();
|
||||||
if (type === "JSON") {
|
if (type === "JSON") {
|
||||||
kv.value.value = JSON.stringify(value);
|
kv.value.value = JSON.stringify(value);
|
||||||
} else if (type === "BOOLEAN") {
|
} else if (type === "BOOLEAN") {
|
||||||
@@ -504,7 +505,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function removeKv(namespace: string, key: string) {
|
function removeKv(namespace: string, key: string) {
|
||||||
toast.confirm("delete confirm", async () => {
|
toast.confirm(t("delete confirm"), async () => {
|
||||||
return namespacesStore
|
return namespacesStore
|
||||||
.deleteKv({namespace, key: key})
|
.deleteKv({namespace, key: key})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -543,14 +544,16 @@
|
|||||||
const type = kv.value.type;
|
const type = kv.value.type;
|
||||||
let value: any = kv.value.value;
|
let value: any = kv.value.value;
|
||||||
|
|
||||||
if (type === "STRING" || type === "DURATION") {
|
if (type === "STRING") {
|
||||||
|
value = JSON.stringify(value);
|
||||||
|
} else if (["DURATION", "JSON"].includes(type)) {
|
||||||
value = value || "";
|
value = value || "";
|
||||||
} else if (type === "DATETIME") {
|
} else if (type === "DATETIME") {
|
||||||
value = new Date(value!).toISOString();
|
value = new Date(value!).toISOString();
|
||||||
} else if (type === "DATE") {
|
} else if (type === "DATE") {
|
||||||
value = new Date(value!).toISOString().split("T")[0];
|
value = new Date(value!).toISOString().split("T")[0];
|
||||||
} else if (["NUMBER", "BOOLEAN", "JSON"].includes(type)) {
|
} else {
|
||||||
value = JSON.stringify(value);
|
value = String(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
const contentType = "text/plain";
|
const contentType = "text/plain";
|
||||||
@@ -605,10 +608,9 @@
|
|||||||
|
|
||||||
const formRef = ref();
|
const formRef = ref();
|
||||||
|
|
||||||
watch(() => kv.value.type, () => {
|
watch(() => kv.value.type, (newType) => {
|
||||||
if (formRef.value) {
|
formRef.value?.clearValidate("value");
|
||||||
(formRef.value as any).clearValidate("value");
|
if (newType === "BOOLEAN") kv.value.value = false;
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
|
|||||||
@@ -72,7 +72,7 @@
|
|||||||
if (props.labels.length === 0) {
|
if (props.labels.length === 0) {
|
||||||
addItem();
|
addItem();
|
||||||
} else {
|
} else {
|
||||||
locals.value = [...props.labels];
|
locals.value = props.labels;
|
||||||
if (locals.value.length === 0) {
|
if (locals.value.length === 0) {
|
||||||
addItem();
|
addItem();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<ContextInfoContent :title="t('feeds.title')">
|
<ContextInfoContent ref="contextInfoRef" :title="t('feeds.title')">
|
||||||
<div
|
<div
|
||||||
class="post"
|
class="post"
|
||||||
:class="{
|
:class="{
|
||||||
@@ -46,9 +46,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {computed, onMounted, reactive} from "vue";
|
import {computed, onMounted, reactive, ref} from "vue";
|
||||||
import {useI18n} from "vue-i18n";
|
import {useI18n} from "vue-i18n";
|
||||||
import {useStorage} from "@vueuse/core"
|
import {useStorage} from "@vueuse/core"
|
||||||
|
import {useScrollMemory} from "../../composables/useScrollMemory"
|
||||||
|
|
||||||
import OpenInNew from "vue-material-design-icons/OpenInNew.vue";
|
import OpenInNew from "vue-material-design-icons/OpenInNew.vue";
|
||||||
import MenuDown from "vue-material-design-icons/MenuDown.vue";
|
import MenuDown from "vue-material-design-icons/MenuDown.vue";
|
||||||
@@ -62,6 +63,7 @@
|
|||||||
const apiStore = useApiStore();
|
const apiStore = useApiStore();
|
||||||
const {t} = useI18n({useScope: "global"});
|
const {t} = useI18n({useScope: "global"});
|
||||||
|
|
||||||
|
const contextInfoRef = ref<InstanceType<typeof ContextInfoContent> | null>(null);
|
||||||
const feeds = computed(() => apiStore.feeds);
|
const feeds = computed(() => apiStore.feeds);
|
||||||
|
|
||||||
const expanded = reactive<Record<string, boolean>>({});
|
const expanded = reactive<Record<string, boolean>>({});
|
||||||
@@ -70,6 +72,9 @@
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
lastNewsReadDate.value = feeds.value[0].publicationDate;
|
lastNewsReadDate.value = feeds.value[0].publicationDate;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const scrollableElement = computed(() => contextInfoRef.value?.contentRef || null)
|
||||||
|
useScrollMemory(ref("context-panel-news"), scrollableElement as any)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
@@ -53,10 +53,11 @@
|
|||||||
if (isChecked(label)) {
|
if (isChecked(label)) {
|
||||||
const replacementQuery = {...route.query};
|
const replacementQuery = {...route.query};
|
||||||
delete replacementQuery[getKey(label.key)];
|
delete replacementQuery[getKey(label.key)];
|
||||||
|
replacementQuery.page = "1";
|
||||||
router.replace({query: replacementQuery});
|
router.replace({query: replacementQuery});
|
||||||
} else {
|
} else {
|
||||||
router.replace({
|
router.replace({
|
||||||
query: {...route.query, [getKey(label.key)]: label.value},
|
query: {...route.query, [getKey(label.key)]: label.value, page: "1"},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="position-relative">
|
<div ref="container" class="position-relative">
|
||||||
<div v-if="hasSelection && data.length" class="bulk-select-header">
|
<div v-if="hasSelection && data.length" class="bulk-select-header">
|
||||||
<slot name="select-actions" />
|
<slot name="select-actions" />
|
||||||
</div>
|
</div>
|
||||||
@@ -9,12 +9,8 @@
|
|||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
:data
|
:data
|
||||||
:rowKey
|
:rowKey
|
||||||
:emptyText="data.length === 0 && infiniteScrollLoad === undefined ? noDataText : ''"
|
:emptyText="data.length === 0 ? noDataText : ''"
|
||||||
@selection-change="selectionChanged"
|
@selection-change="selectionChanged"
|
||||||
v-el-table-infinite-scroll="infiniteScrollLoadWithDisableHandling"
|
|
||||||
:infiniteScrollDisabled="infiniteScrollLoad === undefined ? true : infiniteScrollDisabled"
|
|
||||||
:infiniteScrollDelay="0"
|
|
||||||
:height="data.length === 0 && infiniteScrollLoad === undefined ? '100px' : tableHeight"
|
|
||||||
>
|
>
|
||||||
<el-table-column type="selection" v-if="selectable && showSelection" reserveSelection />
|
<el-table-column type="selection" v-if="selectable && showSelection" reserveSelection />
|
||||||
<slot name="default" />
|
<slot name="default" />
|
||||||
@@ -22,155 +18,107 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import elTableInfiniteScroll from "el-table-infinite-scroll";
|
import {ref, onMounted, onUnmounted, onUpdated, watch} from "vue";
|
||||||
|
|
||||||
export default {
|
const props = withDefaults(defineProps<{
|
||||||
data() {
|
showSelection?: boolean;
|
||||||
return {
|
selectable?: boolean;
|
||||||
hasSelection: false,
|
expandable?: boolean;
|
||||||
infiniteScrollDisabled: false,
|
data?: any[];
|
||||||
tableHeight: this.infiniteScrollLoad === undefined ? "auto" : "100%"
|
noDataText?: string;
|
||||||
}
|
rowKey?: string | ((row: any) => string | number);
|
||||||
},
|
}>(), {
|
||||||
expose: ["resetInfiniteScroll", "setSelection", "waitTableRender", "toggleRowExpansion"],
|
showSelection: true,
|
||||||
computed: {
|
selectable: true,
|
||||||
scrollWrapper() {
|
expandable: false,
|
||||||
if (this.data) {
|
data: () => [],
|
||||||
return this.$refs.table?.$el?.querySelector(".el-scrollbar__wrap");
|
noDataText: undefined,
|
||||||
}
|
rowKey: "id"
|
||||||
|
});
|
||||||
|
|
||||||
return undefined;
|
const emit = defineEmits<{
|
||||||
},
|
"selection-change": [selection: any[]];
|
||||||
tableView() {
|
}>();
|
||||||
if (this.data) {
|
|
||||||
return this.scrollWrapper?.querySelector(".el-scrollbar__view");
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
const table = ref<any>(null);
|
||||||
},
|
const hasSelection = ref(false);
|
||||||
stillHaveDataToFetch() {
|
const container = ref<HTMLElement>();
|
||||||
return this.infiniteScrollDisabled === false;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
directives: {
|
|
||||||
elTableInfiniteScroll
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async resetInfiniteScroll() {
|
|
||||||
this.infiniteScrollDisabled = false;
|
|
||||||
this.tableHeight = await this.computeTableHeight();
|
|
||||||
},
|
|
||||||
async toggleRowExpansion(row, expand){
|
|
||||||
this.$refs.table.toggleRowExpansion(row, expand)
|
|
||||||
// this.$refs.table.clearSelection()
|
|
||||||
},
|
|
||||||
async waitTableRender() {
|
|
||||||
if (this.tableView === undefined) {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.tableView.querySelectorAll(".el-table__body > tbody > *")?.length === this.data?.length) {
|
const toggleRowExpansion = (row: any, expand?: boolean) => {
|
||||||
return Promise.resolve();
|
table.value?.toggleRowExpansion(row, expand);
|
||||||
}
|
};
|
||||||
|
|
||||||
return new Promise(resolve => {
|
const selectionChanged = (selection: any[]) => {
|
||||||
const observer = new MutationObserver(([{target}]) => {
|
hasSelection.value = selection.length > 0;
|
||||||
if (target.childElementCount === this.data?.length) {
|
emit("selection-change", selection);
|
||||||
observer.disconnect();
|
};
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
observer.observe(this.tableView.querySelector(".el-table__body > tbody"), {childList: true});
|
const clearSelection = () => {
|
||||||
});
|
table.value?.clearSelection();
|
||||||
},
|
hasSelection.value = false;
|
||||||
selectionChanged(selection) {
|
};
|
||||||
this.hasSelection = selection.length > 0;
|
|
||||||
this.$emit("selection-change", selection);
|
|
||||||
},
|
|
||||||
setSelection(selection) {
|
|
||||||
this.$refs.table.clearSelection();
|
|
||||||
if (Array.isArray(selection)) {
|
|
||||||
const isFunction = typeof this.rowKey === "function";
|
|
||||||
selection.forEach(sel => {
|
|
||||||
const row = this.data.find(r => isFunction
|
|
||||||
? this.rowKey(r) === this.rowKey(sel)
|
|
||||||
: r[this.rowKey] === sel[this.rowKey]);
|
|
||||||
if (row) this.$refs.table.toggleRowSelection(row, true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.selectionChanged(selection);
|
|
||||||
},
|
|
||||||
computeHeaderSize() {
|
|
||||||
const tableElement = this.$refs.table?.$el;
|
|
||||||
|
|
||||||
if(!tableElement) return;
|
const setSelection = (selection: any[]) => {
|
||||||
|
table.value?.clearSelection();
|
||||||
|
if (Array.isArray(selection)) {
|
||||||
|
const isFunction = typeof props.rowKey === "function";
|
||||||
|
selection.forEach(sel => {
|
||||||
|
const row = props.data.find(r => isFunction
|
||||||
|
? props.rowKey(r) === props.rowKey(sel)
|
||||||
|
: r[props.rowKey] === sel[props.rowKey]);
|
||||||
|
if (row) table.value?.toggleRowSelection(row, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
selectionChanged(selection);
|
||||||
|
};
|
||||||
|
|
||||||
this.$el.style.setProperty("--table-header-width", `${tableElement.clientWidth}px`);
|
const computeHeaderSize = () => {
|
||||||
this.$el.style.setProperty("--table-header-height", `${tableElement.querySelector("thead").clientHeight}px`);
|
const tableElement = table.value?.$el;
|
||||||
},
|
if (!tableElement || !container.value) return;
|
||||||
async computeTableHeight() {
|
container.value.style.setProperty("--table-header-width", `${tableElement.clientWidth}px`);
|
||||||
await this.waitTableRender();
|
container.value.style.setProperty("--table-header-height", `${tableElement.querySelector("thead").clientHeight}px`);
|
||||||
|
};
|
||||||
|
|
||||||
if (this.infiniteScrollLoad === undefined || this.scrollWrapper === undefined) {
|
onMounted(() => {
|
||||||
return "auto";
|
window.addEventListener("resize", computeHeaderSize);
|
||||||
}
|
});
|
||||||
|
|
||||||
if (!this.stillHaveDataToFetch && this.data.length === 0) {
|
onUnmounted(() => {
|
||||||
return "calc(var(--table-header-height) + 60px)";
|
window.removeEventListener("resize", computeHeaderSize);
|
||||||
}
|
});
|
||||||
|
|
||||||
return this.stillHaveDataToFetch || this.tableView === undefined ? "100%" : `min(${this.tableView.scrollHeight}px, 100%)`;
|
onUpdated(() => {
|
||||||
},
|
computeHeaderSize();
|
||||||
async infiniteScrollLoadWithDisableHandling() {
|
});
|
||||||
let load = await this.infiniteScrollLoad?.();
|
|
||||||
while (load !== undefined && load.length === 0) {
|
|
||||||
load = await this.infiniteScrollLoad?.();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.infiniteScrollDisabled = load === undefined;
|
watch(() => props.data, () => {
|
||||||
|
if (props.data.length === 0) {
|
||||||
return load;
|
hasSelection.value = false;
|
||||||
}
|
table.value?.clearSelection();
|
||||||
},
|
} else {
|
||||||
props: {
|
const currentSelection = table.value?.getSelectionRows() ?? [];
|
||||||
showSelection: {type: Boolean, default: true},
|
const validSelection = currentSelection.filter((sel: any) => {
|
||||||
selectable: {type: Boolean, default: true},
|
const isFunction = typeof props.rowKey === "function";
|
||||||
expandable: {type: Boolean, default: false},
|
return props.data.some(r => isFunction
|
||||||
data: {type: Array, default: () => []},
|
? props.rowKey(r) === props.rowKey(sel)
|
||||||
noDataText: {type: String, default: undefined},
|
: r[props.rowKey] === sel[props.rowKey]);
|
||||||
infiniteScrollLoad: {type: Function, default: undefined},
|
});
|
||||||
rowKey: {type: [String, Function], default: "id"}
|
if (validSelection.length !== currentSelection.length) {
|
||||||
},
|
table.value?.clearSelection();
|
||||||
emits: [
|
hasSelection.value = false;
|
||||||
"selection-change"
|
} else if (table.value) {
|
||||||
],
|
selectionChanged(currentSelection);
|
||||||
async mounted() {
|
|
||||||
window.addEventListener("resize", this.computeHeaderSize);
|
|
||||||
},
|
|
||||||
unmounted() {
|
|
||||||
window.removeEventListener("resize", this.computeHeaderSize);
|
|
||||||
},
|
|
||||||
updated() {
|
|
||||||
this.computeHeaderSize();
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
data: {
|
|
||||||
async handler() {
|
|
||||||
this.tableHeight = await this.computeTableHeight();
|
|
||||||
},
|
|
||||||
immediate: true
|
|
||||||
},
|
|
||||||
async stillHaveDataToFetch(newVal, oldVal) {
|
|
||||||
if (oldVal !== newVal) {
|
|
||||||
this.tableHeight = await this.computeTableHeight();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}, {immediate: true});
|
||||||
</script>
|
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
setSelection,
|
||||||
|
clearSelection,
|
||||||
|
toggleRowExpansion
|
||||||
|
});
|
||||||
|
</script>
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.bulk-select-header {
|
.bulk-select-header {
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
@@ -186,4 +134,12 @@
|
|||||||
z-index: 0;
|
z-index: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 500px) {
|
||||||
|
:deep(.el-table__empty-text) {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<TopNavBar v-if="!embed" :title="routeInfo.title" />
|
<TopNavBar v-if="!embed" :title="routeInfo.title" />
|
||||||
<section v-bind="$attrs" :class="{'container': !embed}" class="log-panel">
|
<section v-bind="$attrs" :class="{'container': !embed}" class="log-panel">
|
||||||
<div class="log-content">
|
<div class="log-content">
|
||||||
<DataTable @page-changed="onPageChanged" ref="dataTable" :total="logsStore.total" :size="pageSize" :page="pageNumber" :embed="embed">
|
<DataTable @page-changed="onPageChanged" ref="dataTable" :total="logsStore.total" :size="internalPageSize" :page="internalPageNumber" :embed="embed">
|
||||||
<template #navbar v-if="!embed || showFilters">
|
<template #navbar v-if="!embed || showFilters">
|
||||||
<KSFilter
|
<KSFilter
|
||||||
:configuration="logFilter"
|
:configuration="logFilter"
|
||||||
@@ -11,8 +11,9 @@
|
|||||||
refresh: {shown: true, callback: refresh},
|
refresh: {shown: true, callback: refresh},
|
||||||
columns: {shown: false}
|
columns: {shown: false}
|
||||||
}"
|
}"
|
||||||
|
:defaultScope="false"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>xx
|
||||||
|
|
||||||
<template v-if="showStatChart()" #top>
|
<template v-if="showStatChart()" #top>
|
||||||
<Sections ref="dashboard" :charts :dashboard="{id: 'default', charts: []}" showDefault />
|
<Sections ref="dashboard" :charts :dashboard="{id: 'default', charts: []}" showDefault />
|
||||||
@@ -20,7 +21,7 @@
|
|||||||
|
|
||||||
<template #table>
|
<template #table>
|
||||||
<div v-loading="isLoading">
|
<div v-loading="isLoading">
|
||||||
<div v-if="logsStore.logs !== undefined && logsStore.logs.length > 0" class="logs-wrapper">
|
<div v-if="logsStore.logs !== undefined && logsStore.logs?.length > 0" class="logs-wrapper">
|
||||||
<LogLine
|
<LogLine
|
||||||
v-for="(log, i) in logsStore.logs"
|
v-for="(log, i) in logsStore.logs"
|
||||||
:key="`${log.taskRunId}-${i}`"
|
:key="`${log.taskRunId}-${i}`"
|
||||||
@@ -42,6 +43,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import {ref, computed, onMounted, watch, useTemplateRef} from "vue";
|
||||||
|
import {useRoute} from "vue-router";
|
||||||
|
import {useI18n} from "vue-i18n";
|
||||||
|
import _merge from "lodash/merge";
|
||||||
|
import moment from "moment";
|
||||||
import {useLogFilter} from "../filter/configurations";
|
import {useLogFilter} from "../filter/configurations";
|
||||||
import KSFilter from "../filter/components/KSFilter.vue";
|
import KSFilter from "../filter/components/KSFilter.vue";
|
||||||
import Sections from "../dashboard/sections/Sections.vue";
|
import Sections from "../dashboard/sections/Sections.vue";
|
||||||
@@ -49,193 +55,151 @@
|
|||||||
import TopNavBar from "../../components/layout/TopNavBar.vue";
|
import TopNavBar from "../../components/layout/TopNavBar.vue";
|
||||||
import LogLine from "../logs/LogLine.vue";
|
import LogLine from "../logs/LogLine.vue";
|
||||||
import NoData from "../layout/NoData.vue";
|
import NoData from "../layout/NoData.vue";
|
||||||
|
|
||||||
const logFilter = useLogFilter();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import {mapStores} from "pinia";
|
|
||||||
import RouteContext from "../../mixins/routeContext";
|
|
||||||
import RestoreUrl from "../../mixins/restoreUrl";
|
|
||||||
import DataTableActions from "../../mixins/dataTableActions";
|
|
||||||
import _merge from "lodash/merge";
|
|
||||||
import {storageKeys} from "../../utils/constants";
|
import {storageKeys} from "../../utils/constants";
|
||||||
import {decodeSearchParams} from "../filter/utils/helpers";
|
import {decodeSearchParams} from "../filter/utils/helpers";
|
||||||
import * as YAML_UTILS from "@kestra-io/ui-libs/flow-yaml-utils";
|
import * as YAML_UTILS from "@kestra-io/ui-libs/flow-yaml-utils";
|
||||||
import YAML_CHART from "../dashboard/assets/logs_timeseries_chart.yaml?raw";
|
import YAML_CHART from "../dashboard/assets/logs_timeseries_chart.yaml?raw";
|
||||||
import {useLogsStore} from "../../stores/logs";
|
import {useLogsStore} from "../../stores/logs";
|
||||||
import {defaultNamespace} from "../../composables/useNamespaces";
|
import {useDataTableActions} from "../../composables/useDataTableActions";
|
||||||
import {defineComponent} from "vue";
|
import useRouteContext from "../../composables/useRouteContext";
|
||||||
|
|
||||||
export default defineComponent({
|
const props = withDefaults(defineProps<{
|
||||||
mixins: [RouteContext, RestoreUrl, DataTableActions],
|
logLevel?: string;
|
||||||
props: {
|
embed?: boolean;
|
||||||
logLevel: {
|
showFilters?: boolean;
|
||||||
type: String,
|
filters?: Record<string, any>;
|
||||||
default: undefined
|
reloadLogs?: number;
|
||||||
},
|
}>(), {
|
||||||
embed: {
|
embed: false,
|
||||||
type: Boolean,
|
showFilters: false,
|
||||||
default: false
|
filters: undefined,
|
||||||
},
|
logLevel: undefined,
|
||||||
showFilters: {
|
reloadLogs: undefined
|
||||||
type: Boolean,
|
});
|
||||||
default: false
|
|
||||||
},
|
|
||||||
filters: {
|
|
||||||
type: Object,
|
|
||||||
default: null
|
|
||||||
},
|
|
||||||
reloadLogs: {
|
|
||||||
type: Number,
|
|
||||||
default: undefined
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
isDefaultNamespaceAllow: true,
|
|
||||||
task: undefined,
|
|
||||||
isLoading: false,
|
|
||||||
lastRefreshDate: new Date(),
|
|
||||||
canAutoRefresh: false,
|
|
||||||
showChart: localStorage.getItem(storageKeys.SHOW_LOGS_CHART) !== "false",
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
storageKeys() {
|
|
||||||
return storageKeys
|
|
||||||
},
|
|
||||||
...mapStores(useLogsStore),
|
|
||||||
routeInfo() {
|
|
||||||
return {
|
|
||||||
title: this.$t("logs"),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
isFlowEdit() {
|
|
||||||
return this.$route.name === "flows/update"
|
|
||||||
},
|
|
||||||
isNamespaceEdit() {
|
|
||||||
return this.$route.name === "namespaces/update"
|
|
||||||
},
|
|
||||||
selectedLogLevel() {
|
|
||||||
const decodedParams = decodeSearchParams(this.$route.query);
|
|
||||||
const levelFilters = decodedParams.filter(item => item?.field === "level");
|
|
||||||
const decoded = levelFilters.length > 0 ? levelFilters[0]?.value : "INFO";
|
|
||||||
return this.logLevel || decoded || localStorage.getItem("defaultLogLevel") || "INFO";
|
|
||||||
},
|
|
||||||
endDate() {
|
|
||||||
if (this.$route.query.endDate) {
|
|
||||||
return this.$route.query.endDate;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
},
|
|
||||||
startDate() {
|
|
||||||
// we mention the last refresh date here to trick
|
|
||||||
// VueJs fine grained reactivity system and invalidate
|
|
||||||
// computed property startDate
|
|
||||||
if (this.$route.query.startDate && this.lastRefreshDate) {
|
|
||||||
return this.$route.query.startDate;
|
|
||||||
}
|
|
||||||
if (this.$route.query.timeRange) {
|
|
||||||
return this.$moment().subtract(this.$moment.duration(this.$route.query.timeRange).as("milliseconds")).toISOString(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// the default is PT30D
|
const route = useRoute();
|
||||||
return this.$moment().subtract(7, "days").toISOString(true);
|
const {t} = useI18n();
|
||||||
},
|
const logsStore = useLogsStore();
|
||||||
namespace() {
|
const logFilter = useLogFilter();
|
||||||
return this.$route.params.namespace ?? this.$route.params.id;
|
|
||||||
},
|
|
||||||
flowId() {
|
|
||||||
return this.$route.params.id;
|
|
||||||
},
|
|
||||||
charts() {
|
|
||||||
return [
|
|
||||||
{...YAML_UTILS.parse(YAML_CHART), content: YAML_CHART}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
beforeRouteEnter(to: any, _: any, next: (route?: any) => void) {
|
|
||||||
const query = {...to.query};
|
|
||||||
let queryHasChanged = false;
|
|
||||||
|
|
||||||
const queryKeys = Object.keys(query);
|
const routeInfo = computed(() => ({
|
||||||
if (defaultNamespace() && !queryKeys.some(key => key.startsWith("filters[namespace]"))) {
|
title: t("logs"),
|
||||||
query["filters[namespace][PREFIX]"] = defaultNamespace();
|
}));
|
||||||
queryHasChanged = true;
|
useRouteContext(routeInfo, props.embed);
|
||||||
}
|
|
||||||
|
|
||||||
if (queryHasChanged) {
|
const isLoading = ref(false);
|
||||||
next({
|
const lastRefreshDate = ref(new Date());
|
||||||
...to,
|
const showChart = ref(localStorage.getItem(storageKeys.SHOW_LOGS_CHART) !== "false");
|
||||||
query,
|
const dashboardRef = useTemplateRef("dashboard");
|
||||||
replace: true
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
showStatChart() {
|
|
||||||
return this.showChart;
|
|
||||||
},
|
|
||||||
onShowChartChange(value: boolean) {
|
|
||||||
this.showChart = value;
|
|
||||||
localStorage.setItem(storageKeys.SHOW_LOGS_CHART, value.toString());
|
|
||||||
if (this.showStatChart()) {
|
|
||||||
this.load();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
refresh() {
|
|
||||||
this.lastRefreshDate = new Date();
|
|
||||||
if (this.$refs.dashboard) {
|
|
||||||
this.$refs.dashboard.refreshCharts();
|
|
||||||
}
|
|
||||||
this.load();
|
|
||||||
},
|
|
||||||
loadQuery(base: any) {
|
|
||||||
let queryFilter = this.filters ?? this.queryWithFilter();
|
|
||||||
|
|
||||||
if (this.isFlowEdit) {
|
const isFlowEdit = computed(() => route.name === "flows/update");
|
||||||
queryFilter["filters[namespace][EQUALS]"] = this.namespace;
|
const isNamespaceEdit = computed(() => route.name === "namespaces/update");
|
||||||
queryFilter["filters[flowId][EQUALS]"] = this.flowId;
|
const selectedLogLevel = computed(() => {
|
||||||
} else if (this.isNamespaceEdit) {
|
const decodedParams = decodeSearchParams(route.query);
|
||||||
queryFilter["filters[namespace][EQUALS]"] = this.namespace;
|
const levelFilters = decodedParams.filter(item => item?.field === "level");
|
||||||
}
|
const decoded = levelFilters.length > 0 ? levelFilters[0]?.value : "INFO";
|
||||||
|
return props.logLevel || decoded || localStorage.getItem("defaultLogLevel") || "INFO";
|
||||||
|
});
|
||||||
|
const endDate = computed(() => {
|
||||||
|
if (route.query.endDate) {
|
||||||
|
return route.query.endDate;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
const startDate = computed(() => {
|
||||||
|
// we mention the last refresh date here to trick
|
||||||
|
// VueJs fine grained reactivity system and invalidate
|
||||||
|
// computed property startDate
|
||||||
|
if (route.query.startDate && lastRefreshDate.value) {
|
||||||
|
return route.query.startDate;
|
||||||
|
}
|
||||||
|
if (route.query.timeRange) {
|
||||||
|
return moment().subtract(moment.duration(route.query.timeRange as string).as("milliseconds")).toISOString(true);
|
||||||
|
}
|
||||||
|
|
||||||
if (!queryFilter["startDate"] || !queryFilter["endDate"]) {
|
// the default is PT30D
|
||||||
queryFilter["startDate"] = this.startDate;
|
return moment().subtract(7, "days").toISOString(true);
|
||||||
queryFilter["endDate"] = this.endDate;
|
});
|
||||||
}
|
const flowId = computed(() => route.params.id);
|
||||||
|
const namespace = computed(() => route.params.namespace ?? route.params.id);
|
||||||
|
const charts = computed(() => [
|
||||||
|
{...YAML_UTILS.parse(YAML_CHART), content: YAML_CHART}
|
||||||
|
]);
|
||||||
|
|
||||||
delete queryFilter["level"];
|
const loadQuery = (base: any) => {
|
||||||
|
let queryFilter = props.filters ?? queryWithFilter();
|
||||||
|
|
||||||
return _merge(base, queryFilter)
|
if (isFlowEdit.value) {
|
||||||
},
|
queryFilter["filters[namespace][EQUALS]"] = namespace.value;
|
||||||
load() {
|
queryFilter["filters[flowId][EQUALS]"] = flowId.value;
|
||||||
this.isLoading = true
|
} else if (isNamespaceEdit.value) {
|
||||||
|
queryFilter["filters[namespace][EQUALS]"] = namespace.value;
|
||||||
|
}
|
||||||
|
|
||||||
const data = {
|
if (!queryFilter["startDate"] || !queryFilter["endDate"]) {
|
||||||
page: this.filters ? this.internalPageNumber : this.$route.query.page || this.internalPageNumber,
|
queryFilter["startDate"] = startDate.value;
|
||||||
size: this.filters ? this.internalPageSize : this.$route.query.size || this.internalPageSize,
|
queryFilter["endDate"] = endDate.value;
|
||||||
...this.filters
|
}
|
||||||
};
|
|
||||||
this.logsStore.findLogs(this.loadQuery({
|
|
||||||
...data,
|
|
||||||
minLevel: this.filters ? null : this.selectedLogLevel,
|
|
||||||
sort: "timestamp:desc"
|
|
||||||
}))
|
|
||||||
.finally(() => {
|
|
||||||
this.isLoading = false
|
|
||||||
this.saveRestoreUrl();
|
|
||||||
});
|
|
||||||
|
|
||||||
},
|
delete queryFilter["level"];
|
||||||
},
|
|
||||||
watch: {
|
return _merge(base, queryFilter);
|
||||||
reloadLogs(newValue) {
|
};
|
||||||
if(newValue) this.refresh();
|
|
||||||
},
|
const loadData = (callback?: () => void) => {
|
||||||
|
isLoading.value = true;
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
page: props.filters ? internalPageNumber.value : route.query.page || internalPageNumber.value,
|
||||||
|
size: props.filters ? internalPageSize.value : route.query.size || internalPageSize.value,
|
||||||
|
...props.filters
|
||||||
|
};
|
||||||
|
|
||||||
|
logsStore.findLogs(loadQuery({
|
||||||
|
...data,
|
||||||
|
minLevel: props.filters ? null : selectedLogLevel.value,
|
||||||
|
sort: "timestamp:desc"
|
||||||
|
}))
|
||||||
|
.finally(() => {
|
||||||
|
isLoading.value = false;
|
||||||
|
if (callback) callback();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const {onPageChanged, queryWithFilter, internalPageNumber, internalPageSize} = useDataTableActions({
|
||||||
|
loadData
|
||||||
|
});
|
||||||
|
|
||||||
|
const showStatChart = () => showChart.value;
|
||||||
|
|
||||||
|
const onShowChartChange = (value: boolean) => {
|
||||||
|
showChart.value = value;
|
||||||
|
localStorage.setItem(storageKeys.SHOW_LOGS_CHART, value.toString());
|
||||||
|
if (showStatChart()) {
|
||||||
|
loadData();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const refresh = () => {
|
||||||
|
lastRefreshDate.value = new Date();
|
||||||
|
if (dashboardRef.value) {
|
||||||
|
dashboardRef.value.refreshCharts();
|
||||||
|
}
|
||||||
|
loadData();
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(() => route.query, () => {
|
||||||
|
loadData();
|
||||||
|
}, {deep: true});
|
||||||
|
|
||||||
|
watch(() => props.reloadLogs, (newValue) => {
|
||||||
|
if (newValue) refresh();
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// Load data on mount if not embedded
|
||||||
|
if (!props.embed) {
|
||||||
|
loadData();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
|
|
||||||
const namespace = computed(() => route.params?.id) as Ref<string>;
|
const namespace = computed(() => route.params?.id) as Ref<string>;
|
||||||
|
|
||||||
|
const miscStore = useMiscStore();
|
||||||
const namespacesStore = useNamespacesStore();
|
const namespacesStore = useNamespacesStore();
|
||||||
|
|
||||||
watch(namespace, (newID) => {
|
watch(namespace, (newID) => {
|
||||||
@@ -40,13 +41,12 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
watch(() => route.params.tab, (newTab) => {
|
watch(() => route.params.tab, (newTab) => {
|
||||||
if (newTab === "overview") {
|
if (newTab === "overview" || newTab === "executions") {
|
||||||
const dateTimeKeys = ["startDate", "endDate", "timeRange"];
|
const dateTimeKeys = ["startDate", "endDate", "timeRange"];
|
||||||
|
|
||||||
if (!Object.keys(route.query).some((key) => dateTimeKeys.some((dateTimeKey) => key.includes(dateTimeKey)))) {
|
if (!Object.keys(route.query).some((key) => dateTimeKeys.some((dateTimeKey) => key.includes(dateTimeKey)))) {
|
||||||
const miscStore = useMiscStore();
|
const DEFAULT_DURATION = miscStore.configs?.chartDefaultDuration ?? "P30D";
|
||||||
const defaultDuration = miscStore.configs?.chartDefaultDuration || "P30D";
|
const newQuery = {...route.query, "filters[timeRange][EQUALS]": DEFAULT_DURATION};
|
||||||
const newQuery = {...route.query, "filters[timeRange][EQUALS]": defaultDuration};
|
|
||||||
router.replace({name: route.name, params: route.params, query: newQuery});
|
router.replace({name: route.name, params: route.params, query: newQuery});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user