mirror of
https://github.com/kestra-io/kestra.git
synced 2025-12-26 14:00:23 -05:00
Compare commits
28 Commits
fix/load-i
...
chore/test
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f43947b649 | ||
|
|
18b6b4ce5d | ||
|
|
dd65b4697e | ||
|
|
9294c9f885 | ||
|
|
ee63c33ef3 | ||
|
|
d620dd7dec | ||
|
|
02425586d6 | ||
|
|
56d48ddf32 | ||
|
|
1a5c79827b | ||
|
|
08b20fda68 | ||
|
|
7192ad1494 | ||
|
|
f164cddf7a | ||
|
|
c1e18eb490 | ||
|
|
4365a108ac | ||
|
|
bb0e15a2cc | ||
|
|
3ab6d6a94f | ||
|
|
e116186201 | ||
|
|
6439671b91 | ||
|
|
c044634381 | ||
|
|
776ea0a93c | ||
|
|
a799ef8b64 | ||
|
|
e2e4335771 | ||
|
|
f8b0d4217f | ||
|
|
c594aa6764 | ||
|
|
d09bf5ac96 | ||
|
|
ef0a4e6b1a | ||
|
|
5f81c19fc7 | ||
|
|
701f7e22d8 |
9
.github/ISSUE_TEMPLATE/bug.yml
vendored
9
.github/ISSUE_TEMPLATE/bug.yml
vendored
@@ -1,5 +1,8 @@
|
||||
name: Bug report
|
||||
description: File a bug report
|
||||
description: Report a bug or unexpected behavior in the project
|
||||
|
||||
labels: ["bug", "area/backend", "area/frontend"]
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
@@ -20,7 +23,3 @@ body:
|
||||
- Kestra Version: develop
|
||||
validations:
|
||||
required: false
|
||||
labels:
|
||||
- bug
|
||||
- area/backend
|
||||
- area/frontend
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,4 +1,4 @@
|
||||
contact_links:
|
||||
- name: Chat
|
||||
url: https://kestra.io/slack
|
||||
about: Chat with us on Slack.
|
||||
about: Chat with us on Slack
|
||||
|
||||
9
.github/ISSUE_TEMPLATE/feature.yml
vendored
9
.github/ISSUE_TEMPLATE/feature.yml
vendored
@@ -1,5 +1,8 @@
|
||||
name: Feature request
|
||||
description: Create a new feature request
|
||||
description: Suggest a new feature or improvement to enhance the project
|
||||
|
||||
labels: ["enhancement", "area/backend", "area/frontend"]
|
||||
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
@@ -7,7 +10,3 @@ body:
|
||||
placeholder: Tell us more about your feature request. Don't forget to give us a star! ⭐
|
||||
validations:
|
||||
required: true
|
||||
labels:
|
||||
- enhancement
|
||||
- area/backend
|
||||
- area/frontend
|
||||
|
||||
@@ -64,7 +64,8 @@ jobs:
|
||||
cd kestra
|
||||
|
||||
# Create and push release branch
|
||||
git checkout -b "$PUSH_RELEASE_BRANCH";
|
||||
git checkout -B "$PUSH_RELEASE_BRANCH";
|
||||
git pull origin "$PUSH_RELEASE_BRANCH" --rebase || echo "No existing branch to pull";
|
||||
git push -u origin "$PUSH_RELEASE_BRANCH";
|
||||
|
||||
# Run gradle release
|
||||
|
||||
12
.github/workflows/release-docker.yml
vendored
12
.github/workflows/release-docker.yml
vendored
@@ -13,11 +13,11 @@ on:
|
||||
required: true
|
||||
type: boolean
|
||||
default: false
|
||||
plugin-version:
|
||||
description: 'Plugin version'
|
||||
required: false
|
||||
type: string
|
||||
default: "LATEST"
|
||||
dry-run:
|
||||
description: 'Dry run mode that will not write or release anything'
|
||||
required: true
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
publish-docker:
|
||||
@@ -25,9 +25,9 @@ jobs:
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: kestra-io/actions/.github/workflows/kestra-oss-publish-docker.yml@main
|
||||
with:
|
||||
plugin-version: ${{ inputs.plugin-version }}
|
||||
retag-latest: ${{ inputs.retag-latest }}
|
||||
retag-lts: ${{ inputs.retag-lts }}
|
||||
dry-run: ${{ inputs.dry-run }}
|
||||
secrets:
|
||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
package io.kestra.cli.commands.migrations;
|
||||
|
||||
import io.kestra.cli.AbstractCommand;
|
||||
import jakarta.inject.Inject;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import picocli.CommandLine;
|
||||
|
||||
@CommandLine.Command(
|
||||
name = "metadata",
|
||||
description = "populate metadata for entities"
|
||||
)
|
||||
@Slf4j
|
||||
public class MetadataMigrationCommand extends AbstractCommand {
|
||||
@Inject
|
||||
private MetadataMigrationService metadataMigrationService;
|
||||
|
||||
@Override
|
||||
public Integer call() throws Exception {
|
||||
super.call();
|
||||
int returnCode = metadataMigrationService.migrateMetadata();
|
||||
if (returnCode == 0) {
|
||||
System.out.println("✅ Metadata migration complete.");
|
||||
}
|
||||
return returnCode;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package io.kestra.cli.commands.migrations;
|
||||
|
||||
import io.kestra.cli.AbstractCommand;
|
||||
import io.kestra.cli.App;
|
||||
import io.kestra.cli.commands.migrations.metadata.MetadataMigrationCommand;
|
||||
import io.micronaut.configuration.picocli.PicocliRunner;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package io.kestra.cli.commands.migrations.metadata;
|
||||
|
||||
import io.kestra.cli.AbstractCommand;
|
||||
import jakarta.inject.Inject;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import picocli.CommandLine;
|
||||
|
||||
@CommandLine.Command(
|
||||
name = "kv",
|
||||
description = "populate metadata for KV"
|
||||
)
|
||||
@Slf4j
|
||||
public class KvMetadataMigrationCommand extends AbstractCommand {
|
||||
@Inject
|
||||
private MetadataMigrationService metadataMigrationService;
|
||||
|
||||
@Override
|
||||
public Integer call() throws Exception {
|
||||
super.call();
|
||||
try {
|
||||
metadataMigrationService.kvMigration();
|
||||
} catch (Exception e) {
|
||||
System.err.println("❌ KV Metadata migration failed: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
return 1;
|
||||
}
|
||||
System.out.println("✅ KV Metadata migration complete.");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package io.kestra.cli.commands.migrations.metadata;
|
||||
|
||||
import io.kestra.cli.AbstractCommand;
|
||||
import jakarta.inject.Inject;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import picocli.CommandLine;
|
||||
|
||||
@CommandLine.Command(
|
||||
name = "metadata",
|
||||
description = "populate metadata for entities",
|
||||
subcommands = {
|
||||
KvMetadataMigrationCommand.class,
|
||||
SecretsMetadataMigrationCommand.class
|
||||
}
|
||||
)
|
||||
@Slf4j
|
||||
public class MetadataMigrationCommand extends AbstractCommand {
|
||||
@Override
|
||||
public Integer call() throws Exception {
|
||||
super.call();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package io.kestra.cli.commands.migrations;
|
||||
package io.kestra.cli.commands.migrations.metadata;
|
||||
|
||||
import io.kestra.core.models.kv.PersistedKvMetadata;
|
||||
import io.kestra.core.repositories.FlowRepositoryInterface;
|
||||
@@ -22,6 +22,7 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static io.kestra.core.utils.Rethrow.throwConsumer;
|
||||
import static io.kestra.core.utils.Rethrow.throwFunction;
|
||||
|
||||
@Singleton
|
||||
@@ -43,19 +44,7 @@ public class MetadataMigrationService {
|
||||
return Map.of(tenantId, flowRepository.findDistinctNamespace(tenantId));
|
||||
}
|
||||
|
||||
public int migrateMetadata() {
|
||||
try {
|
||||
kvMigration();
|
||||
} catch (IOException e) {
|
||||
System.err.println("❌ KV metadata migration failed: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private void kvMigration() throws IOException {
|
||||
public void kvMigration() throws IOException {
|
||||
this.namespacesPerTenant().entrySet().stream()
|
||||
.flatMap(namespacesForTenant -> namespacesForTenant.getValue().stream().map(namespace -> Map.entry(namespacesForTenant.getKey(), namespace)))
|
||||
.flatMap(throwFunction(namespaceForTenant -> {
|
||||
@@ -79,7 +68,15 @@ public class MetadataMigrationService {
|
||||
|
||||
return entriesByIsExpired.get(false).stream().map(kvEntry -> PersistedKvMetadata.from(namespaceForTenant.getKey(), kvEntry));
|
||||
}))
|
||||
.forEach(kvMetadataRepository::save);
|
||||
.forEach(throwConsumer(kvMetadata -> {
|
||||
if (kvMetadataRepository.findByName(kvMetadata.getTenantId(), kvMetadata.getNamespace(), kvMetadata.getName()).isEmpty()) {
|
||||
kvMetadataRepository.save(kvMetadata);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public void secretMigration() throws Exception {
|
||||
throw new UnsupportedOperationException("Secret migration is not needed in the OSS version");
|
||||
}
|
||||
|
||||
private static List<FileAttributes> listAllFromStorage(StorageInterface storage, String tenant, String namespace) throws IOException {
|
||||
@@ -0,0 +1,30 @@
|
||||
package io.kestra.cli.commands.migrations.metadata;
|
||||
|
||||
import io.kestra.cli.AbstractCommand;
|
||||
import jakarta.inject.Inject;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import picocli.CommandLine;
|
||||
|
||||
@CommandLine.Command(
|
||||
name = "secrets",
|
||||
description = "populate metadata for secrets"
|
||||
)
|
||||
@Slf4j
|
||||
public class SecretsMetadataMigrationCommand extends AbstractCommand {
|
||||
@Inject
|
||||
private MetadataMigrationService metadataMigrationService;
|
||||
|
||||
@Override
|
||||
public Integer call() throws Exception {
|
||||
super.call();
|
||||
try {
|
||||
metadataMigrationService.secretMigration();
|
||||
} catch (Exception e) {
|
||||
System.err.println("❌ Secrets Metadata migration failed: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
return 1;
|
||||
}
|
||||
System.out.println("✅ Secrets Metadata migration complete.");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -62,7 +62,7 @@ public class KvUpdateCommand extends AbstractApiCommand {
|
||||
Duration ttl = expiration == null ? null : Duration.parse(expiration);
|
||||
MutableHttpRequest<String> request = HttpRequest
|
||||
.PUT(apiUri("/namespaces/", tenantService.getTenantId(tenantId)) + namespace + "/kv/" + key, value)
|
||||
.contentType(MediaType.APPLICATION_JSON_TYPE);
|
||||
.contentType(MediaType.TEXT_PLAIN);
|
||||
|
||||
if (ttl != null) {
|
||||
request.header("ttl", ttl.toString());
|
||||
|
||||
@@ -243,6 +243,10 @@ kestra:
|
||||
ui-anonymous-usage-report:
|
||||
enabled: true
|
||||
|
||||
ui:
|
||||
charts:
|
||||
default-duration: P30D
|
||||
|
||||
anonymous-usage-report:
|
||||
enabled: true
|
||||
uri: https://api.kestra.io/v1/reports/server-events
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package io.kestra.cli.commands.migrations;
|
||||
package io.kestra.cli.commands.migrations.metadata;
|
||||
|
||||
import io.kestra.cli.App;
|
||||
import io.kestra.core.exceptions.ResourceExpiredException;
|
||||
@@ -33,7 +33,7 @@ import java.util.Optional;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class MetadataMigrationCommandTest {
|
||||
public class KvMetadataMigrationCommandTest {
|
||||
@Test
|
||||
void run() throws IOException, ResourceExpiredException {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
@@ -42,6 +42,11 @@ public class MetadataMigrationCommandTest {
|
||||
System.setErr(new PrintStream(err));
|
||||
|
||||
try (ApplicationContext ctx = ApplicationContext.run(Environment.CLI, Environment.TEST)) {
|
||||
/* Initial setup:
|
||||
* - namespace 1: key, description, value
|
||||
* - namespace 1: expiredKey
|
||||
* - namespace 2: anotherKey, anotherDescription
|
||||
* - Nothing in database */
|
||||
String namespace = TestsUtils.randomNamespace();
|
||||
String key = "myKey";
|
||||
StorageInterface storage = ctx.getBean(StorageInterface.class);
|
||||
@@ -64,17 +69,20 @@ public class MetadataMigrationCommandTest {
|
||||
KvMetadataRepositoryInterface kvMetadataRepository = ctx.getBean(KvMetadataRepositoryInterface.class);
|
||||
assertThat(kvMetadataRepository.findByName(tenantId, namespace, key).isPresent()).isFalse();
|
||||
|
||||
/* Expected outcome from the migration command:
|
||||
* - no KV has been migrated because no flow exist in the namespace so they are not picked up because we don't know they exist */
|
||||
String[] kvMetadataMigrationCommand = {
|
||||
"migrate", "metadata"
|
||||
"migrate", "metadata", "kv"
|
||||
};
|
||||
PicocliRunner.call(App.class, ctx, kvMetadataMigrationCommand);
|
||||
|
||||
|
||||
assertThat(out.toString()).contains("✅ Metadata migration complete.");
|
||||
assertThat(out.toString()).contains("✅ KV Metadata migration complete.");
|
||||
// Still it's not in the metadata repository because no flow exist to find that kv
|
||||
assertThat(kvMetadataRepository.findByName(tenantId, namespace, key).isPresent()).isFalse();
|
||||
assertThat(kvMetadataRepository.findByName(tenantId, anotherNamespace, anotherKey).isPresent()).isFalse();
|
||||
|
||||
// A flow is created from namespace 1, so the KV in this namespace should be migrated
|
||||
FlowRepositoryInterface flowRepository = ctx.getBean(FlowRepositoryInterface.class);
|
||||
flowRepository.create(GenericFlow.of(Flow.builder()
|
||||
.tenantId(tenantId)
|
||||
@@ -83,13 +91,18 @@ public class MetadataMigrationCommandTest {
|
||||
.tasks(List.of(Log.builder().id("log").type(Log.class.getName()).message("logging").build()))
|
||||
.build()));
|
||||
|
||||
/* We run the migration again:
|
||||
* - namespace 1 KV is seen and metadata is migrated to database
|
||||
* - namespace 2 KV is not seen because no flow exist in this namespace
|
||||
* - expiredKey is deleted from storage and not migrated */
|
||||
out.reset();
|
||||
PicocliRunner.call(App.class, ctx, kvMetadataMigrationCommand);
|
||||
|
||||
assertThat(out.toString()).contains("✅ Metadata migration complete.");
|
||||
assertThat(out.toString()).contains("✅ KV Metadata migration complete.");
|
||||
Optional<PersistedKvMetadata> foundKv = kvMetadataRepository.findByName(tenantId, namespace, key);
|
||||
assertThat(foundKv.isPresent()).isTrue();
|
||||
assertThat(foundKv.get().getDescription()).isEqualTo(description);
|
||||
|
||||
assertThat(kvMetadataRepository.findByName(tenantId, anotherNamespace, anotherKey).isPresent()).isFalse();
|
||||
|
||||
KVStore kvStore = new InternalKVStore(tenantId, namespace, storage, kvMetadataRepository);
|
||||
@@ -103,6 +116,15 @@ public class MetadataMigrationCommandTest {
|
||||
|
||||
assertThat(kvMetadataRepository.findByName(tenantId, namespace, expiredKey).isPresent()).isFalse();
|
||||
assertThat(storage.exists(tenantId, null, getKvStorageUri(namespace, expiredKey))).isFalse();
|
||||
|
||||
/* We run one last time the migration without any change to verify that we don't resave an existing metadata.
|
||||
* It covers the case where user didn't perform the migrate command yet but they played and added some KV from the UI (so those ones will already be in metadata database). */
|
||||
out.reset();
|
||||
PicocliRunner.call(App.class, ctx, kvMetadataMigrationCommand);
|
||||
|
||||
assertThat(out.toString()).contains("✅ KV Metadata migration complete.");
|
||||
foundKv = kvMetadataRepository.findByName(tenantId, namespace, key);
|
||||
assertThat(foundKv.get().getVersion()).isEqualTo(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package io.kestra.cli.commands.migrations.metadata;
|
||||
|
||||
import io.kestra.cli.App;
|
||||
import io.micronaut.configuration.picocli.PicocliRunner;
|
||||
import io.micronaut.context.ApplicationContext;
|
||||
import io.micronaut.context.env.Environment;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.PrintStream;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class SecretsMetadataMigrationCommandTest {
|
||||
@Test
|
||||
void run() {
|
||||
ByteArrayOutputStream err = new ByteArrayOutputStream();
|
||||
System.setErr(new PrintStream(err));
|
||||
|
||||
try (ApplicationContext ctx = ApplicationContext.run(Environment.CLI, Environment.TEST)) {
|
||||
String[] secretMetadataMigrationCommand = {
|
||||
"migrate", "metadata", "secrets"
|
||||
};
|
||||
PicocliRunner.call(App.class, ctx, secretMetadataMigrationCommand);
|
||||
|
||||
assertThat(err.toString()).contains("❌ Secrets Metadata migration failed: Secret migration is not needed in the OSS version");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import io.kestra.core.utils.IdUtils;
|
||||
import io.kestra.core.utils.ListUtils;
|
||||
import io.kestra.core.utils.MapUtils;
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.annotation.Nullable;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
@@ -77,10 +78,12 @@ public class Execution implements DeletedInterface, TenantInterface {
|
||||
|
||||
@With
|
||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||
@Schema(implementation = Object.class)
|
||||
Map<String, Object> inputs;
|
||||
|
||||
@With
|
||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||
@Schema(implementation = Object.class)
|
||||
Map<String, Object> outputs;
|
||||
|
||||
@JsonSerialize(using = ListOrMapOfLabelSerializer.class)
|
||||
@@ -88,6 +91,7 @@ public class Execution implements DeletedInterface, TenantInterface {
|
||||
List<Label> labels;
|
||||
|
||||
@With
|
||||
@Schema(implementation = Object.class)
|
||||
Map<String, Object> variables;
|
||||
|
||||
@NotNull
|
||||
|
||||
@@ -9,6 +9,7 @@ import io.kestra.core.models.tasks.Task;
|
||||
import io.kestra.core.models.tasks.retrys.AbstractRetry;
|
||||
import io.kestra.core.utils.IdUtils;
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.annotation.Nullable;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
@@ -55,6 +56,7 @@ public class TaskRun implements TenantInterface {
|
||||
@With
|
||||
@JsonInclude(JsonInclude.Include.ALWAYS)
|
||||
@Nullable
|
||||
@Schema(implementation = Object.class)
|
||||
Variables outputs;
|
||||
|
||||
@NotNull
|
||||
|
||||
@@ -2,6 +2,7 @@ package io.kestra.core.plugins;
|
||||
|
||||
import io.kestra.core.contexts.KestraContext;
|
||||
import io.kestra.core.utils.ListUtils;
|
||||
import io.kestra.core.utils.Version;
|
||||
import io.micronaut.core.type.Argument;
|
||||
import io.micronaut.http.HttpMethod;
|
||||
import io.micronaut.http.HttpRequest;
|
||||
@@ -45,6 +46,8 @@ public class PluginCatalogService {
|
||||
|
||||
private final boolean icons;
|
||||
private final boolean oss;
|
||||
|
||||
private final Version currentStableVersion;
|
||||
|
||||
/**
|
||||
* Creates a new {@link PluginCatalogService} instance.
|
||||
@@ -59,7 +62,10 @@ public class PluginCatalogService {
|
||||
this.httpClient = httpClient;
|
||||
this.icons = icons;
|
||||
this.oss = communityOnly;
|
||||
|
||||
|
||||
Version version = Version.of(KestraContext.getContext().getVersion());
|
||||
this.currentStableVersion = new Version(version.majorVersion(), version.minorVersion(), version.patchVersion(), null);
|
||||
|
||||
// Immediately trigger an async load of plugin artifacts.
|
||||
this.isLoaded.set(true);
|
||||
this.plugins = CompletableFuture.supplyAsync(this::load);
|
||||
@@ -189,9 +195,10 @@ public class PluginCatalogService {
|
||||
}
|
||||
|
||||
private List<ApiPluginArtifact> getAllCompatiblePlugins() {
|
||||
|
||||
MutableHttpRequest<Object> request = HttpRequest.create(
|
||||
HttpMethod.GET,
|
||||
"/v1/plugins/artifacts/core-compatibility/" + KestraContext.getContext().getVersion()
|
||||
"/v1/plugins/artifacts/core-compatibility/" + currentStableVersion
|
||||
);
|
||||
if (oss) {
|
||||
request.getParameters().add("license", "OPENSOURCE");
|
||||
|
||||
@@ -109,6 +109,17 @@ public class FlowValidator implements ConstraintValidator<FlowValidation, Flow>
|
||||
violations.add("Duplicate output with name [" + String.join(", ", duplicateIds) + "]");
|
||||
}
|
||||
|
||||
// preconditions unique id
|
||||
duplicateIds = getDuplicates(ListUtils.emptyOnNull(value.getTriggers()).stream()
|
||||
.filter(it -> it instanceof io.kestra.plugin.core.trigger.Flow)
|
||||
.map(it -> (io.kestra.plugin.core.trigger.Flow) it)
|
||||
.filter(it -> it.getPreconditions() != null && it.getPreconditions().getId() != null)
|
||||
.map(it -> it.getPreconditions().getId())
|
||||
.toList());
|
||||
if (!duplicateIds.isEmpty()) {
|
||||
violations.add("Duplicate preconditions with id [" + String.join(", ", duplicateIds) + "]");
|
||||
}
|
||||
|
||||
// system labels
|
||||
ListUtils.emptyOnNull(value.getLabels()).stream()
|
||||
.filter(label -> label.key() != null && label.key().startsWith(SYSTEM_PREFIX) && !label.key().equals(READ_ONLY))
|
||||
|
||||
@@ -52,10 +52,11 @@ import java.util.OptionalInt;
|
||||
- id: basic_auth_api
|
||||
type: io.kestra.plugin.core.http.Request
|
||||
uri: http://host.docker.internal:8080/api/v1/executions/dev/inputs_demo
|
||||
auth:
|
||||
type: BASIC
|
||||
username: "{{ secret('API_USERNAME') }}"
|
||||
password: "{{ secret('API_PASSWORD') }}"
|
||||
options:
|
||||
auth:
|
||||
type: BASIC
|
||||
username: "{{ secret('API_USERNAME') }}"
|
||||
password: "{{ secret('API_PASSWORD') }}"
|
||||
method: POST
|
||||
contentType: multipart/form-data
|
||||
formData:
|
||||
|
||||
@@ -52,7 +52,10 @@ tasks:
|
||||
message: |
|
||||
Got the following outputs from the previous task:
|
||||
{{ outputs.output_values.values.taskrun_data }}
|
||||
{{ outputs.output_values.values.execution_data }}"""
|
||||
{{ outputs.output_values.values.execution_data }}
|
||||
{{ outputs.output_values.values.number_value }}
|
||||
{{ outputs.output_values.values.array_value[1] }}
|
||||
{{ outputs.output_values.values.nested_object.key2 }}"""
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -261,6 +261,12 @@ public abstract class AbstractRunnerTest {
|
||||
multipleConditionTriggerCaseTest.forEachItemWithFlowTrigger();
|
||||
}
|
||||
|
||||
@Test
|
||||
@LoadFlows({"flows/valids/flow-trigger-multiple-preconditions-flow-a.yaml", "flows/valids/flow-trigger-multiple-preconditions-flow-listen.yaml"})
|
||||
void flowTriggerMultiplePreconditions() throws Exception {
|
||||
multipleConditionTriggerCaseTest.flowTriggerMultiplePreconditions();
|
||||
}
|
||||
|
||||
@Test
|
||||
@LoadFlows({"flows/valids/each-null.yaml"})
|
||||
void eachWithNull() throws Exception {
|
||||
|
||||
@@ -11,6 +11,7 @@ import io.kestra.core.repositories.FlowRepositoryInterface;
|
||||
|
||||
import io.micronaut.data.model.Pageable;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
@@ -20,6 +21,7 @@ import jakarta.inject.Singleton;
|
||||
|
||||
import static io.kestra.core.tenant.TenantService.MAIN_TENANT;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
@Singleton
|
||||
public class MultipleConditionTriggerCaseTest {
|
||||
@@ -190,4 +192,24 @@ public class MultipleConditionTriggerCaseTest {
|
||||
assertThat(exec.getTaskRunList().size()).isEqualTo(2);
|
||||
});
|
||||
}
|
||||
|
||||
public void flowTriggerMultiplePreconditions() throws TimeoutException, QueueException {
|
||||
Execution execution = runnerUtils.runOne(MAIN_TENANT, "io.kestra.tests.trigger.multiple.preconditions",
|
||||
"flow-trigger-multiple-preconditions-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.preconditions", "flow-trigger-multiple-preconditions-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.preconditions", "flow-trigger-multiple-preconditions-flow-listen", Duration.ofSeconds(1)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,27 +4,19 @@ import io.kestra.core.junit.annotations.ExecuteFlow;
|
||||
import io.kestra.core.junit.annotations.KestraTest;
|
||||
import io.kestra.core.junit.annotations.LoadFlows;
|
||||
import io.kestra.core.models.executions.Execution;
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.models.flows.State;
|
||||
import io.kestra.core.queues.QueueException;
|
||||
import io.kestra.core.queues.QueueFactoryInterface;
|
||||
import io.kestra.core.queues.QueueInterface;
|
||||
import io.kestra.core.repositories.FlowRepositoryInterface;
|
||||
import io.kestra.core.runners.ConcurrencyLimit;
|
||||
import io.kestra.core.runners.RunnerUtils;
|
||||
import io.kestra.core.runners.TestRunnerUtils;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Named;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestInstance;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import static io.kestra.core.utils.Rethrow.throwRunnable;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@KestraTest(startRunner = true)
|
||||
@@ -34,14 +26,7 @@ class ConcurrencyLimitServiceTest {
|
||||
private static final String TENANT_ID = "main";
|
||||
|
||||
@Inject
|
||||
private RunnerUtils runnerUtils;
|
||||
|
||||
@Inject
|
||||
@Named(QueueFactoryInterface.EXECUTION_NAMED)
|
||||
private QueueInterface<Execution> executionQueue;
|
||||
|
||||
@Inject
|
||||
private FlowRepositoryInterface flowRepositoryInterface;
|
||||
private TestRunnerUtils runnerUtils;
|
||||
|
||||
@Inject
|
||||
private ConcurrencyLimitService concurrencyLimitService;
|
||||
@@ -57,7 +42,8 @@ class ConcurrencyLimitServiceTest {
|
||||
void unqueueExecution() throws QueueException, TimeoutException {
|
||||
// run a first flow so the second is queued
|
||||
runnerUtils.runOneUntilRunning(TENANT_ID, TESTS_FLOW_NS, "flow-concurrency-queue");
|
||||
Execution result = runUntilQueued(TESTS_FLOW_NS, "flow-concurrency-queue");
|
||||
|
||||
Execution result = runnerUtils.runOneUntil(TENANT_ID, TESTS_FLOW_NS, "flow-concurrency-queue", execution -> execution.getState().isQueued());
|
||||
assertThat(result.getState().isQueued()).isTrue();
|
||||
|
||||
Execution unqueued = concurrencyLimitService.unqueue(result, State.Type.RUNNING);
|
||||
@@ -101,21 +87,4 @@ class ConcurrencyLimitServiceTest {
|
||||
assertThat(list.getFirst().getNamespace()).isEqualTo(execution.getNamespace());
|
||||
assertThat(list.getFirst().getFlowId()).isEqualTo(execution.getFlowId());
|
||||
}
|
||||
|
||||
private Execution runUntilQueued(String namespace, String flowId) throws TimeoutException, QueueException {
|
||||
return runUntilState(namespace, flowId, State.Type.QUEUED);
|
||||
}
|
||||
|
||||
private Execution runUntilState(String namespace, String flowId, State.Type state) throws TimeoutException, QueueException {
|
||||
Execution execution = this.createExecution(namespace, flowId);
|
||||
return runnerUtils.awaitExecution(
|
||||
it -> execution.getId().equals(it.getId()) && it.getState().getCurrent() == state,
|
||||
throwRunnable(() -> this.executionQueue.emit(execution)),
|
||||
Duration.ofSeconds(1));
|
||||
}
|
||||
|
||||
private Execution createExecution(String namespace, String flowId) {
|
||||
Flow flow = flowRepositoryInterface.findById(TENANT_ID, namespace, flowId).orElseThrow();
|
||||
return Execution.newExecution(flow, null);
|
||||
}
|
||||
}
|
||||
@@ -68,7 +68,7 @@ class FlowValidationTest {
|
||||
|
||||
assertThat(validate.isPresent()).isFalse();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void shouldGetConstraintErrorGivenInputWithBothDefaultsAndPrefill() {
|
||||
// Given
|
||||
@@ -82,15 +82,15 @@ class FlowValidationTest {
|
||||
defaults: "defaults"
|
||||
tasks: []
|
||||
""");
|
||||
|
||||
|
||||
// When
|
||||
Optional<ConstraintViolationException> validate = modelValidator.isValid(flow);
|
||||
|
||||
|
||||
// Then
|
||||
assertThat(validate.isPresent()).isEqualTo(true);
|
||||
assertThat(validate.get().getMessage()).contains("Inputs with a default value cannot also have a prefill.");
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void shouldGetConstraintErrorGivenOptionalInputWithDefault() {
|
||||
// Given
|
||||
@@ -104,15 +104,24 @@ class FlowValidationTest {
|
||||
required: false
|
||||
tasks: []
|
||||
""");
|
||||
|
||||
|
||||
// When
|
||||
Optional<ConstraintViolationException> validate = modelValidator.isValid(flow);
|
||||
|
||||
|
||||
// Then
|
||||
assertThat(validate.isPresent()).isEqualTo(true);
|
||||
assertThat(validate.get().getMessage()).contains("Inputs with a default value must be required, since the default is always applied.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void duplicatePreconditionsIdShouldFailValidation() {
|
||||
Flow flow = this.parse("flows/invalids/duplicate-preconditions.yaml");
|
||||
Optional<ConstraintViolationException> validate = modelValidator.isValid(flow);
|
||||
|
||||
assertThat(validate.isPresent()).isEqualTo(true);
|
||||
assertThat(validate.get().getMessage()).contains("Duplicate preconditions with id [flows]");
|
||||
}
|
||||
|
||||
private Flow parse(String path) {
|
||||
URL resource = TestsUtils.class.getClassLoader().getResource(path);
|
||||
assert resource != null;
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
id: duplicate-preconditions
|
||||
namespace: io.kestra.tests
|
||||
|
||||
tasks:
|
||||
- id: hello
|
||||
type: io.kestra.plugin.core.log.Log
|
||||
message: Hello World! 🚀
|
||||
|
||||
triggers:
|
||||
- id: on_completion
|
||||
type: io.kestra.plugin.core.trigger.Flow
|
||||
preconditions:
|
||||
id: flows
|
||||
flows:
|
||||
- namespace: io.kestra.tests
|
||||
flowId: flow_a
|
||||
states: [SUCCESS]
|
||||
|
||||
- id: on_failure
|
||||
type: io.kestra.plugin.core.trigger.Flow
|
||||
preconditions:
|
||||
id: flows
|
||||
flows:
|
||||
- namespace: io.kestra.tests
|
||||
flowId: flow_a
|
||||
states: [FAILED]
|
||||
@@ -0,0 +1,10 @@
|
||||
id: flow-trigger-multiple-preconditions-flow-a
|
||||
namespace: io.kestra.tests.trigger.multiple.preconditions
|
||||
|
||||
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-multiple-preconditions-flow-listen
|
||||
namespace: io.kestra.tests.trigger.multiple.preconditions
|
||||
|
||||
triggers:
|
||||
- id: flow1
|
||||
type: io.kestra.plugin.core.trigger.Flow
|
||||
preconditions:
|
||||
id: flow1
|
||||
flows:
|
||||
- namespace: io.kestra.tests.trigger.multiple.preconditions
|
||||
flowId: flow-trigger-multiple-preconditions-flow-a
|
||||
states: [SUCCESS]
|
||||
- id: flow2
|
||||
type: io.kestra.plugin.core.trigger.Flow
|
||||
preconditions:
|
||||
id: flow2
|
||||
flows:
|
||||
- namespace: io.kestra.tests.trigger.multiple.preconditions
|
||||
flowId: flow-trigger-multiple-preconditions-flow-a
|
||||
states: [FAILED]
|
||||
|
||||
tasks:
|
||||
- id: only
|
||||
type: io.kestra.plugin.core.debug.Return
|
||||
format: "It works"
|
||||
@@ -1,4 +1,4 @@
|
||||
version=1.1.0-SNAPSHOT
|
||||
version=1.2.0-SNAPSHOT
|
||||
|
||||
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||
org.gradle.parallel=true
|
||||
|
||||
@@ -1238,6 +1238,7 @@ public class JdbcExecutor implements ExecutorInterface {
|
||||
flowTriggerService.withFlowTriggersOnly(allFlows.stream())
|
||||
.filter(f -> ListUtils.emptyOnNull(f.getTrigger().getConditions()).stream().anyMatch(c -> c instanceof MultipleCondition) || f.getTrigger().getPreconditions() != null)
|
||||
.map(f -> new MultipleConditionEvent(f.getFlow(), execution))
|
||||
.distinct() // we can have multiple MultipleConditionEvent if a flow contains multiple triggers as it would lead to multiple FlowWithFlowTrigger
|
||||
.forEach(throwConsumer(multipleCondition -> multipleConditionEventQueue.emit(multipleCondition)));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package io.kestra.jdbc.runner;
|
||||
|
||||
import io.kestra.core.junit.annotations.ExecuteFlow;
|
||||
import io.kestra.core.junit.annotations.FlakyTest;
|
||||
import io.kestra.core.junit.annotations.KestraTest;
|
||||
import io.kestra.core.junit.annotations.LoadFlows;
|
||||
import io.kestra.core.models.executions.Execution;
|
||||
@@ -138,6 +139,7 @@ public abstract class JdbcRunnerRetryTest {
|
||||
retryCaseTest.retryDynamicTask(execution);
|
||||
}
|
||||
|
||||
@FlakyTest(description = "it seems this flow sometimes stay stuck in RUNNING")
|
||||
@Test
|
||||
@ExecuteFlow("flows/valids/retry-with-flowable-errors.yaml")
|
||||
void retryWithFlowableErrors(Execution execution){
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package io.kestra.core.runners;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import io.kestra.core.models.Label;
|
||||
import io.kestra.core.models.executions.Execution;
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
@@ -162,6 +161,34 @@ public class TestRunnerUtils {
|
||||
return this.emitAndAwaitExecution(isRunningExecution(execution), execution, duration);
|
||||
}
|
||||
|
||||
public Execution runOneUntil(String tenantId, String namespace, String flowId, Predicate<Execution> predicate)
|
||||
throws QueueException {
|
||||
return this.runOneUntil(tenantId, namespace, flowId, null, null, null, predicate);
|
||||
}
|
||||
|
||||
public Execution runOneUntil(String tenantId, String namespace, String flowId, Integer revision, BiFunction<FlowInterface, Execution, Map<String, Object>> inputs, Duration duration, Predicate<Execution> predicate)
|
||||
throws QueueException {
|
||||
return this.runOneUntil(
|
||||
flowRepository
|
||||
.findById(tenantId, namespace, flowId, revision != null ? Optional.of(revision) : Optional.empty())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Unable to find flow '" + flowId + "'")),
|
||||
inputs,
|
||||
duration,
|
||||
predicate
|
||||
);
|
||||
}
|
||||
|
||||
public Execution runOneUntil(Flow flow, BiFunction<FlowInterface, Execution, Map<String, Object>> inputs, Duration duration, Predicate<Execution> predicate)
|
||||
throws QueueException {
|
||||
if (duration == null) {
|
||||
duration = DEFAULT_MAX_WAIT_DURATION;
|
||||
}
|
||||
|
||||
Execution execution = Execution.newExecution(flow, inputs, null, Optional.empty());
|
||||
|
||||
return this.emitAndAwaitExecution(predicate, execution, duration);
|
||||
}
|
||||
|
||||
public Execution emitAndAwaitExecution(Predicate<Execution> predicate, Execution execution) throws QueueException {
|
||||
return emitAndAwaitExecution(predicate, execution, Duration.ofSeconds(20));
|
||||
}
|
||||
@@ -300,7 +327,6 @@ public class TestRunnerUtils {
|
||||
return receive.get();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public Execution awaitChildExecution(Flow flow, Execution parentExecution, Execution execution, Duration duration)
|
||||
throws QueueException {
|
||||
return this.emitAndAwaitExecution(isTerminatedChildExecution(parentExecution, flow), execution, duration);
|
||||
|
||||
@@ -445,12 +445,6 @@
|
||||
sort: String(route.query?.sort ?? "triggerId:asc")
|
||||
});
|
||||
|
||||
for (const key in query) {
|
||||
if (key.startsWith("filters[trigger_state]")) {
|
||||
delete query[key];
|
||||
}
|
||||
}
|
||||
|
||||
const previousSelection = selection.value;
|
||||
triggerStore.search(query).then(async triggersData => {
|
||||
triggers.value = triggersData?.results;
|
||||
@@ -747,10 +741,7 @@
|
||||
};
|
||||
}) ?? [];
|
||||
|
||||
if (!route.query?.["filters[trigger_state][EQUALS]"]) return all;
|
||||
|
||||
const disabled = String(route.query["filters[trigger_state][EQUALS]"]) === "DISABLED" ? true : false;
|
||||
return all.filter((trigger: any) => trigger.disabled === disabled);
|
||||
return all;
|
||||
});
|
||||
|
||||
watch(ready, (newReady: any) => {
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
@import "@kestra-io/ui-libs/src/scss/variables";
|
||||
|
||||
.filterPadding {
|
||||
margin: 2rem 0.25rem 0;
|
||||
margin-top: 1.5rem;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -130,7 +130,7 @@
|
||||
import Utils from "../../utils/utils";
|
||||
import LogLine from "../logs/LogLine.vue";
|
||||
import Restart from "./Restart.vue";
|
||||
import LogUtils from "../../utils/logs";
|
||||
import * as LogUtils from "../../utils/logs";
|
||||
import Refresh from "vue-material-design-icons/Refresh.vue";
|
||||
import {mapStores} from "pinia";
|
||||
import {useExecutionsStore} from "../../stores/executions";
|
||||
|
||||
@@ -114,7 +114,7 @@
|
||||
.expand-panel {
|
||||
animation: slideDown 0.2s ease-out;
|
||||
border-top: 1px solid var(--ks-border-secondary);
|
||||
padding: 0.5rem 0;
|
||||
padding-top: 0.5rem;
|
||||
|
||||
.options-row {
|
||||
display: flex;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<section class="filter">
|
||||
<div class="top">
|
||||
<div class="top" :class="{'options': showOptions}">
|
||||
<MainFilter />
|
||||
<RightFilter>
|
||||
<template #extra>
|
||||
@@ -180,7 +180,10 @@
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-wrap: nowrap;
|
||||
padding: 1rem 0;
|
||||
|
||||
&.options {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -33,9 +33,10 @@
|
||||
import {
|
||||
AppliedFilter,
|
||||
COMPARATOR_LABELS,
|
||||
Comparators,
|
||||
FilterKeyConfig,
|
||||
FilterValue
|
||||
FilterValue,
|
||||
TEXT_COMPARATORS,
|
||||
KV_COMPARATORS
|
||||
} from "../../utils/filterTypes";
|
||||
import {FILTER_CONTEXT_INJECTION_KEY} from "../../utils/filterInjectionKeys";
|
||||
import FilterText from "./FilterText.vue";
|
||||
@@ -43,7 +44,7 @@
|
||||
import FilterFooter from "./FilterFooter.vue";
|
||||
import FilterHeader from "./FilterHeader.vue";
|
||||
import FilterSelect from "./FilterSelect.vue";
|
||||
import FilterDetails from "./FilterDetails.vue";
|
||||
import FilterKVPairs from "./FilterKVPairs.vue";
|
||||
import FilterDateTime from "./FilterDateTime.vue";
|
||||
import FilterMultiSelect from "./FilterMultiSelect.vue";
|
||||
import FilterComparatorSelect from "./FilterComparatorSelect.vue";
|
||||
@@ -68,7 +69,7 @@
|
||||
selectValue: "",
|
||||
radioValue: "ALL",
|
||||
dateValue: null as Date | null,
|
||||
multiSelectValue: [] as string[],
|
||||
keyValuePair: [] as string[],
|
||||
endDateValue: null as Date | null,
|
||||
valueOptions: [] as FilterValue[],
|
||||
startDateValue: null as Date | null,
|
||||
@@ -77,30 +78,32 @@
|
||||
});
|
||||
|
||||
const shouldShowComparator = computed(
|
||||
() => props.filterKey.showComparatorSelection ?? props.showComparatorSelection ?? false
|
||||
() => props.filterKey?.showComparatorSelection ?? props.showComparatorSelection ?? false
|
||||
);
|
||||
|
||||
const TEXT_COMPARATORS = [
|
||||
Comparators.STARTS_WITH,
|
||||
Comparators.ENDS_WITH,
|
||||
Comparators.CONTAINS
|
||||
];
|
||||
const isTextOp = computed(() =>
|
||||
TEXT_COMPARATORS.includes(state.selectedComparator) && props.filterKey?.key !== "resources"
|
||||
);
|
||||
|
||||
const isTextComparator = computed(() =>
|
||||
TEXT_COMPARATORS.includes(state.selectedComparator) && props.filterKey.key !== "resources"
|
||||
const isKVPairFilter = computed(() =>
|
||||
props.filterKey?.valueType === "key-value" || (props.filterKey?.key === "labels" && KV_COMPARATORS.includes(state.selectedComparator))
|
||||
);
|
||||
|
||||
const valueComponent = computed(() => {
|
||||
if (isTextComparator.value) {
|
||||
if (isTextOp.value) {
|
||||
return {
|
||||
component: FilterText,
|
||||
props: {
|
||||
textValue: state.textValue,
|
||||
label: props.filterKey.label
|
||||
},
|
||||
events: {
|
||||
"update:text-value": (value: string) => (state.textValue = value)
|
||||
}
|
||||
props: {textValue: state.textValue, label: props.filterKey?.label},
|
||||
events: {"update:text-value": (value: string) => (state.textValue = value)}
|
||||
};
|
||||
}
|
||||
|
||||
// Key-value pair filters (details, labels)
|
||||
if (isKVPairFilter.value) {
|
||||
return {
|
||||
component: FilterKVPairs,
|
||||
props: {modelValue: state.keyValuePair},
|
||||
events: {"update:modelValue": (value: string[]) => (state.keyValuePair = value)}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -111,8 +114,8 @@
|
||||
props: {
|
||||
modelValue: state.selectValue,
|
||||
options: state.valueOptions,
|
||||
searchable: props.filterKey.searchable,
|
||||
label: props.filterKey.label,
|
||||
searchable: props.filterKey?.searchable,
|
||||
label: props.filterKey?.label,
|
||||
filterKey: props.filterKey,
|
||||
timeRangeMode: state.timeRangeMode,
|
||||
startDateValue: state.startDateValue,
|
||||
@@ -131,7 +134,7 @@
|
||||
component: FilterText,
|
||||
props: {
|
||||
textValue: state.textValue,
|
||||
label: props.filterKey.label
|
||||
label: props.filterKey?.label
|
||||
},
|
||||
events: {
|
||||
"update:text-value": (value: string) => (state.textValue = value)
|
||||
@@ -140,35 +143,26 @@
|
||||
"multi-select": {
|
||||
component: FilterMultiSelect,
|
||||
props: {
|
||||
modelValue: state.multiSelectValue,
|
||||
modelValue: state.keyValuePair,
|
||||
options: state.valueOptions,
|
||||
searchable: props.filterKey.searchable,
|
||||
label: props.filterKey.label,
|
||||
filterKey: props.filterKey.key
|
||||
searchable: props.filterKey?.searchable,
|
||||
label: props.filterKey?.label,
|
||||
filterKey: props.filterKey?.key
|
||||
},
|
||||
events: {
|
||||
"update:modelValue": (value: string[]) => (state.multiSelectValue = value)
|
||||
"update:modelValue": (value: string[]) => (state.keyValuePair = value)
|
||||
}
|
||||
},
|
||||
date: {
|
||||
component: FilterDateTime,
|
||||
props: {
|
||||
dateValue: state.dateValue,
|
||||
label: props.filterKey.label
|
||||
label: props.filterKey?.label
|
||||
},
|
||||
events: {
|
||||
"update:date-value": (value: Date | null) => (state.dateValue = value)
|
||||
}
|
||||
},
|
||||
details: {
|
||||
component: FilterDetails,
|
||||
props: {
|
||||
modelValue: state.multiSelectValue
|
||||
},
|
||||
events: {
|
||||
"update:modelValue": (value: string[]) => (state.multiSelectValue = value)
|
||||
}
|
||||
},
|
||||
radio: {
|
||||
component: FilterRadio,
|
||||
props: {
|
||||
@@ -187,22 +181,21 @@
|
||||
});
|
||||
|
||||
const footerText = computed(() => {
|
||||
if (isTextComparator.value) {
|
||||
return state.textValue ?? "";
|
||||
if (isTextOp.value) return state.textValue ?? "";
|
||||
|
||||
if (isKVPairFilter.value) {
|
||||
const label = props.filterKey?.label || "key/value";
|
||||
return state.keyValuePair.length > 1
|
||||
? `${state.keyValuePair.length} ${label} pairs`
|
||||
: state.keyValuePair[0] ?? "";
|
||||
}
|
||||
|
||||
switch (props.filterKey.valueType) {
|
||||
switch (props.filterKey?.valueType) {
|
||||
case "multi-select":
|
||||
return `${state.multiSelectValue.length} ${props.filterKey.label} selected`;
|
||||
case "details":
|
||||
return state.multiSelectValue.length > 1
|
||||
? `${state.multiSelectValue.length} key:value pairs`
|
||||
: state.multiSelectValue.length === 1
|
||||
? state.multiSelectValue[0]
|
||||
: "";
|
||||
return `${state.keyValuePair.length} ${props.filterKey?.label} selected`;
|
||||
case "select":
|
||||
if (state.selectValue) {
|
||||
const option = state.valueOptions.find(opt => opt.value === state.selectValue);
|
||||
const option = state.valueOptions?.find(opt => opt.value === state.selectValue);
|
||||
return option ? option.label : state.selectValue;
|
||||
}
|
||||
return "";
|
||||
@@ -214,108 +207,104 @@
|
||||
});
|
||||
|
||||
const resetState = () => {
|
||||
const hasPreApplied = filterContext?.hasPreApplied(props.filterKey.key);
|
||||
const defaultFilter = filterContext?.hasPreApplied(props.filterKey.key)
|
||||
? filterContext?.getPreApplied(props.filterKey.key)
|
||||
: null;
|
||||
|
||||
if (hasPreApplied) {
|
||||
const defaultFilter = filterContext?.getPreApplied(props.filterKey.key);
|
||||
if (defaultFilter) {
|
||||
initializeStateFromFilter(defaultFilter);
|
||||
return;
|
||||
}
|
||||
if (defaultFilter) {
|
||||
initializeStateFromFilter(defaultFilter);
|
||||
return;
|
||||
}
|
||||
|
||||
state.textValue = "";
|
||||
state.selectValue = "";
|
||||
state.multiSelectValue = [];
|
||||
state.radioValue = "ALL";
|
||||
state.dateValue = null;
|
||||
state.timeRangeMode = "predefined";
|
||||
state.startDateValue = null;
|
||||
state.endDateValue = null;
|
||||
Object.assign(state, {
|
||||
textValue: "",
|
||||
selectValue: "",
|
||||
keyValuePair: [],
|
||||
radioValue: "ALL",
|
||||
dateValue: null,
|
||||
timeRangeMode: "predefined",
|
||||
startDateValue: null,
|
||||
endDateValue: null
|
||||
});
|
||||
};
|
||||
|
||||
const applyFilter = () => {
|
||||
if (!state.selectedComparator) return;
|
||||
const getFilterValue = () => {
|
||||
if (isTextOp.value) {
|
||||
return {value: state.textValue, label: state.textValue};
|
||||
}
|
||||
if (isKVPairFilter.value) {
|
||||
return {
|
||||
value: state.keyValuePair,
|
||||
label: state.keyValuePair[0] || ""
|
||||
};
|
||||
}
|
||||
|
||||
let filterValue: string | string[] | Date | {startDate: Date; endDate: Date};
|
||||
let valueLabel: string;
|
||||
|
||||
if (isTextComparator.value) {
|
||||
filterValue = state.textValue;
|
||||
valueLabel = state.textValue;
|
||||
} else {
|
||||
switch (props.filterKey.valueType) {
|
||||
case "text":
|
||||
filterValue = state.textValue;
|
||||
valueLabel = state.textValue;
|
||||
break;
|
||||
case "select":
|
||||
if (props.filterKey.key === "timeRange" && state.timeRangeMode === "custom") {
|
||||
filterValue = {
|
||||
switch (props.filterKey.valueType) {
|
||||
case "text":
|
||||
return {value: state.textValue, label: state.textValue};
|
||||
case "select":
|
||||
if (props.filterKey?.key === "timeRange" && state.timeRangeMode === "custom") {
|
||||
return {
|
||||
value: {
|
||||
startDate: state.startDateValue!,
|
||||
endDate: state.endDateValue!
|
||||
};
|
||||
valueLabel = `${state.startDateValue!.toLocaleDateString()} - ${state.endDateValue!.toLocaleDateString()}`;
|
||||
} else {
|
||||
filterValue = state.selectValue;
|
||||
valueLabel =
|
||||
state.valueOptions.find((opt: FilterValue) => opt.value === state.selectValue)
|
||||
?.label || state.selectValue;
|
||||
}
|
||||
break;
|
||||
case "multi-select":
|
||||
filterValue = state.multiSelectValue;
|
||||
valueLabel = state.multiSelectValue
|
||||
.map(
|
||||
val =>
|
||||
state.valueOptions.find((opt: FilterValue) => opt.value === val)?.label ??
|
||||
val
|
||||
)
|
||||
.join(", ");
|
||||
break;
|
||||
case "details":
|
||||
filterValue = state.multiSelectValue;
|
||||
valueLabel = state.multiSelectValue.length > 1
|
||||
? `${state.multiSelectValue.length} Details key/value pairs`
|
||||
: state.multiSelectValue.length === 1
|
||||
? state.multiSelectValue[0]
|
||||
: "";
|
||||
break;
|
||||
case "date":
|
||||
filterValue = state.dateValue ?? "";
|
||||
valueLabel = state.dateValue?.toLocaleDateString() ?? "";
|
||||
break;
|
||||
case "radio":
|
||||
if (state.radioValue === "ALL") {
|
||||
emits("remove", props.filter.id);
|
||||
emits("close");
|
||||
return;
|
||||
}
|
||||
filterValue = state.radioValue;
|
||||
valueLabel = state.radioValue;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
},
|
||||
label: `${state.startDateValue!.toLocaleDateString()} - ${state.endDateValue!.toLocaleDateString()}`
|
||||
};
|
||||
}
|
||||
return {
|
||||
value: state.selectValue,
|
||||
label:
|
||||
state.valueOptions?.find(opt => opt.value === state.selectValue)
|
||||
?.label || state.selectValue
|
||||
};
|
||||
case "multi-select":
|
||||
return {
|
||||
value: state.keyValuePair,
|
||||
label: state.keyValuePair
|
||||
.map(val =>
|
||||
state.valueOptions?.find(opt => opt.value === val)?.label ?? val
|
||||
)
|
||||
.join(", ")
|
||||
};
|
||||
case "date":
|
||||
return {
|
||||
value: state.dateValue ?? "",
|
||||
label: state.dateValue?.toLocaleDateString() ?? ""
|
||||
};
|
||||
case "radio":
|
||||
if (state.radioValue === "ALL") return null;
|
||||
return {value: state.radioValue, label: state.radioValue};
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const handleApply = () => {
|
||||
if (!state.selectedComparator) return;
|
||||
|
||||
const filterData = getFilterValue();
|
||||
if (!filterData) {
|
||||
emits("remove", props.filter.id);
|
||||
emits("close");
|
||||
return;
|
||||
}
|
||||
|
||||
emits("update", {
|
||||
...props.filter,
|
||||
comparator: state.selectedComparator,
|
||||
comparatorLabel: COMPARATOR_LABELS[state.selectedComparator],
|
||||
value: filterValue,
|
||||
valueLabel
|
||||
value: filterData.value,
|
||||
valueLabel: filterData.label
|
||||
});
|
||||
};
|
||||
|
||||
const handleApply = () => {
|
||||
applyFilter();
|
||||
emits("close");
|
||||
};
|
||||
|
||||
const initializeTimeRange = (filter: AppliedFilter) => {
|
||||
const initializeStateFromFilter = (filter: AppliedFilter) => {
|
||||
state.selectedComparator = filter.comparator;
|
||||
|
||||
if (
|
||||
props.filterKey.key === "timeRange" &&
|
||||
props.filterKey?.key === "timeRange" &&
|
||||
typeof filter.value === "object" &&
|
||||
filter.value !== null &&
|
||||
"startDate" in filter.value
|
||||
@@ -329,62 +318,63 @@
|
||||
state.startDateValue = null;
|
||||
state.endDateValue = null;
|
||||
}
|
||||
};
|
||||
|
||||
const initializeStateFromFilter = (filter: AppliedFilter) => {
|
||||
state.selectedComparator = filter.comparator;
|
||||
initializeTimeRange(filter);
|
||||
const isTextOp = TEXT_COMPARATORS.includes(filter.comparator) && props.filterKey?.key !== "resources";
|
||||
const isKVPair = props.filterKey?.valueType === "key-value" || (props.filterKey?.key === "labels" && KV_COMPARATORS.includes(filter.comparator));
|
||||
|
||||
const isTextComp = TEXT_COMPARATORS.includes(filter.comparator) && props.filterKey.key !== "resources";
|
||||
|
||||
if (isTextComp) {
|
||||
if (isTextOp) {
|
||||
state.textValue = typeof filter.value === "string" ? filter.value : "";
|
||||
} else if (isKVPair) {
|
||||
state.keyValuePair = Array.isArray(filter.value)
|
||||
? filter.value
|
||||
: typeof filter.value === "string"
|
||||
? [filter.value]
|
||||
: [];
|
||||
} else {
|
||||
switch (props.filterKey.valueType) {
|
||||
case "text":
|
||||
state.textValue = typeof filter.value === "string" ? filter.value : "";
|
||||
break;
|
||||
case "select":
|
||||
if (typeof filter.value === "string") {
|
||||
const matchingOption = state.valueOptions.find(
|
||||
option => option.value === filter.value
|
||||
);
|
||||
state.selectValue = matchingOption ? filter.value : "";
|
||||
} else {
|
||||
state.selectValue = "";
|
||||
}
|
||||
break;
|
||||
case "multi-select":
|
||||
case "details":
|
||||
state.multiSelectValue = Array.isArray(filter.value) ? filter.value : [];
|
||||
state.keyValuePair = Array.isArray(filter.value) ? filter.value : [];
|
||||
break;
|
||||
case "select":
|
||||
state.selectValue =
|
||||
typeof filter.value === "string" &&
|
||||
state.valueOptions.find(option => option.value === filter.value)
|
||||
? filter.value
|
||||
: "";
|
||||
break;
|
||||
case "date":
|
||||
state.dateValue = filter.value instanceof Date
|
||||
? filter.value
|
||||
: typeof filter.value === "string"
|
||||
? new Date(filter.value)
|
||||
state.dateValue = filter.value instanceof Date
|
||||
? filter.value
|
||||
: typeof filter.value === "string"
|
||||
? new Date(filter.value)
|
||||
: null;
|
||||
break;
|
||||
case "radio":
|
||||
state.radioValue = typeof filter.value === "string" ? filter.value : "ALL";
|
||||
state.radioValue = typeof filter.value === "string"
|
||||
? filter.value
|
||||
: "ALL";
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const initializeValueByType = () => {
|
||||
initializeStateFromFilter(props.filter);
|
||||
};
|
||||
|
||||
const loadValueOptions = async () => {
|
||||
if (!props.filterKey.valueProvider) return;
|
||||
if (!props.filterKey?.valueProvider) return;
|
||||
|
||||
state.valueOptions = await props.filterKey.valueProvider();
|
||||
|
||||
if (props.filterKey.key === "timeRange" && typeof props.filter.value === "string") {
|
||||
if (
|
||||
props.filterKey?.key === "timeRange" &&
|
||||
typeof props.filter.value === "string"
|
||||
) {
|
||||
const currentValue = props.filter.value;
|
||||
const exists = state.valueOptions.some(option => option.value === currentValue);
|
||||
if (!exists && isTimeRangeValue(currentValue)) {
|
||||
const exists = state.valueOptions.some(
|
||||
option => option.value === currentValue
|
||||
);
|
||||
if (!exists && /^P(T?\d+[HMD]|\d+[YMDW])/.test(currentValue)) {
|
||||
state.valueOptions.push({
|
||||
value: currentValue,
|
||||
label: getRelativeDateLabel(currentValue)
|
||||
@@ -393,15 +383,12 @@
|
||||
}
|
||||
};
|
||||
|
||||
const isTimeRangeValue = (value: string): boolean =>
|
||||
/^P(T?\d+[HMD]|\d+[YMDW])/.test(value);
|
||||
|
||||
const initializeFilter = async () => {
|
||||
state.selectedComparator = shouldShowComparator.value
|
||||
? props.filter.comparator
|
||||
: props.filterKey.comparators[0];
|
||||
await loadValueOptions();
|
||||
initializeValueByType();
|
||||
initializeStateFromFilter(props.filter);
|
||||
};
|
||||
|
||||
onMounted(initializeFilter);
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<label class="input-label">{{ $t('filter.key') }}</label>
|
||||
<el-input
|
||||
v-model="newKey"
|
||||
placeholder="e.g. type"
|
||||
placeholder="e.g. flowId"
|
||||
@keydown.enter="addPair"
|
||||
/>
|
||||
</div>
|
||||
@@ -31,7 +31,7 @@
|
||||
<label class="input-label">{{ $t('filter.value') }}</label>
|
||||
<el-input
|
||||
v-model="newValue"
|
||||
placeholder="e.g. io.kestra.ee.models..."
|
||||
placeholder="e.g. orchestrator-1234"
|
||||
@keydown.enter="addPair"
|
||||
/>
|
||||
</div>
|
||||
@@ -54,9 +54,10 @@
|
||||
import {ref, watch} from "vue";
|
||||
import {Plus} from "../../utils/icons";
|
||||
|
||||
const props = defineProps<{
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue: string[];
|
||||
}>();
|
||||
}>(), {
|
||||
});
|
||||
|
||||
const emits = defineEmits<{
|
||||
"update:modelValue": [value: string[]];
|
||||
@@ -68,26 +69,19 @@
|
||||
|
||||
// For Auditlogs Details KV pairs parsing and serialization
|
||||
const parseDetailPairs = (values: string[]) =>
|
||||
values
|
||||
?.map(value => {
|
||||
const [key, ...valueParts] = value?.split(":") ?? [];
|
||||
return {
|
||||
key: key ?? "",
|
||||
value: valueParts?.join(":") ?? ""
|
||||
};
|
||||
})
|
||||
?.filter(pair => pair?.key && pair?.value) ?? [];
|
||||
values?.map(value => {
|
||||
const [key, ...valueParts] = value?.split(":") ?? [];
|
||||
return {key: key ?? "", value: valueParts?.join(":") ?? ""};
|
||||
}).filter(pair => pair.key && pair.value) ?? [];
|
||||
|
||||
const serializeDetailPairs = (pairs: typeof detailPairs.value) =>
|
||||
pairs.map(pair => `${pair.key}:${pair.value}`);
|
||||
|
||||
const addPair = () => {
|
||||
const key = newKey.value.trim();
|
||||
const value = newValue.value.trim();
|
||||
const key = newKey.value.trim(), value = newValue.value.trim();
|
||||
if (!key || !value) return;
|
||||
|
||||
const existingIndex = detailPairs.value.findIndex(pair => pair.key === key);
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
detailPairs.value[existingIndex].value = value;
|
||||
} else {
|
||||
@@ -95,8 +89,7 @@
|
||||
}
|
||||
|
||||
emits("update:modelValue", serializeDetailPairs(detailPairs.value));
|
||||
newKey.value = "";
|
||||
newValue.value = "";
|
||||
newKey.value = newValue.value = "";
|
||||
};
|
||||
|
||||
const removePair = (index: number) => {
|
||||
@@ -104,17 +97,14 @@
|
||||
emits("update:modelValue", serializeDetailPairs(detailPairs.value));
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
detailPairs.value = newValue ? parseDetailPairs(newValue) : [];
|
||||
}, {immediate: true}
|
||||
);
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
detailPairs.value = newValue ? parseDetailPairs(newValue) : [];
|
||||
}, {immediate: true});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.active-pairs {
|
||||
padding: 12px;
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid var(--ks-border-primary);
|
||||
|
||||
.section-title {
|
||||
@@ -171,7 +161,7 @@
|
||||
}
|
||||
|
||||
.add-pair {
|
||||
padding: 12px;
|
||||
padding: 1rem;
|
||||
|
||||
.input-group {
|
||||
margin-bottom: 12px;
|
||||
@@ -1,14 +1,21 @@
|
||||
import {ref, watch, computed} from "vue";
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
import {keyOfComparator} from "../utils/helpers";
|
||||
import {AppliedFilter, FilterConfiguration, COMPARATOR_LABELS, Comparators} from "../utils/filterTypes";
|
||||
import {
|
||||
keyOfComparator,
|
||||
decodeSearchParams,
|
||||
encodeFiltersToQuery,
|
||||
isValidFilter,
|
||||
getUniqueFilters,
|
||||
clearFilterQueryParams
|
||||
} from "../utils/helpers";
|
||||
import {
|
||||
AppliedFilter,
|
||||
FilterConfiguration,
|
||||
COMPARATOR_LABELS,
|
||||
Comparators,
|
||||
TEXT_COMPARATORS,
|
||||
KV_COMPARATORS
|
||||
} from "../utils/filterTypes";
|
||||
import {usePreAppliedFilters} from "./usePreAppliedFilters";
|
||||
|
||||
export function useFilters(configuration: FilterConfiguration, showSearchInput = true, legacyQuery = false) {
|
||||
@@ -25,63 +32,39 @@ export function useFilters(configuration: FilterConfiguration, showSearchInput =
|
||||
getAllPreApplied
|
||||
} = usePreAppliedFilters();
|
||||
|
||||
/**
|
||||
* Appends value to query param, handling arrays.
|
||||
* @param query - Query object to modify
|
||||
* @param key - Query parameter key
|
||||
* @param value - Value to append
|
||||
*/
|
||||
|
||||
const appendQueryParam = (query: Record<string, any>, key: string, value: string) => {
|
||||
if (query[key]) {
|
||||
if (Array.isArray(query[key])) {
|
||||
query[key].push(value);
|
||||
} else {
|
||||
query[key] = [query[key], value];
|
||||
}
|
||||
query[key] = Array.isArray(query[key]) ? [...query[key], value] : [query[key], value];
|
||||
} else {
|
||||
query[key] = value;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if filter is a time range filter.
|
||||
* @param filter - Filter to check
|
||||
* @returns True if time range filter
|
||||
*/
|
||||
const isTimeRange = (filter: AppliedFilter) =>
|
||||
typeof filter.value === "object" && "startDate" in filter.value && filter.key === "timeRange";
|
||||
typeof filter.value === "object" &&
|
||||
"startDate" in filter.value &&
|
||||
filter.key === "timeRange";
|
||||
|
||||
/**
|
||||
* Updates search query in URL query object.
|
||||
* @param query - Query object to update
|
||||
*/
|
||||
const updateSearchQuery = (query: Record<string, any>) => {
|
||||
const trimmedQuery = searchQuery.value?.trim();
|
||||
if (!trimmedQuery || !showSearchInput) {
|
||||
delete query.q;
|
||||
delete query.search;
|
||||
delete query["filters[q][EQUALS]"];
|
||||
return;
|
||||
delete query.q;
|
||||
delete query.search;
|
||||
delete query["filters[q][EQUALS]"];
|
||||
|
||||
if (trimmedQuery && showSearchInput) {
|
||||
const searchKey = configuration.keys?.length > 0 && !legacyQuery
|
||||
? "filters[q][EQUALS]"
|
||||
: "q";
|
||||
query[searchKey] = trimmedQuery;
|
||||
}
|
||||
const searchKey = configuration.keys?.length > 0 && !legacyQuery
|
||||
? "filters[q][EQUALS]"
|
||||
: "q";
|
||||
query[searchKey] = trimmedQuery;
|
||||
};
|
||||
|
||||
/**
|
||||
* Clears legacy query parameters from query object.
|
||||
* @param query - Query object to clean
|
||||
*/
|
||||
const clearLegacyParams = (query: Record<string, any>) => {
|
||||
configuration.keys?.forEach(({key}) => {
|
||||
delete query[key];
|
||||
if (key === "details") {
|
||||
Object.keys(query).forEach(queryKey => {
|
||||
if (queryKey.startsWith("details.")) {
|
||||
delete query[queryKey];
|
||||
}
|
||||
if (queryKey.startsWith("details.")) delete query[queryKey];
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -95,13 +78,15 @@ export function useFilters(configuration: FilterConfiguration, showSearchInput =
|
||||
*/
|
||||
const buildLegacyQuery = (query: Record<string, any>) => {
|
||||
getUniqueFilters(appliedFilters.value.filter(isValidFilter)).forEach(filter => {
|
||||
if (filter.key === "details") { // AuditLogs Details
|
||||
if (filter.key === "details") {
|
||||
(filter.value as string[]).forEach(item => {
|
||||
const [k, v] = item.split(":");
|
||||
query[`details.${k}`] = v;
|
||||
});
|
||||
} else if (Array.isArray(filter.value)) {
|
||||
filter.value.forEach(item => appendQueryParam(query, filter.key, item?.toString() || ""));
|
||||
filter.value.forEach(item =>
|
||||
appendQueryParam(query, filter.key, item?.toString() ?? "")
|
||||
);
|
||||
} else if (isTimeRange(filter)) {
|
||||
const {startDate, endDate} = filter.value as { startDate: Date; endDate: Date };
|
||||
query.startDate = startDate.toISOString();
|
||||
@@ -112,9 +97,6 @@ export function useFilters(configuration: FilterConfiguration, showSearchInput =
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates route with current filter state.
|
||||
*/
|
||||
const updateRoute = () => {
|
||||
const query = {...route.query};
|
||||
clearFilterQueryParams(query);
|
||||
@@ -131,16 +113,6 @@ export function useFilters(configuration: FilterConfiguration, showSearchInput =
|
||||
router.push({query});
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates AppliedFilter object.
|
||||
* @param key - Filter key
|
||||
* @param config - Filter configuration
|
||||
* @param comparator - Comparison operator
|
||||
* @param value - Filter value
|
||||
* @param valueLabel - Display label for value
|
||||
* @param idSuffix - Suffix for unique ID
|
||||
* @returns AppliedFilter object
|
||||
*/
|
||||
const createAppliedFilter = (
|
||||
key: string,
|
||||
config: any,
|
||||
@@ -158,31 +130,22 @@ export function useFilters(configuration: FilterConfiguration, showSearchInput =
|
||||
valueLabel
|
||||
});
|
||||
|
||||
/**
|
||||
* Creates standard filter object.
|
||||
* @param key - Filter key
|
||||
* @param config - Filter configuration
|
||||
* @param value - Filter value(s)
|
||||
* @returns AppliedFilter object
|
||||
*/
|
||||
const createFilter = (key: string, config: any, value: string | string[]): AppliedFilter => {
|
||||
const createFilter = (
|
||||
key: string,
|
||||
config: any,
|
||||
value: string | string[]
|
||||
): AppliedFilter => {
|
||||
const comparator = (config?.comparators?.[0] as Comparators) ?? Comparators.EQUALS;
|
||||
const valueLabel = Array.isArray(value)
|
||||
? key === "details"
|
||||
? value.length > 1 ? `${value[0]} +${value.length - 1}` : value[0]
|
||||
: value.join(", ")
|
||||
? key === "details" && value.length > 1
|
||||
? `${value[0]} +${value.length - 1}`
|
||||
: Array.isArray(value)
|
||||
? value.join(", ")
|
||||
: value[0]
|
||||
: (value as string);
|
||||
return createAppliedFilter(key, config, comparator, value, valueLabel, "EQUALS");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates time range filter object.
|
||||
* @param config - Filter configuration
|
||||
* @param startDate - Start date
|
||||
* @param endDate - End date
|
||||
* @param comparator - Comparison operator
|
||||
* @returns Time range AppliedFilter object
|
||||
*/
|
||||
const createTimeRangeFilter = (
|
||||
config: any,
|
||||
startDate: Date,
|
||||
@@ -190,16 +153,17 @@ export function useFilters(configuration: FilterConfiguration, showSearchInput =
|
||||
comparator = Comparators.EQUALS
|
||||
): AppliedFilter => {
|
||||
const valueLabel = `${startDate.toLocaleDateString()} - ${endDate.toLocaleDateString()}`;
|
||||
const filter = createAppliedFilter(
|
||||
"timeRange",
|
||||
config,
|
||||
comparator,
|
||||
{startDate, endDate},
|
||||
valueLabel,
|
||||
keyOfComparator(comparator)
|
||||
);
|
||||
filter.comparatorLabel = "Is Between";
|
||||
return filter;
|
||||
return {
|
||||
...createAppliedFilter(
|
||||
"timeRange",
|
||||
config,
|
||||
comparator,
|
||||
{startDate, endDate},
|
||||
valueLabel,
|
||||
keyOfComparator(comparator)
|
||||
),
|
||||
comparatorLabel: "Is Between"
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -212,11 +176,12 @@ export function useFilters(configuration: FilterConfiguration, showSearchInput =
|
||||
|
||||
Object.entries(route.query).forEach(([key, value]) => {
|
||||
if (["q", "search", "filters[q][EQUALS]"].includes(key)) return;
|
||||
|
||||
if (key.startsWith("details.")) {
|
||||
const detailKey = key.split(".")[1];
|
||||
details.push(`${detailKey}:${value}`);
|
||||
details.push(`${key.split(".")[1]}:${value}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const config = configuration.keys?.find(k => k.key === key);
|
||||
if (!config) return;
|
||||
|
||||
@@ -224,7 +189,7 @@ export function useFilters(configuration: FilterConfiguration, showSearchInput =
|
||||
? (value as string[]).filter(v => v !== null)
|
||||
: config?.valueType === "multi-select"
|
||||
? ((value as string) ?? "").split(",")
|
||||
: (value as string) ?? "";
|
||||
: ((value as string) ?? "");
|
||||
|
||||
filtersMap.set(key, createFilter(key, config, processedValue));
|
||||
});
|
||||
@@ -239,11 +204,13 @@ export function useFilters(configuration: FilterConfiguration, showSearchInput =
|
||||
if (route.query.startDate && route.query.endDate) {
|
||||
const timeRangeConfig = configuration.keys?.find(k => k.key === "timeRange");
|
||||
if (timeRangeConfig) {
|
||||
const startDate = new Date(route.query.startDate as string);
|
||||
const endDate = new Date(route.query.endDate as string);
|
||||
filtersMap.set(
|
||||
"timeRange",
|
||||
createTimeRangeFilter(timeRangeConfig, startDate, endDate)
|
||||
createTimeRangeFilter(
|
||||
timeRangeConfig,
|
||||
new Date(route.query.startDate as string),
|
||||
new Date(route.query.endDate as string)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -251,55 +218,47 @@ export function useFilters(configuration: FilterConfiguration, showSearchInput =
|
||||
return Array.from(filtersMap.values());
|
||||
};
|
||||
|
||||
const TEXT_COMPARATORS = [
|
||||
Comparators.STARTS_WITH,
|
||||
Comparators.ENDS_WITH,
|
||||
Comparators.CONTAINS
|
||||
];
|
||||
const isKVFilter = (field: string, comparator: Comparators) =>
|
||||
field === "details" || (field === "labels" && KV_COMPARATORS.includes(comparator));
|
||||
|
||||
/**
|
||||
* Processes field values for filters.
|
||||
* @param config - Filter configuration
|
||||
* @param params - Parameter objects array
|
||||
* @param field - Field name
|
||||
* @param comparator - Comparison operator
|
||||
* @returns Processed value and label
|
||||
*/
|
||||
const processFieldValue = (config: any, params: any[], field: string, comparator: Comparators) => {
|
||||
const filterTextComparator = TEXT_COMPARATORS.includes(comparator);
|
||||
const isTextOp = TEXT_COMPARATORS.includes(comparator);
|
||||
|
||||
if (config?.valueType === "multi-select" && !filterTextComparator) {
|
||||
const combinedValue = field === "labels"
|
||||
? params.map(p => p?.value as string)
|
||||
: params.flatMap(p =>
|
||||
Array.isArray(p?.value) ? p.value : (p?.value as string)?.split(",") ?? []
|
||||
);
|
||||
return {value: combinedValue, valueLabel: combinedValue.join(", ")};
|
||||
} else {
|
||||
const param = params[0];
|
||||
let value = Array.isArray(param?.value) ? param.value[0] : param?.value as string;
|
||||
|
||||
if (config?.valueType === "date" && typeof value === "string") {
|
||||
value = new Date(value);
|
||||
}
|
||||
|
||||
const valueLabel = value instanceof Date ? value.toLocaleDateString() : value;
|
||||
return {value, valueLabel};
|
||||
if (isKVFilter(field, comparator)) {
|
||||
const combinedValue = params.map(p => p?.value as string);
|
||||
return {
|
||||
value: combinedValue,
|
||||
valueLabel: combinedValue.length > 1
|
||||
? `${combinedValue[0]} +${combinedValue.length - 1}`
|
||||
: combinedValue[0] ?? ""
|
||||
};
|
||||
}
|
||||
|
||||
if (config?.valueType === "multi-select" && !isTextOp) {
|
||||
const combinedValue = params.flatMap(p =>
|
||||
Array.isArray(p?.value) ? p.value : (p?.value as string)?.split(",") ?? []
|
||||
);
|
||||
return {
|
||||
value: combinedValue,
|
||||
valueLabel: combinedValue.join(", ")
|
||||
};
|
||||
}
|
||||
|
||||
const param = params[0];
|
||||
let value = Array.isArray(param?.value)
|
||||
? param.value[0]
|
||||
: (param?.value as string);
|
||||
|
||||
if (config?.valueType === "date" && typeof value === "string") {
|
||||
value = new Date(value);
|
||||
}
|
||||
|
||||
return {
|
||||
value,
|
||||
valueLabel: value instanceof Date ? value.toLocaleDateString() : value
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if date filters contain valid time range.
|
||||
* @param dateFilters - Date filter objects
|
||||
* @returns True if both dates present
|
||||
*/
|
||||
const hasValidTimeRange = (dateFilters: Record<string, any>) =>
|
||||
dateFilters.startDate && dateFilters.endDate;
|
||||
|
||||
/**
|
||||
* Parses filters from encoded URL parameters.
|
||||
* @returns Array of AppliedFilter objects
|
||||
*/
|
||||
const parseEncodedFilters = (): AppliedFilter[] => {
|
||||
const filtersMap = new Map<string, AppliedFilter>();
|
||||
const dateFilters: Record<string, {comparatorKey: string; value: string}> = {};
|
||||
@@ -332,17 +291,20 @@ export function useFilters(configuration: FilterConfiguration, showSearchInput =
|
||||
);
|
||||
});
|
||||
|
||||
if (hasValidTimeRange(dateFilters)) {
|
||||
if (dateFilters.startDate && dateFilters.endDate) {
|
||||
const timeRangeConfig = configuration.keys?.find(k => k?.key === "timeRange");
|
||||
if (timeRangeConfig) {
|
||||
const comparator = Comparators[
|
||||
dateFilters.startDate?.comparatorKey as keyof typeof Comparators
|
||||
];
|
||||
const startDate = new Date(dateFilters.startDate?.value);
|
||||
const endDate = new Date(dateFilters.endDate?.value);
|
||||
filtersMap.set(
|
||||
"timeRange",
|
||||
createTimeRangeFilter(timeRangeConfig, startDate, endDate, comparator)
|
||||
createTimeRangeFilter(
|
||||
timeRangeConfig,
|
||||
new Date(dateFilters.startDate?.value),
|
||||
new Date(dateFilters.endDate?.value),
|
||||
comparator
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -350,9 +312,6 @@ export function useFilters(configuration: FilterConfiguration, showSearchInput =
|
||||
return Array.from(filtersMap.values());
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes filter state from route query parameters.
|
||||
*/
|
||||
const initializeFromRoute = () => {
|
||||
if (showSearchInput) {
|
||||
searchQuery.value =
|
||||
@@ -360,6 +319,7 @@ export function useFilters(configuration: FilterConfiguration, showSearchInput =
|
||||
(route.query?.q as string) ??
|
||||
"";
|
||||
}
|
||||
|
||||
const parsedFilters = legacyQuery
|
||||
? parseLegacyFilters()
|
||||
: parseEncodedFilters();
|
||||
@@ -374,38 +334,27 @@ export function useFilters(configuration: FilterConfiguration, showSearchInput =
|
||||
watch(() => route.query, initializeFromRoute, {deep: true, immediate: false});
|
||||
initializeFromRoute();
|
||||
|
||||
/**
|
||||
* Adds filter to applied filters list.
|
||||
* @param filter - Filter to add
|
||||
*/
|
||||
const addFilter = (filter: AppliedFilter) => {
|
||||
const index = appliedFilters.value.findIndex(f => f?.key === filter?.key);
|
||||
if (index === -1) {
|
||||
appliedFilters.value.push(filter);
|
||||
} else {
|
||||
appliedFilters.value[index] = filter;
|
||||
}
|
||||
appliedFilters.value = index === -1
|
||||
? [...appliedFilters.value, filter]
|
||||
: appliedFilters.value.map((f, i) => (i === index ? filter : f));
|
||||
updateRoute();
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes filter by ID.
|
||||
* @param filterId - ID of filter to remove
|
||||
*/
|
||||
const removeFilter = (filterId: string) => {
|
||||
const filter = appliedFilters.value.find(f => f?.id === filterId);
|
||||
if (filter) {
|
||||
appliedFilters.value = appliedFilters.value.filter(f => f?.key !== filter?.key);
|
||||
updateRoute();
|
||||
}
|
||||
updateRoute();
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates existing filter.
|
||||
* @param updatedFilter - Updated filter object
|
||||
*/
|
||||
const updateFilter = (updatedFilter: AppliedFilter) => {
|
||||
appliedFilters.value = [...appliedFilters.value.filter(f => f?.key !== updatedFilter?.key), updatedFilter];
|
||||
appliedFilters.value = [
|
||||
...appliedFilters.value.filter(f => f?.key !== updatedFilter?.key),
|
||||
updatedFilter
|
||||
];
|
||||
updateRoute();
|
||||
};
|
||||
|
||||
|
||||
@@ -56,7 +56,6 @@ export function useValues(label: string | undefined, t?: ReturnType<typeof useI1
|
||||
EXECUTION_STATES: buildFromArray(
|
||||
State.arrayAllStates().map((state: { name: string }) => state.name),
|
||||
),
|
||||
TRIGGER_STATES: buildFromArray(["ENABLED", "DISABLED"], true),
|
||||
SCOPES: [
|
||||
{
|
||||
label: t("scope_filter.user", {label: SCOPE_LABEL}),
|
||||
|
||||
@@ -14,6 +14,13 @@ export enum Comparators {
|
||||
PREFIX = "^.=",
|
||||
}
|
||||
|
||||
export const KV_COMPARATORS = [Comparators.EQUALS, Comparators.NOT_EQUALS];
|
||||
export const TEXT_COMPARATORS = [
|
||||
Comparators.CONTAINS,
|
||||
Comparators.ENDS_WITH,
|
||||
Comparators.STARTS_WITH,
|
||||
];
|
||||
|
||||
export interface FilterKeyConfig {
|
||||
key: string;
|
||||
label: string;
|
||||
@@ -22,7 +29,7 @@ export interface FilterKeyConfig {
|
||||
comparators: Comparators[];
|
||||
showComparatorSelection?: boolean;
|
||||
valueProvider?: () => Promise<FilterValue[]>;
|
||||
valueType: "text" | "select" | "date" | "multi-select" | "details" | "radio";
|
||||
valueType: "text" | "select" | "date" | "multi-select" | "key-value" | "radio";
|
||||
}
|
||||
|
||||
export interface FilterValue {
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
import FlowConcurrency from "./FlowConcurrency.vue";
|
||||
import DemoAuditLogs from "../demo/AuditLogs.vue";
|
||||
import {useAuthStore} from "override/stores/auth"
|
||||
import {useMiscStore} from "override/stores/misc";
|
||||
|
||||
export default {
|
||||
mixins: [RouteContext],
|
||||
@@ -62,7 +63,9 @@
|
||||
const dateTimeKeys = ["startDate", "endDate", "timeRange"];
|
||||
|
||||
if (!Object.keys(this.$route.query).some((key) => dateTimeKeys.some((dateTimeKey) => key.includes(dateTimeKey)))) {
|
||||
const newQuery = {...this.$route.query, "filters[timeRange][EQUALS]": "PT168H"};
|
||||
const miscStore = useMiscStore();
|
||||
const defaultDuration = miscStore.configs?.chartDefaultDuration || "P30D";
|
||||
const newQuery = {...this.$route.query, "filters[timeRange][EQUALS]": defaultDuration};
|
||||
this.$router.replace({name: this.$route.name, params: this.$route.params, query: newQuery});
|
||||
}
|
||||
}
|
||||
@@ -98,8 +101,8 @@
|
||||
if(!this.$route.params.tab) {
|
||||
const tab = localStorage.getItem("flowDefaultTab") || "overview";
|
||||
this.$router.replace({
|
||||
name: "flows/update",
|
||||
params: {...this.$route.params, tab},
|
||||
name: "flows/update",
|
||||
params: {...this.$route.params, tab},
|
||||
query: {...this.$route.query}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -37,8 +37,6 @@
|
||||
@page-changed="onPageChanged"
|
||||
ref="dataTable"
|
||||
:total="flowStore.total"
|
||||
:size="internalPageSize"
|
||||
:page="internalPageNumber"
|
||||
>
|
||||
<template #navbar>
|
||||
<KSFilter
|
||||
@@ -250,7 +248,7 @@
|
||||
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, computed, onMounted, watch, useTemplateRef} from "vue";
|
||||
import {ref, computed, onMounted, useTemplateRef} from "vue";
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import _merge from "lodash/merge";
|
||||
@@ -292,7 +290,7 @@
|
||||
import {useExecutionsStore} from "../../stores/executions";
|
||||
|
||||
import {useTableColumns} from "../../composables/useTableColumns";
|
||||
import {useDataTableActions} from "../../composables/useDataTableActions";
|
||||
import {DataTableRef, useDataTableActions} from "../../composables/useDataTableActions";
|
||||
import {useSelectTableActions} from "../../composables/useSelectTableActions";
|
||||
|
||||
|
||||
@@ -318,9 +316,6 @@
|
||||
|
||||
const flowFilter = useFlowFilter();
|
||||
|
||||
const ready = ref(true);
|
||||
const internalPageSize = ref(25);
|
||||
const internalPageNumber = ref(1);
|
||||
const lastExecutionByFlowReady = ref(false);
|
||||
const latestExecutions = ref<any[]>([]);
|
||||
const file = ref<HTMLInputElement | null>(null);
|
||||
@@ -382,14 +377,43 @@
|
||||
|
||||
const routeInfo = computed(() => ({title: t("flows")}));
|
||||
|
||||
const dataTableRef = useTemplateRef<DataTableRef>("dataTable");
|
||||
const selectTableRef = useTemplateRef<typeof SelectTable>("selectTable");
|
||||
|
||||
function loadData(callback?: () => void) {
|
||||
const q = route.query;
|
||||
flowStore
|
||||
.findFlows(
|
||||
loadQuery({
|
||||
size: parseInt(q.size as string ?? "25"),
|
||||
page: parseInt(q.page as string ?? "1"),
|
||||
sort: (q.sort as string) ?? "id:asc",
|
||||
})
|
||||
)
|
||||
.then((data: any) => {
|
||||
if (user.value?.hasAnyActionOnAnyNamespace(permission.EXECUTION, action.READ)) {
|
||||
executionsStore.loadLatestExecutions({
|
||||
flowFilters: data.results.map((flow: any) => ({id: flow.id, namespace: flow.namespace})),
|
||||
}).then((latestExecs: any) => {
|
||||
latestExecutions.value = latestExecs;
|
||||
lastExecutionByFlowReady.value = true;
|
||||
});
|
||||
}
|
||||
})
|
||||
.finally(() => callback?.());
|
||||
}
|
||||
|
||||
const {
|
||||
queryWithFilter,
|
||||
onPageChanged,
|
||||
onRowDoubleClick,
|
||||
onSort
|
||||
} = useDataTableActions({dblClickRouteName: "flows/update"});
|
||||
onSort,
|
||||
ready
|
||||
} = useDataTableActions({
|
||||
dblClickRouteName: "flows/update",
|
||||
dataTableRef,
|
||||
loadData
|
||||
});
|
||||
|
||||
function selectionMapper({id, namespace, disabled}: {id: string; namespace: string; disabled: boolean}) {
|
||||
return {
|
||||
@@ -584,29 +608,6 @@
|
||||
return _merge(base, queryFilter);
|
||||
}
|
||||
|
||||
function loadData(callback: () => void) {
|
||||
const q = route.query;
|
||||
flowStore
|
||||
.findFlows(
|
||||
loadQuery({
|
||||
size: parseInt(props.namespace ? internalPageSize.value.toString() : (q.size as string) ?? "25"),
|
||||
page: parseInt(props.namespace ? internalPageNumber.value.toString() : (q.page as string) ?? "1"),
|
||||
sort: (q.sort as string) ?? "id:asc",
|
||||
})
|
||||
)
|
||||
.then((data: any) => {
|
||||
if (user.value?.hasAnyActionOnAnyNamespace(permission.EXECUTION, action.READ)) {
|
||||
executionsStore.loadLatestExecutions({
|
||||
flowFilters: data.results.map((flow: any) => ({id: flow.id, namespace: flow.namespace})),
|
||||
}).then((latestExecs: any) => {
|
||||
latestExecutions.value = latestExecs;
|
||||
lastExecutionByFlowReady.value = true;
|
||||
});
|
||||
}
|
||||
})
|
||||
.finally(callback);
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
loadData(() => {});
|
||||
}
|
||||
@@ -637,14 +638,8 @@
|
||||
}
|
||||
|
||||
if (queryHasChanged) router.replace({query});
|
||||
|
||||
loadData(() => ready.value = true);
|
||||
});
|
||||
|
||||
watch(() => route.query, async () => {
|
||||
await loadData(() => {});
|
||||
}, {deep: true});
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -553,7 +553,7 @@
|
||||
value = JSON.stringify(value);
|
||||
}
|
||||
|
||||
const contentType = ["DATE", "DATETIME"].includes(type) ? "text/plain" : "application/json";
|
||||
const contentType = "text/plain";
|
||||
|
||||
const namespace = kv.value.namespace!;
|
||||
const key = kv.value.key!;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<el-select
|
||||
:modelValue="value"
|
||||
@update:model-value="onInput"
|
||||
@update:model-value="emit('update:modelValue', $event)"
|
||||
filterable
|
||||
:persistent="false"
|
||||
:placeholder="$t('revisions')"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in levelOptions"
|
||||
v-for="item in LEVELS"
|
||||
:key="item"
|
||||
:label="item"
|
||||
:value="item"
|
||||
@@ -16,34 +16,22 @@
|
||||
</el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
emits: ["update:modelValue"],
|
||||
data() {
|
||||
return {
|
||||
levelOptions: [
|
||||
"TRACE",
|
||||
"DEBUG",
|
||||
"INFO",
|
||||
"WARN",
|
||||
"ERROR",
|
||||
],
|
||||
};
|
||||
},
|
||||
props: {
|
||||
router: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
default: "INFO"
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onInput(value) {
|
||||
this.$emit("update:modelValue", value);
|
||||
},
|
||||
},
|
||||
};
|
||||
<script setup lang="ts">
|
||||
const emit = defineEmits<{(e: "update:modelValue", value: string): void;}>()
|
||||
|
||||
withDefaults(defineProps<{
|
||||
value?: string,
|
||||
router?: boolean
|
||||
}>(), {
|
||||
value: "INFO",
|
||||
router: true
|
||||
})
|
||||
|
||||
const LEVELS = [
|
||||
"TRACE",
|
||||
"DEBUG",
|
||||
"INFO",
|
||||
"WARN",
|
||||
"ERROR",
|
||||
];
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
class="py-2 line font-monospace"
|
||||
:class="{['log-border-' + log.level.toLowerCase()]: cursor && log.level !== undefined, ['key-' + $.vnode.key]: true}"
|
||||
:class="{['log-border-' + log.level.toLowerCase()]: cursor && log.level !== undefined}"
|
||||
v-if="filtered"
|
||||
:style="logLineStyle"
|
||||
>
|
||||
@@ -16,7 +16,7 @@
|
||||
:class="{'d-inline-block': metaWithValue.length === 0, 'me-3': metaWithValue.length === 0}"
|
||||
>
|
||||
<span class="header-badge text-secondary">
|
||||
{{ $filters.date(log.timestamp, "iso") }}
|
||||
{{ Filters.date(log.timestamp, "iso") }}
|
||||
</span>
|
||||
<span v-for="(meta, x) in metaWithValue" :key="x">
|
||||
<span class="header-badge property">
|
||||
@@ -39,157 +39,134 @@
|
||||
<CopyToClipboard :text="`${log.level} ${log.timestamp} ${log.message}`" link />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
<script setup lang="ts">
|
||||
import {ref, computed, onMounted, watch, nextTick} from "vue";
|
||||
import Convert from "ansi-to-html";
|
||||
import {useStorage} from "@vueuse/core";
|
||||
import xss from "xss";
|
||||
import * as Markdown from "../../utils/markdown";
|
||||
import MenuRight from "vue-material-design-icons/MenuRight.vue";
|
||||
import linkify from "./linkify";
|
||||
import CopyToClipboard from "../layout/CopyToClipboard.vue";
|
||||
import {LevelKey} from "../../utils/logs";
|
||||
import {Log} from "../../stores/logs";
|
||||
import {useRouter} from "vue-router";
|
||||
import * as Filters from "../../utils/filters";
|
||||
|
||||
let convert = new Convert();
|
||||
// Props
|
||||
const props = defineProps<{
|
||||
cursor?: boolean,
|
||||
log: Log,
|
||||
filter?: string,
|
||||
level?: LevelKey,
|
||||
excludeMetas?: (keyof Log)[],
|
||||
title?: boolean
|
||||
}>();
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MenuRight,
|
||||
CopyToClipboard
|
||||
},
|
||||
props: {
|
||||
cursor: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
log: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
filter: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
level: {
|
||||
type: String,
|
||||
default: "INFO",
|
||||
},
|
||||
excludeMetas: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
title: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
renderedMarkdown: undefined,
|
||||
logsFontSize: parseInt(localStorage.getItem("logsFontSize") || "12"),
|
||||
};
|
||||
},
|
||||
async created() {
|
||||
this.renderedMarkdown = await Markdown.render(this.message, {onlyLink: true, html: true});
|
||||
},
|
||||
computed: {
|
||||
logLineStyle() {
|
||||
return {
|
||||
fontSize: `${this.logsFontSize}px`,
|
||||
};
|
||||
},
|
||||
metaWithValue() {
|
||||
const metaWithValue = [];
|
||||
const excludes = [
|
||||
"message",
|
||||
"timestamp",
|
||||
"thread",
|
||||
"taskRunId",
|
||||
"level",
|
||||
"index",
|
||||
"attemptNumber",
|
||||
"executionKind"
|
||||
];
|
||||
excludes.push.apply(excludes, this.excludeMetas);
|
||||
for (const key in this.log) {
|
||||
if (this.log[key] && !excludes.includes(key)) {
|
||||
let meta = {key, value: this.log[key]};
|
||||
if (key === "executionId") {
|
||||
meta["router"] = {
|
||||
name: "executions/update",
|
||||
params: {
|
||||
namespace: this.log["namespace"],
|
||||
flowId: this.log["flowId"],
|
||||
id: this.log[key],
|
||||
},
|
||||
};
|
||||
}
|
||||
// State
|
||||
const renderedMarkdown = ref<string | undefined>(undefined);
|
||||
const logsFontSize = useStorage<number>("logsFontSize", 12);
|
||||
const lineContent = ref<HTMLElement>();
|
||||
|
||||
if (key === "namespace") {
|
||||
meta["router"] = {name: "flows/list", query: {namespace: this.log[key]}};
|
||||
}
|
||||
const convert = new Convert();
|
||||
|
||||
if (key === "flowId") {
|
||||
meta["router"] = {
|
||||
name: "flows/update",
|
||||
params: {namespace: this.log["namespace"], id: this.log[key]},
|
||||
};
|
||||
}
|
||||
// Computed
|
||||
const logLineStyle = computed(() => ({
|
||||
fontSize: `${logsFontSize.value}px`,
|
||||
}));
|
||||
|
||||
metaWithValue.push(meta);
|
||||
}
|
||||
const metaWithValue = computed(() => {
|
||||
const metaWithValue: any[] = [];
|
||||
const excludes:(keyof Log)[] = [
|
||||
"message",
|
||||
"timestamp",
|
||||
"thread",
|
||||
"taskRunId",
|
||||
"level",
|
||||
"index",
|
||||
"attemptNumber",
|
||||
"executionKind",
|
||||
...(props.excludeMetas ?? [])
|
||||
];
|
||||
for (const keyString in props.log) {
|
||||
const key = keyString as keyof Log;
|
||||
if (props.log[key] && !excludes.includes(key)) {
|
||||
let meta: any = {key, value: props.log[key]};
|
||||
if (key === "executionId") {
|
||||
meta["router"] = {
|
||||
name: "executions/update",
|
||||
params: {
|
||||
namespace: props.log["namespace"],
|
||||
flowId: props.log["flowId"],
|
||||
id: props.log[key],
|
||||
},
|
||||
};
|
||||
}
|
||||
return metaWithValue;
|
||||
},
|
||||
levelStyle() {
|
||||
const lowerCaseLevel = this.log?.level?.toLowerCase();
|
||||
return {
|
||||
"border-color": `var(--ks-log-border-${lowerCaseLevel})`,
|
||||
"color": `var(--ks-log-content-${lowerCaseLevel})`,
|
||||
"background-color": `var(--ks-log-background-${lowerCaseLevel})`,
|
||||
};
|
||||
},
|
||||
filtered() {
|
||||
return (
|
||||
this.filter === "" || (this.log.message && this.log.message.toLowerCase().includes(this.filter))
|
||||
);
|
||||
},
|
||||
iconColor() {
|
||||
const logLevel = this.log.level?.toLowerCase();
|
||||
return `var(--ks-log-content-${logLevel}) !important`; // Use CSS variable for icon color
|
||||
},
|
||||
message() {
|
||||
let logMessage = !this.log.message
|
||||
? ""
|
||||
: convert.toHtml(
|
||||
xss(this.log.message, {
|
||||
allowList: {span: ["style"]},
|
||||
})
|
||||
);
|
||||
|
||||
logMessage = logMessage.replaceAll(
|
||||
/(['"]?)(https?:\/\/[^'"\s]+)(['"]?)/g,
|
||||
"$1<a href='$2' target='_blank'>$2</a>$3"
|
||||
);
|
||||
return logMessage;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener("storage", (event) => {
|
||||
if (event.key === "logsFontSize") {
|
||||
this.logsFontSize = parseInt(event.newValue);
|
||||
if (key === "namespace") {
|
||||
meta["router"] = {name: "flows/list", query: {namespace: props.log[key]}};
|
||||
}
|
||||
});
|
||||
if (key === "flowId") {
|
||||
meta["router"] = {
|
||||
name: "flows/update",
|
||||
params: {namespace: props.log["namespace"], id: props.log[key]},
|
||||
};
|
||||
}
|
||||
metaWithValue.push(meta);
|
||||
}
|
||||
}
|
||||
return metaWithValue;
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
linkify(this.$refs.lineContent, this.$router);
|
||||
}, 200);
|
||||
},
|
||||
watch: {
|
||||
renderedMarkdown() {
|
||||
this.$nextTick(() => {
|
||||
linkify(this.$refs.lineContent, this.$router);
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
const levelStyle = computed(() => {
|
||||
const lowerCaseLevel = props.log?.level?.toLowerCase();
|
||||
return {
|
||||
"border-color": `var(--ks-log-border-${lowerCaseLevel})`,
|
||||
"color": `var(--ks-log-content-${lowerCaseLevel})`,
|
||||
"background-color": `var(--ks-log-background-${lowerCaseLevel})`,
|
||||
};
|
||||
});
|
||||
|
||||
const filtered = computed(() =>
|
||||
props.filter === "" || (props.log.message && props.log.message.toLowerCase().includes(props.filter ?? ""))
|
||||
);
|
||||
|
||||
const iconColor = computed(() => {
|
||||
const logLevel = props.log.level?.toLowerCase();
|
||||
return `var(--ks-log-content-${logLevel}) !important`;
|
||||
});
|
||||
|
||||
const message = computed(() => {
|
||||
let logMessage = !props.log.message
|
||||
? ""
|
||||
: convert.toHtml(
|
||||
xss(props.log.message, {
|
||||
allowList: {span: ["style"]},
|
||||
})
|
||||
);
|
||||
logMessage = logMessage.replaceAll(
|
||||
/(['"]?)(https?:\/\/[^'"\s]+)(['"]?)/g,
|
||||
"$1<a href='$2' target='_blank'>$2</a>$3"
|
||||
);
|
||||
return logMessage;
|
||||
});
|
||||
|
||||
const router = useRouter()
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
linkify(lineContent.value, router);
|
||||
}, 200);
|
||||
});
|
||||
|
||||
watch(renderedMarkdown, () => {
|
||||
nextTick(() => {
|
||||
linkify(lineContent.value, router);
|
||||
});
|
||||
});
|
||||
|
||||
// Initial markdown render
|
||||
(async () => {
|
||||
renderedMarkdown.value = await Markdown.render(message.value, {onlyLink: true, html: true});
|
||||
})();
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
div.line {
|
||||
|
||||
@@ -131,7 +131,7 @@
|
||||
import FilePreview from "../executions/FilePreview.vue";
|
||||
import {apiUrl} from "override/utils/route";
|
||||
import Utils from "../../utils/utils";
|
||||
import LogUtils from "../../utils/logs";
|
||||
import * as LogUtils from "../../utils/logs";
|
||||
import throttle from "lodash/throttle";
|
||||
|
||||
export default {
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
import {useNamespacesStore} from "override/stores/namespaces";
|
||||
import TopNavBar from "../layout/TopNavBar.vue";
|
||||
import Actions from "override/components/namespaces/Actions.vue";
|
||||
import {useMiscStore} from "override/stores/misc";
|
||||
// @ts-expect-error no types in Tabs yet
|
||||
import Tabs from "../Tabs.vue";
|
||||
|
||||
@@ -43,7 +44,9 @@
|
||||
const dateTimeKeys = ["startDate", "endDate", "timeRange"];
|
||||
|
||||
if (!Object.keys(route.query).some((key) => dateTimeKeys.some((dateTimeKey) => key.includes(dateTimeKey)))) {
|
||||
const newQuery = {...route.query, "filters[timeRange][EQUALS]": "PT168H"};
|
||||
const miscStore = useMiscStore();
|
||||
const defaultDuration = miscStore.configs?.chartDefaultDuration || "P30D";
|
||||
const newQuery = {...route.query, "filters[timeRange][EQUALS]": defaultDuration};
|
||||
router.replace({name: route.name, params: route.params, query: newQuery});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,9 +37,10 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, computed, watch, onMounted, nextTick, inject} from "vue";
|
||||
import getTaskComponent, {Schema} from "./getTaskComponent";
|
||||
import {Schema} from "./getTaskComponent";
|
||||
import * as YAML_UTILS from "@kestra-io/ui-libs/flow-yaml-utils";
|
||||
import {SCHEMA_DEFINITIONS_INJECTION_KEY} from "../../injectionKeys";
|
||||
import {useBlockComponent} from "./useBlockComponent";
|
||||
|
||||
const props = defineProps<{
|
||||
schema: Schema,
|
||||
@@ -161,8 +162,10 @@
|
||||
return consolidateAllOfSchemas(rawSchema, definitions.value);
|
||||
});
|
||||
|
||||
const {getBlockComponent} = useBlockComponent();
|
||||
|
||||
const currentSchemaType = computed(() =>
|
||||
delayedSelectedSchema.value ? getTaskComponent(currentSchema.value) : undefined
|
||||
delayedSelectedSchema.value ? getBlockComponent.value(currentSchema.value) : undefined
|
||||
);
|
||||
|
||||
const isSelectingPlugins = computed(() => schemas.value.length > 4);
|
||||
|
||||
@@ -45,9 +45,9 @@
|
||||
import {DeleteOutline, ChevronUp, ChevronDown} from "../../utils/icons";
|
||||
|
||||
import Add from "../Add.vue";
|
||||
import getTaskComponent from "./getTaskComponent";
|
||||
import Wrapper from "./Wrapper.vue";
|
||||
import {BLOCK_SCHEMA_PATH_INJECTION_KEY} from "../../injectionKeys";
|
||||
import {useBlockComponent} from "./useBlockComponent";
|
||||
|
||||
defineOptions({inheritAttrs: false});
|
||||
|
||||
@@ -70,8 +70,10 @@
|
||||
root: undefined,
|
||||
});
|
||||
|
||||
const {getBlockComponent} = useBlockComponent();
|
||||
|
||||
const componentType = computed(() => {
|
||||
return getTaskComponent(props.schema.items, props.root);
|
||||
return getBlockComponent.value?.(props.schema.items, props.root);
|
||||
});
|
||||
|
||||
const needWrapper = computed(() => {
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<component
|
||||
:is="getTaskComponent(schema, key, properties)"
|
||||
:is="getBlockComponent(schema, key, properties)"
|
||||
:modelValue="getPropertiesValue(key)"
|
||||
@update:model-value="onObjectInput(key, $event)"
|
||||
:root="getKey(key)"
|
||||
@@ -50,17 +50,23 @@
|
||||
</el-form>
|
||||
</template>
|
||||
<script setup>
|
||||
import getTaskComponent from "./getTaskComponent";
|
||||
import Help from "vue-material-design-icons/HelpBox.vue";
|
||||
import Markdown from "../../../layout/Markdown.vue";
|
||||
</script>
|
||||
<script>
|
||||
import Task from "./MixinTask";
|
||||
import {useBlockComponent} from "./useBlockComponent";
|
||||
|
||||
export default {
|
||||
name: "TaskBasic",
|
||||
mixins: [Task],
|
||||
emits: ["update:modelValue"],
|
||||
setup() {
|
||||
const {getBlockComponent} = useBlockComponent();
|
||||
return {
|
||||
getBlockComponent,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
properties() {
|
||||
if (this.schema) {
|
||||
|
||||
@@ -74,9 +74,10 @@
|
||||
import InputText from "../inputs/InputText.vue";
|
||||
import TaskExpression from "./TaskExpression.vue";
|
||||
import Add from "../Add.vue";
|
||||
import getTaskComponent from "./getTaskComponent";
|
||||
import debounce from "lodash/debounce";
|
||||
import Wrapper from "./Wrapper.vue";
|
||||
import {useBlockComponent} from "./useBlockComponent";
|
||||
import {useToast} from "../../../../utils/toast";
|
||||
|
||||
const {t, te} = useI18n();
|
||||
|
||||
@@ -98,8 +99,13 @@
|
||||
schema: () => ({type: "object"})
|
||||
});
|
||||
|
||||
const {getBlockComponent} = useBlockComponent();
|
||||
|
||||
const componentType = computed(() => {
|
||||
return props.schema.additionalProperties ? getTaskComponent(props.schema.additionalProperties, props.root) : null;
|
||||
return props.schema?.additionalProperties ? getBlockComponent.value(
|
||||
props.schema.additionalProperties,
|
||||
props.root
|
||||
) : undefined;
|
||||
});
|
||||
|
||||
const currentValue = ref<[string, any][]>([])
|
||||
@@ -167,8 +173,11 @@
|
||||
emitUpdate()
|
||||
}
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
function addItem() {
|
||||
if(addButtonDisabled.value) {
|
||||
toast.warning(t("no_code.add.disabled_warning"));
|
||||
return;
|
||||
}
|
||||
currentValue.value.push(["", undefined]);
|
||||
|
||||
@@ -69,7 +69,8 @@
|
||||
import Markdown from "../../../layout/Markdown.vue";
|
||||
import TaskLabelWithBoolean from "./TaskLabelWithBoolean.vue";
|
||||
import ClearButton from "./ClearButton.vue";
|
||||
import getTaskComponent from "./getTaskComponent";
|
||||
import {useBlockComponent} from "./useBlockComponent";
|
||||
|
||||
|
||||
const props = defineProps<{
|
||||
schema: any;
|
||||
@@ -134,8 +135,10 @@
|
||||
return type.value.ksTaskName;
|
||||
})
|
||||
|
||||
const {getBlockComponent} = useBlockComponent();
|
||||
|
||||
const type = computed(() => {
|
||||
return getTaskComponent(props.schema, props.fieldKey)
|
||||
return getBlockComponent.value(props.schema, props.fieldKey)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import {inject} from "vue";
|
||||
import {pascalCase} from "change-case";
|
||||
import {resolve$ref} from "../../../../utils/utils";
|
||||
import {SCHEMA_DEFINITIONS_INJECTION_KEY} from "../../injectionKeys";
|
||||
|
||||
const TasksComponents = import.meta.glob<{ default: any }>("./Task*.vue", {eager: true});
|
||||
|
||||
@@ -20,9 +18,8 @@ export interface Schema{
|
||||
format?: string;
|
||||
}
|
||||
|
||||
function getType(property: any, key?: string): string {
|
||||
const definitionsRef = inject(SCHEMA_DEFINITIONS_INJECTION_KEY);
|
||||
const definitions = definitionsRef?.value;
|
||||
function getType(property: any, definitions: Record<string, any>, key?: string): string {
|
||||
|
||||
if (property.enum !== undefined) {
|
||||
return "enum";
|
||||
}
|
||||
@@ -109,12 +106,12 @@ function getType(property: any, key?: string): string {
|
||||
return property.type || "expression";
|
||||
}
|
||||
|
||||
export default function getTaskComponent(property: any, key?: string): any {
|
||||
const typeString = getType(property, key);
|
||||
export function getTaskComponent(property: any, definitions: Record<string, any>, key?: string): any {
|
||||
const typeString = getType(property, definitions, key);
|
||||
const type = pascalCase(typeString);
|
||||
const component = TasksComponents[`./Task${type}.vue`]?.default;
|
||||
if (component) {
|
||||
component.ksTaskName = typeString;
|
||||
}
|
||||
return component ?? {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import {h, inject, onMounted, ref} from "vue"
|
||||
import {SCHEMA_DEFINITIONS_INJECTION_KEY} from "../../injectionKeys";
|
||||
|
||||
export function useBlockComponent() {
|
||||
const definitionsRef = inject(SCHEMA_DEFINITIONS_INJECTION_KEY);
|
||||
const definitions = definitionsRef?.value ?? {};
|
||||
|
||||
const getBlockComponent = ref<(property: any, key?: string) => any>(() => {
|
||||
return h("div", {class: "no-code-skeleton"}, "Loading...");
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
const module = await import("./getTaskComponent");
|
||||
getBlockComponent.value = (property: any, key?: string) => module.getTaskComponent(property, definitions, key);
|
||||
})
|
||||
|
||||
return {
|
||||
getBlockComponent
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@
|
||||
tooltip,
|
||||
getFormat,
|
||||
} from "../dashboard/composables/charts";
|
||||
import Logs from "../../utils/logs";
|
||||
import * as Logs from "../../utils/logs";
|
||||
|
||||
export default defineComponent({
|
||||
components: {Bar},
|
||||
|
||||
@@ -78,10 +78,17 @@ export const useBaseNamespacesStore = () => {
|
||||
}
|
||||
const data = response.data;
|
||||
const contentLength = response.headers?.["content-length"];
|
||||
|
||||
let value = data;
|
||||
if (contentLength === (data.length + 2).toString()) {
|
||||
return `"${data}"`;
|
||||
value = `"${data}"`;
|
||||
}
|
||||
return data;
|
||||
return {
|
||||
type: response.headers?.["content-type"] || "STRING",
|
||||
value: value,
|
||||
description: response.headers?.["description"] || "",
|
||||
ttl: response.headers?.["ttl"] || undefined
|
||||
};
|
||||
}
|
||||
|
||||
async function loadInheritedKVs(this: any, id: string) {
|
||||
|
||||
@@ -7,13 +7,16 @@ import DemoAuditLogs from "../components/demo/AuditLogs.vue"
|
||||
import DemoInstance from "../components/demo/Instance.vue"
|
||||
import DemoApps from "../components/demo/Apps.vue"
|
||||
import DemoTests from "../components/demo/Tests.vue"
|
||||
import {useMiscStore} from "override/stores/misc";
|
||||
|
||||
function maybeAddTimeRangeFilter(to) {
|
||||
const dateTimeKeys = ["startDate", "endDate", "timeRange"];
|
||||
|
||||
// Default to the last 7 days if no time range is set
|
||||
// Default to the configured duration if no time range is set
|
||||
if (!Object.keys(to.query).some((key) => dateTimeKeys.some((dateTimeKey) => key.includes(dateTimeKey)))) {
|
||||
to.query["filters[timeRange][EQUALS]"] = "PT168H";
|
||||
const miscStore = useMiscStore();
|
||||
const defaultDuration = miscStore.configs?.chartDefaultDuration || "P30D"; // Fallback to 30 days
|
||||
to.query["filters[timeRange][EQUALS]"] = defaultDuration;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,28 +1,50 @@
|
||||
import {defineStore} from "pinia";
|
||||
import {apiUrl} from "override/utils/route";
|
||||
import {ref} from "vue";
|
||||
import {useAxios} from "../utils/axios";
|
||||
import {LevelKey} from "../utils/logs";
|
||||
|
||||
interface LogStoreState {
|
||||
logs: any[] | undefined;
|
||||
total: number;
|
||||
level: string;
|
||||
export interface Log{
|
||||
level: LevelKey;
|
||||
namespace: string;
|
||||
flowId: string;
|
||||
executionId: string;
|
||||
triggerId?: string;
|
||||
taskId?: string;
|
||||
thread: string;
|
||||
taskRunId?: string;
|
||||
index: number;
|
||||
attemptNumber: number;
|
||||
executionKind: "flow" | "playground";
|
||||
timestamp: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export const useLogsStore = defineStore("logs", {
|
||||
state: (): LogStoreState => ({
|
||||
logs: undefined,
|
||||
total: 0,
|
||||
level: "INFO",
|
||||
}),
|
||||
actions: {
|
||||
findLogs(options: any) {
|
||||
return this.$http.get(`${apiUrl()}/logs/search`, {params: options}).then(response => {
|
||||
this.logs = response.data.results
|
||||
this.total = response.data.total
|
||||
})
|
||||
},
|
||||
deleteLogs(log: { namespace: string, flowId: string, triggerId?: string }) {
|
||||
const URL = `${apiUrl()}/logs/${log.namespace}/${log.flowId}${log.triggerId ? `?triggerId=${log.triggerId}` : ""}`;
|
||||
return this.$http.delete(URL).then(() => (this.logs = undefined))
|
||||
},
|
||||
export const useLogsStore = defineStore("logs", () => {
|
||||
const logs = ref<Log[]>()
|
||||
const total = ref(0)
|
||||
const level = ref<LevelKey>("INFO")
|
||||
|
||||
const axios = useAxios();
|
||||
|
||||
|
||||
function findLogs(options: any) {
|
||||
return axios.get(`${apiUrl()}/logs/search`, {params: options}).then(response => {
|
||||
logs.value = response.data.results
|
||||
total.value = response.data.total
|
||||
})
|
||||
}
|
||||
|
||||
function deleteLogs(log: { namespace: string, flowId: string, triggerId?: string }) {
|
||||
const URL = `${apiUrl()}/logs/${log.namespace}/${log.flowId}${log.triggerId ? `?triggerId=${log.triggerId}` : ""}`;
|
||||
return axios.delete(URL).then(() => (logs.value = undefined))
|
||||
}
|
||||
|
||||
return {
|
||||
logs,
|
||||
total,
|
||||
level,
|
||||
findLogs,
|
||||
deleteLogs,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -188,3 +188,50 @@ html.full-screen {
|
||||
body{
|
||||
background-color: var(--ks-background-body);
|
||||
}
|
||||
|
||||
.no-code-skeleton{
|
||||
color: var(--ks-content-secondary);
|
||||
background-color: var(--ks-background-card);
|
||||
width: 100%;
|
||||
border-radius: 4px;
|
||||
padding: 0 .5rem;
|
||||
// slowly appearing grey background animation
|
||||
animation: skeleton-loading 6s 1;
|
||||
position: relative;
|
||||
&:before{
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-image: linear-gradient(110deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, .1) 35%, rgba(255, 255, 255, 0) 75%, rgba(255, 255, 255, 0) 100%);
|
||||
background-size: 200% 200%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 0 0;
|
||||
opacity: 0.7;
|
||||
border-radius: 4px;
|
||||
animation: skeleton-animation 3s infinite;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes skeleton-animation {
|
||||
0% {
|
||||
background-position: -55% -30%;
|
||||
}
|
||||
100% {
|
||||
background-position: 185% 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes skeleton-loading {
|
||||
0% {
|
||||
opacity:0;
|
||||
}
|
||||
40% {
|
||||
opacity:0;
|
||||
}
|
||||
100% {
|
||||
opacity:.7;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -917,7 +917,6 @@
|
||||
"status": "Status",
|
||||
"task": "Aufgabe",
|
||||
"text": "I'm sorry, but it seems that there is no text provided after \"----------\" for translation. Could you please provide the text you would like me to translate?",
|
||||
"trigger_state": "Zustand",
|
||||
"type": "Typ",
|
||||
"user": "Benutzer"
|
||||
},
|
||||
|
||||
@@ -1124,7 +1124,6 @@
|
||||
"namespace": "Namespace",
|
||||
"flow": "Flow",
|
||||
"state": "State",
|
||||
"trigger_state": "State",
|
||||
"scope": "Scope",
|
||||
"child": "Child",
|
||||
"level": "Log level",
|
||||
|
||||
@@ -917,7 +917,6 @@
|
||||
"status": "Estado",
|
||||
"task": "Tarea",
|
||||
"text": "I'm sorry, but it seems like the text you want me to translate is missing. Could you please provide the text again?",
|
||||
"trigger_state": "Estado",
|
||||
"type": "Tipo",
|
||||
"user": "Usuario"
|
||||
},
|
||||
|
||||
@@ -917,7 +917,6 @@
|
||||
"status": "Statut",
|
||||
"task": "Tâche",
|
||||
"text": "I'm sorry, but it seems like there is no text provided for translation. Could you please provide the text you would like to have translated into French?",
|
||||
"trigger_state": "État",
|
||||
"type": "Type",
|
||||
"user": "Utilisateur"
|
||||
},
|
||||
|
||||
@@ -917,7 +917,6 @@
|
||||
"status": "स्थिति",
|
||||
"task": "कार्य",
|
||||
"text": "मुझे अनुवाद के लिए कोई पाठ नहीं मिला। कृपया अनुवाद के लिए पाठ प्रदान करें।",
|
||||
"trigger_state": "स्थिति",
|
||||
"type": "प्रकार",
|
||||
"user": "उपयोगकर्ता"
|
||||
},
|
||||
|
||||
@@ -917,7 +917,6 @@
|
||||
"status": "Stato",
|
||||
"task": "Compito",
|
||||
"text": "I'm sorry, but it seems like the text you want me to translate is missing. Could you please provide the text you would like translated?",
|
||||
"trigger_state": "Stato",
|
||||
"type": "Tipo",
|
||||
"user": "Utente"
|
||||
},
|
||||
|
||||
@@ -917,7 +917,6 @@
|
||||
"status": "ステータス",
|
||||
"task": "タスク",
|
||||
"text": "申し訳ありませんが、翻訳するためのテキストが提供されていないようです。翻訳が必要なテキストをもう一度送信してください。",
|
||||
"trigger_state": "状態",
|
||||
"type": "タイプ",
|
||||
"user": "ユーザー"
|
||||
},
|
||||
|
||||
@@ -917,7 +917,6 @@
|
||||
"status": "상태",
|
||||
"task": "작업",
|
||||
"text": "텍스트",
|
||||
"trigger_state": "상태",
|
||||
"type": "유형",
|
||||
"user": "사용자"
|
||||
},
|
||||
|
||||
@@ -917,7 +917,6 @@
|
||||
"status": "Status",
|
||||
"task": "Zadanie",
|
||||
"text": "tekst",
|
||||
"trigger_state": "Stan",
|
||||
"type": "Typ",
|
||||
"user": "Użytkownik"
|
||||
},
|
||||
|
||||
@@ -917,7 +917,6 @@
|
||||
"status": "Status",
|
||||
"task": "Tarefa",
|
||||
"text": "Parece que você não forneceu nenhum texto para tradução. Por favor, forneça o texto que deseja que eu traduza para o português.",
|
||||
"trigger_state": "Estado",
|
||||
"type": "Tipo",
|
||||
"user": "Usuário"
|
||||
},
|
||||
|
||||
@@ -917,7 +917,6 @@
|
||||
"status": "Status",
|
||||
"task": "Tarefa",
|
||||
"text": "Parece que você não forneceu nenhum texto para tradução. Por favor, forneça o texto que deseja que eu traduza para o português.",
|
||||
"trigger_state": "Estado",
|
||||
"type": "Tipo",
|
||||
"user": "Usuário"
|
||||
},
|
||||
|
||||
@@ -917,7 +917,6 @@
|
||||
"status": "Статус",
|
||||
"task": "Задача",
|
||||
"text": "I'm sorry, but it seems that the text you want me to translate is missing. Could you please provide the text that needs to be translated?",
|
||||
"trigger_state": "Состояние",
|
||||
"type": "Тип",
|
||||
"user": "Пользователь"
|
||||
},
|
||||
|
||||
@@ -917,7 +917,6 @@
|
||||
"status": "状态",
|
||||
"task": "任务",
|
||||
"text": "文本",
|
||||
"trigger_state": "状态",
|
||||
"type": "类型",
|
||||
"user": "用户"
|
||||
},
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
import {cssVariable} from "@kestra-io/ui-libs";
|
||||
|
||||
const LEVELS = [
|
||||
"ERROR",
|
||||
"WARN",
|
||||
"INFO",
|
||||
"DEBUG",
|
||||
"TRACE"
|
||||
];
|
||||
|
||||
export default class Logs {
|
||||
static color() {
|
||||
return Object.fromEntries(LEVELS.map(level => [level, cssVariable("--log-chart-" + level.toLowerCase())]));
|
||||
}
|
||||
|
||||
static graphColors(state) {
|
||||
const COLORS = {
|
||||
ERROR: "#AB0009",
|
||||
WARN: "#DD5F00",
|
||||
INFO: "#029E73",
|
||||
DEBUG: "#1761FD",
|
||||
TRACE: "#8405FF",
|
||||
};
|
||||
|
||||
return COLORS[state];
|
||||
}
|
||||
|
||||
static chartColorFromLevel(level, alpha = 1) {
|
||||
const hex = Logs.color()[level];
|
||||
if (!hex) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [r, g, b] = hex.match(/\w\w/g).map(x => parseInt(x, 16));
|
||||
return `rgba(${r},${g},${b},${alpha})`;
|
||||
}
|
||||
|
||||
static sort(value) {
|
||||
return Object.keys(value)
|
||||
.sort((a, b) => {
|
||||
return Logs.index(LEVELS, a) - Logs.index(LEVELS, b);
|
||||
})
|
||||
.reduce(
|
||||
(obj, key) => {
|
||||
obj[key] = value[key];
|
||||
return obj;
|
||||
},
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
static index(based, value) {
|
||||
const index = based.indexOf(value);
|
||||
|
||||
return index === -1 ? Number.MAX_SAFE_INTEGER : index;
|
||||
}
|
||||
|
||||
static levelOrLower(level) {
|
||||
const levels = [];
|
||||
for (const currentLevel of LEVELS) {
|
||||
levels.push(currentLevel);
|
||||
if (currentLevel === level) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return levels.reverse();
|
||||
}
|
||||
}
|
||||
70
ui/src/utils/logs.ts
Normal file
70
ui/src/utils/logs.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import {cssVariable} from "@kestra-io/ui-libs";
|
||||
|
||||
const LEVELS = [
|
||||
"ERROR",
|
||||
"WARN",
|
||||
"INFO",
|
||||
"DEBUG",
|
||||
"TRACE"
|
||||
] as const
|
||||
|
||||
export type LevelKey = typeof LEVELS[number];
|
||||
|
||||
|
||||
export function color() {
|
||||
return Object.fromEntries(LEVELS.map(level => [level, cssVariable("--log-chart-" + level.toLowerCase())]));
|
||||
}
|
||||
|
||||
export function graphColors(state: LevelKey) {
|
||||
const COLORS = {
|
||||
ERROR: "#AB0009",
|
||||
WARN: "#DD5F00",
|
||||
INFO: "#029E73",
|
||||
DEBUG: "#1761FD",
|
||||
TRACE: "#8405FF",
|
||||
};
|
||||
|
||||
return COLORS[state];
|
||||
}
|
||||
|
||||
export function chartColorFromLevel(level: LevelKey, alpha = 1) {
|
||||
const hex = color()[level];
|
||||
if (!hex) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [r, g, b] = hex.match(/\w\w/g)?.map(x => parseInt(x, 16)) || [];
|
||||
return `rgba(${r},${g},${b},${alpha})`;
|
||||
}
|
||||
|
||||
export function sort(value: Record<string, any>) {
|
||||
return Object.keys(value)
|
||||
.sort((a, b) => {
|
||||
return index(LEVELS, a) - index(LEVELS, b);
|
||||
})
|
||||
.reduce(
|
||||
(obj, key) => {
|
||||
obj[key] = value[key];
|
||||
return obj;
|
||||
},
|
||||
{} as Record<string, any>
|
||||
);
|
||||
}
|
||||
|
||||
export function index(based: readonly string[], value: string) {
|
||||
const index = based.indexOf(value);
|
||||
|
||||
return index === -1 ? Number.MAX_SAFE_INTEGER : index;
|
||||
}
|
||||
|
||||
export function levelOrLower(level: LevelKey) {
|
||||
const levels: LevelKey[] = [];
|
||||
for (const currentLevel of LEVELS) {
|
||||
levels.push(currentLevel);
|
||||
if (currentLevel === level) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return levels.reverse();
|
||||
}
|
||||
|
||||
@@ -62,27 +62,21 @@ export const AppTableBlock: Story = {
|
||||
render: AppTableBlockRender,
|
||||
async play({canvasElement}) {
|
||||
const canvas = within(canvasElement);
|
||||
canvas.getByText("+ Add a new value").click();
|
||||
await waitFor(() => {
|
||||
expect(canvas.getByText(/null/)).toBeVisible();
|
||||
});
|
||||
canvas.getByText("+ Add a new value", {selector: ".schema-wrapper .schema-wrapper button"}).click();
|
||||
fireEvent.click(await canvas.findByText("+ Add a new value"));
|
||||
expect(await canvas.findByText(/null/, {selector: "pre"})).toBeVisible();
|
||||
fireEvent.click(await canvas.findByText("+ Add a new value", {selector: ".schema-wrapper .schema-wrapper button"}));
|
||||
|
||||
await waitFor(function getByPlaceholderKey() {
|
||||
expect(canvas.getByPlaceholderText("Key")).toBeVisible();
|
||||
});
|
||||
fireEvent.input(await canvas.findByPlaceholderText("Key"), {target: {value: "key1"}})
|
||||
fireEvent.input(await canvas.findByTestId("monaco-editor-hidden-synced-textarea"), {target: {value: "value1"}})
|
||||
|
||||
fireEvent.input(canvas.getByPlaceholderText("Key"), {target: {value: "key1"}})
|
||||
fireEvent.input(canvas.getByTestId("monaco-editor-hidden-synced-textarea"), {target: {value: "value1"}})
|
||||
|
||||
fireEvent.click(canvas.getByText("+ Add a new value", {selector: ".schema-wrapper .schema-wrapper button"}))
|
||||
fireEvent.click(await canvas.findByText("+ Add a new value", {selector: ".schema-wrapper .schema-wrapper button"}))
|
||||
|
||||
await waitFor(function getByPlaceholderKey() {
|
||||
expect(canvas.getAllByPlaceholderText("Key")[1]).toBeVisible();
|
||||
});
|
||||
|
||||
fireEvent.input(canvas.getAllByPlaceholderText("Key")[1], {target: {value: "key2"}})
|
||||
fireEvent.input(canvas.getAllByTestId("monaco-editor-hidden-synced-textarea")[1], {target: {value: "value2"}})
|
||||
fireEvent.input((await canvas.findAllByPlaceholderText("Key"))[1], {target: {value: "key2"}})
|
||||
fireEvent.input((await canvas.findAllByTestId("monaco-editor-hidden-synced-textarea"))[1], {target: {value: "value2"}})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(canvas.getByTestId("resulting-object").innerHTML).toBe(JSON.stringify({
|
||||
|
||||
@@ -71,6 +71,9 @@ import io.opentelemetry.context.propagation.ContextPropagators;
|
||||
import io.opentelemetry.context.propagation.TextMapPropagator;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.extensions.Extension;
|
||||
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
|
||||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.ExampleObject;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
@@ -206,7 +209,7 @@ public class ExecutionController {
|
||||
@Parameter(description = "The current page") @QueryValue(defaultValue = "1") @Min(1) int page,
|
||||
@Parameter(description = "The current page size") @QueryValue(defaultValue = "10") @Min(1) int size,
|
||||
@Parameter(description = "The sort of current page") @Nullable @QueryValue List<String> sort,
|
||||
@Parameter(description = "Filters") @QueryFilterFormat List<QueryFilter> filters,
|
||||
@Parameter(description = "Filters", in = ParameterIn.QUERY) @QueryFilterFormat List<QueryFilter> filters,
|
||||
//Deprecated params
|
||||
@Parameter(description = "A string filter", deprecated = true) @Nullable @QueryValue(value = "q") String query,
|
||||
@Parameter(description = "The scope of the executions to include", deprecated = true) @Nullable @QueryValue(value = "scope") List<FlowScope> scope,
|
||||
@@ -355,9 +358,9 @@ public class ExecutionController {
|
||||
@ApiResponse(responseCode = "204", description = "On success")
|
||||
public HttpResponse<Void> deleteExecution(
|
||||
@Parameter(description = "The execution id") @PathVariable String executionId,
|
||||
@Parameter(description = "Whether to delete execution logs") @QueryValue(defaultValue = "true") Boolean deleteLogs,
|
||||
@Parameter(description = "Whether to delete execution metrics") @QueryValue(defaultValue = "true") Boolean deleteMetrics,
|
||||
@Parameter(description = "Whether to delete execution files in the internal storage") @QueryValue(defaultValue = "true") Boolean deleteStorage
|
||||
@Parameter(description = "Whether to delete execution logs", required = false) @QueryValue(defaultValue = "true") Boolean deleteLogs,
|
||||
@Parameter(description = "Whether to delete execution metrics", required = false) @QueryValue(defaultValue = "true") Boolean deleteMetrics,
|
||||
@Parameter(description = "Whether to delete execution files in the internal storage", required = false) @QueryValue(defaultValue = "true") Boolean deleteStorage
|
||||
) throws IOException {
|
||||
Optional<Execution> execution = executionRepository.findById(tenantService.resolveTenant(), executionId);
|
||||
if (execution.isPresent()) {
|
||||
@@ -376,9 +379,9 @@ public class ExecutionController {
|
||||
public MutableHttpResponse<?> deleteExecutionsByIds(
|
||||
@RequestBody(description = "The execution id") @Body List<String> executionsId,
|
||||
@Parameter(description = "Whether to delete non-terminated executions") @Nullable @QueryValue(defaultValue = "false") Boolean includeNonTerminated,
|
||||
@Parameter(description = "Whether to delete execution logs") @QueryValue(defaultValue = "true") Boolean deleteLogs,
|
||||
@Parameter(description = "Whether to delete execution metrics") @QueryValue(defaultValue = "true") Boolean deleteMetrics,
|
||||
@Parameter(description = "Whether to delete execution files in the internal storage") @QueryValue(defaultValue = "true") Boolean deleteStorage
|
||||
@Parameter(description = "Whether to delete execution logs", required = false) @QueryValue(defaultValue = "true") Boolean deleteLogs,
|
||||
@Parameter(description = "Whether to delete execution metrics", required = false) @QueryValue(defaultValue = "true") Boolean deleteMetrics,
|
||||
@Parameter(description = "Whether to delete execution files in the internal storage", required = false) @QueryValue(defaultValue = "true") Boolean deleteStorage
|
||||
) throws IOException {
|
||||
List<Execution> executions = new ArrayList<>();
|
||||
Set<ManualConstraintViolation<String>> invalids = new HashSet<>();
|
||||
@@ -417,27 +420,27 @@ public class ExecutionController {
|
||||
@ExecuteOn(TaskExecutors.IO)
|
||||
@Operation(tags = {"Executions"}, summary = "Delete executions filter by query parameters")
|
||||
public HttpResponse<?> deleteExecutionsByQuery(
|
||||
@Parameter(description = "Filters") @QueryFilterFormat List<QueryFilter> filters,
|
||||
@Parameter(description = "Filters", in = ParameterIn.QUERY) @QueryFilterFormat List<QueryFilter> filters,
|
||||
|
||||
@Deprecated @Parameter(description = "A string filter") @Nullable @QueryValue(value = "q") String query,
|
||||
@Deprecated @Parameter(description = "The scope of the executions to include") @Nullable @QueryValue(value = "scope") List<FlowScope> scope,
|
||||
@Deprecated @Parameter(description = "A namespace filter prefix") @Nullable @QueryValue String namespace,
|
||||
@Deprecated @Parameter(description = "A flow id filter") @Nullable @QueryValue String flowId,
|
||||
@Deprecated @Parameter(description = "The start datetime") @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime startDate,
|
||||
@Deprecated @Parameter(description = "The end datetime") @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime endDate,
|
||||
@Deprecated @Parameter(description = "A time range filter relative to the current time", examples = {
|
||||
@Deprecated @Parameter(description = "A string filter", deprecated = true) @Nullable @QueryValue(value = "q") String query,
|
||||
@Deprecated @Parameter(description = "The scope of the executions to include", deprecated = true) @Nullable @QueryValue(value = "scope") List<FlowScope> scope,
|
||||
@Deprecated @Parameter(description = "A namespace filter prefix", deprecated = true) @Nullable @QueryValue String namespace,
|
||||
@Deprecated @Parameter(description = "A flow id filter", deprecated = true) @Nullable @QueryValue String flowId,
|
||||
@Deprecated @Parameter(description = "The start datetime", deprecated = true) @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime startDate,
|
||||
@Deprecated @Parameter(description = "The end datetime", deprecated = true) @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime endDate,
|
||||
@Deprecated @Parameter(description = "A time range filter relative to the current time", deprecated = true, examples = {
|
||||
@ExampleObject(name = "Filter last 5 minutes", value = "PT5M"),
|
||||
@ExampleObject(name = "Filter last 24 hours", value = "P1D")
|
||||
}) @Nullable @QueryValue Duration timeRange,
|
||||
@Deprecated @Parameter(description = "A state filter") @Nullable @QueryValue List<State.Type> state,
|
||||
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'") @Nullable @QueryValue @Format("MULTI") List<String> labels,
|
||||
@Deprecated @Parameter(description = "The trigger execution id") @Nullable @QueryValue String triggerExecutionId,
|
||||
@Deprecated @Parameter(description = "A execution child filter") @Nullable @QueryValue ExecutionRepositoryInterface.ChildFilter childFilter,
|
||||
@Deprecated @Parameter(description = "A state filter", deprecated = true) @Nullable @QueryValue List<State.Type> state,
|
||||
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'", deprecated = true) @Nullable @QueryValue @Format("MULTI") List<String> labels,
|
||||
@Deprecated @Parameter(description = "The trigger execution id", deprecated = true) @Nullable @QueryValue String triggerExecutionId,
|
||||
@Deprecated @Parameter(description = "A execution child filter", deprecated = true) @Nullable @QueryValue ExecutionRepositoryInterface.ChildFilter childFilter,
|
||||
|
||||
@Parameter(description = "Whether to delete non-terminated executions") @Nullable @QueryValue(defaultValue = "false") Boolean includeNonTerminated,
|
||||
@Parameter(description = "Whether to delete execution logs") @QueryValue(defaultValue = "true") Boolean deleteLogs,
|
||||
@Parameter(description = "Whether to delete execution metrics") @QueryValue(defaultValue = "true") Boolean deleteMetrics,
|
||||
@Parameter(description = "Whether to delete execution files in the internal storage") @QueryValue(defaultValue = "true") Boolean deleteStorage
|
||||
@Parameter(description = "Whether to delete execution logs", required = false) @QueryValue(defaultValue = "true") Boolean deleteLogs,
|
||||
@Parameter(description = "Whether to delete execution metrics", required = false) @QueryValue(defaultValue = "true") Boolean deleteMetrics,
|
||||
@Parameter(description = "Whether to delete execution files in the internal storage", required = false) @QueryValue(defaultValue = "true") Boolean deleteStorage
|
||||
) throws IOException {
|
||||
filters = RequestUtils.getFiltersOrDefaultToLegacyMapping(
|
||||
filters,
|
||||
@@ -690,7 +693,16 @@ public class ExecutionController {
|
||||
|
||||
@ExecuteOn(TaskExecutors.IO)
|
||||
@Post(uri = "/{namespace}/{id}", consumes = MediaType.MULTIPART_FORM_DATA)
|
||||
@Operation(tags = {"Executions"}, summary = "Create a new execution for a flow")
|
||||
@Operation(
|
||||
tags = {"Executions"},
|
||||
summary = "Create a new execution for a flow",
|
||||
extensions = @Extension(
|
||||
name = "x-sdk-customization",
|
||||
properties = {
|
||||
@ExtensionProperty(name = "x-multipart", value = "true")
|
||||
}
|
||||
)
|
||||
)
|
||||
@ApiResponse(responseCode = "409", description = "if the flow is disabled")
|
||||
@ApiResponse(responseCode = "200", description = "On execution created", content = {@Content(schema = @Schema(implementation = ExecutionResponse.class))})
|
||||
@SingleResult
|
||||
@@ -1020,22 +1032,22 @@ public class ExecutionController {
|
||||
@Post(uri = "/restart/by-query")
|
||||
@Operation(tags = {"Executions"}, summary = "Restart executions filter by query parameters")
|
||||
public HttpResponse<?> restartExecutionsByQuery(
|
||||
@Parameter(description = "Filters") @QueryFilterFormat List<QueryFilter> filters,
|
||||
@Parameter(description = "Filters", in = ParameterIn.QUERY) @QueryFilterFormat List<QueryFilter> filters,
|
||||
|
||||
@Deprecated @Parameter(description = "A string filter") @Nullable @QueryValue(value = "q") String query,
|
||||
@Deprecated @Parameter(description = "The scope of the executions to include") @Nullable @QueryValue(value = "scope") List<FlowScope> scope,
|
||||
@Deprecated @Parameter(description = "A namespace filter prefix") @Nullable @QueryValue String namespace,
|
||||
@Deprecated @Parameter(description = "A flow id filter") @Nullable @QueryValue String flowId,
|
||||
@Deprecated @Parameter(description = "The start datetime") @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime startDate,
|
||||
@Deprecated @Parameter(description = "The end datetime") @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime endDate,
|
||||
@Deprecated @Parameter(description = "A time range filter relative to the current time", examples = {
|
||||
@Deprecated @Parameter(description = "A string filter", deprecated = true) @Nullable @QueryValue(value = "q") String query,
|
||||
@Deprecated @Parameter(description = "The scope of the executions to include", deprecated = true) @Nullable @QueryValue(value = "scope") List<FlowScope> scope,
|
||||
@Deprecated @Parameter(description = "A namespace filter prefix", deprecated = true) @Nullable @QueryValue String namespace,
|
||||
@Deprecated @Parameter(description = "A flow id filter", deprecated = true) @Nullable @QueryValue String flowId,
|
||||
@Deprecated @Parameter(description = "The start datetime", deprecated = true) @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime startDate,
|
||||
@Deprecated @Parameter(description = "The end datetime", deprecated = true) @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime endDate,
|
||||
@Deprecated @Parameter(description = "A time range filter relative to the current time", deprecated = true, examples = {
|
||||
@ExampleObject(name = "Filter last 5 minutes", value = "PT5M"),
|
||||
@ExampleObject(name = "Filter last 24 hours", value = "P1D")
|
||||
}) @Nullable @QueryValue Duration timeRange,
|
||||
@Deprecated @Parameter(description = "A state filter") @Nullable @QueryValue List<State.Type> state,
|
||||
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'") @Nullable @QueryValue @Format("MULTI") List<String> labels,
|
||||
@Deprecated @Parameter(description = "The trigger execution id") @Nullable @QueryValue String triggerExecutionId,
|
||||
@Deprecated @Parameter(description = "A execution child filter") @Nullable @QueryValue ExecutionRepositoryInterface.ChildFilter childFilter
|
||||
@Deprecated @Parameter(description = "A state filter", deprecated = true) @Nullable @QueryValue List<State.Type> state,
|
||||
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'", deprecated = true) @Nullable @QueryValue @Format("MULTI") List<String> labels,
|
||||
@Deprecated @Parameter(description = "The trigger execution id", deprecated = true) @Nullable @QueryValue String triggerExecutionId,
|
||||
@Deprecated @Parameter(description = "A execution child filter", deprecated = true) @Nullable @QueryValue ExecutionRepositoryInterface.ChildFilter childFilter
|
||||
) throws Exception {
|
||||
filters = RequestUtils.getFiltersOrDefaultToLegacyMapping(
|
||||
filters,
|
||||
@@ -1080,13 +1092,32 @@ public class ExecutionController {
|
||||
|
||||
@ExecuteOn(TaskExecutors.IO)
|
||||
@Post(uri = "/{executionId}/replay-with-inputs", consumes = MediaType.MULTIPART_FORM_DATA)
|
||||
@Operation(tags = {"Executions"}, summary = "Create a new execution from an old one and start it from a specified task run id")
|
||||
@Operation(
|
||||
tags = {"Executions"},
|
||||
summary = "Create a new execution from an old one and start it from a specified task run id",
|
||||
extensions = @Extension(
|
||||
name = "x-sdk-customization",
|
||||
properties = {
|
||||
@ExtensionProperty(name = "x-multipart", value = "true")
|
||||
}
|
||||
)
|
||||
)
|
||||
public Mono<Execution> replayExecutionWithinputs(
|
||||
@Parameter(description = "the original execution id to clone") @PathVariable String executionId,
|
||||
@Parameter(description = "The taskrun id") @Nullable @QueryValue String taskRunId,
|
||||
@Parameter(description = "The flow revision to use for new execution") @Nullable @QueryValue Integer revision,
|
||||
@Parameter(description = "Set a list of breakpoints at specific tasks 'id.value', separated by a coma.") @QueryValue Optional<String> breakpoints,
|
||||
@RequestBody(description = "The inputs") @Body MultipartBody inputs
|
||||
@RequestBody(
|
||||
description = "The inputs (multipart map)",
|
||||
content = @Content(
|
||||
mediaType = MediaType.MULTIPART_FORM_DATA,
|
||||
schema = @Schema(
|
||||
type = "object",
|
||||
additionalProperties = Schema.AdditionalPropertiesValue.TRUE,
|
||||
additionalPropertiesSchema = Object.class
|
||||
)
|
||||
)
|
||||
) @Body MultipartBody inputs
|
||||
) {
|
||||
Optional<Execution> execution = executionRepository.findById(tenantService.resolveTenant(), executionId);
|
||||
if (execution.isEmpty()) {
|
||||
@@ -1263,22 +1294,22 @@ public class ExecutionController {
|
||||
@ApiResponse(responseCode = "200", description = "On success", content = {@Content(schema = @Schema(implementation = BulkResponse.class))})
|
||||
@ApiResponse(responseCode = "422", description = "Changed state with errors", content = {@Content(schema = @Schema(implementation = BulkErrorResponse.class))})
|
||||
public HttpResponse<?> updateExecutionsStatusByQuery(
|
||||
@Parameter(description = "Filters") @QueryFilterFormat List<QueryFilter> filters,
|
||||
@Parameter(description = "Filters", in = ParameterIn.QUERY) @QueryFilterFormat List<QueryFilter> filters,
|
||||
|
||||
@Deprecated @Parameter(description = "A string filter") @Nullable @QueryValue(value = "q") String query,
|
||||
@Deprecated @Parameter(description = "The scope of the executions to include") @Nullable @QueryValue(value = "scope") List<FlowScope> scope,
|
||||
@Deprecated @Parameter(description = "A namespace filter prefix") @Nullable @QueryValue String namespace,
|
||||
@Deprecated @Parameter(description = "A flow id filter") @Nullable @QueryValue String flowId,
|
||||
@Deprecated @Parameter(description = "The start datetime") @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime startDate,
|
||||
@Deprecated @Parameter(description = "The end datetime") @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime endDate,
|
||||
@Deprecated @Parameter(description = "A time range filter relative to the current time", examples = {
|
||||
@Deprecated @Parameter(description = "A string filter", deprecated = true) @Nullable @QueryValue(value = "q") String query,
|
||||
@Deprecated @Parameter(description = "The scope of the executions to include", deprecated = true) @Nullable @QueryValue(value = "scope") List<FlowScope> scope,
|
||||
@Deprecated @Parameter(description = "A namespace filter prefix", deprecated = true) @Nullable @QueryValue String namespace,
|
||||
@Deprecated @Parameter(description = "A flow id filter", deprecated = true) @Nullable @QueryValue String flowId,
|
||||
@Deprecated @Parameter(description = "The start datetime", deprecated = true) @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime startDate,
|
||||
@Deprecated @Parameter(description = "The end datetime", deprecated = true) @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime endDate,
|
||||
@Deprecated @Parameter(description = "A time range filter relative to the current time", deprecated = true, examples = {
|
||||
@ExampleObject(name = "Filter last 5 minutes", value = "PT5M"),
|
||||
@ExampleObject(name = "Filter last 24 hours", value = "P1D")
|
||||
}) @Nullable @QueryValue Duration timeRange,
|
||||
@Deprecated @Parameter(description = "A state filter") @Nullable @QueryValue List<State.Type> state,
|
||||
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'") @Nullable @QueryValue @Format("MULTI") List<String> labels,
|
||||
@Deprecated @Parameter(description = "The trigger execution id") @Nullable @QueryValue String triggerExecutionId,
|
||||
@Deprecated @Parameter(description = "A execution child filter") @Nullable @QueryValue ExecutionRepositoryInterface.ChildFilter childFilter,
|
||||
@Deprecated @Parameter(description = "A state filter", deprecated = true) @Nullable @QueryValue List<State.Type> state,
|
||||
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'", deprecated = true) @Nullable @QueryValue @Format("MULTI") List<String> labels,
|
||||
@Deprecated @Parameter(description = "The trigger execution id", deprecated = true) @Nullable @QueryValue String triggerExecutionId,
|
||||
@Deprecated @Parameter(description = "A execution child filter", deprecated = true) @Nullable @QueryValue ExecutionRepositoryInterface.ChildFilter childFilter,
|
||||
@Parameter(description = "The new state of the executions") @NotNull @QueryValue State.Type newStatus
|
||||
) throws QueueException {
|
||||
filters = RequestUtils.getFiltersOrDefaultToLegacyMapping(
|
||||
@@ -1541,22 +1572,22 @@ public class ExecutionController {
|
||||
@Post(uri = "/resume/by-query")
|
||||
@Operation(tags = {"Executions"}, summary = "Resume executions filter by query parameters")
|
||||
public HttpResponse<?> resumeExecutionsByQuery(
|
||||
@Parameter(description = "Filters") @QueryFilterFormat List<QueryFilter> filters,
|
||||
@Parameter(description = "Filters", in = ParameterIn.QUERY) @QueryFilterFormat List<QueryFilter> filters,
|
||||
|
||||
@Deprecated @Parameter(description = "A string filter") @Nullable @QueryValue(value = "q") String query,
|
||||
@Deprecated @Parameter(description = "The scope of the executions to include") @Nullable @QueryValue(value = "scope") List<FlowScope> scope,
|
||||
@Deprecated @Parameter(description = "A namespace filter prefix") @Nullable @QueryValue String namespace,
|
||||
@Deprecated @Parameter(description = "A flow id filter") @Nullable @QueryValue String flowId,
|
||||
@Deprecated @Parameter(description = "The start datetime") @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime startDate,
|
||||
@Deprecated @Parameter(description = "The end datetime") @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime endDate,
|
||||
@Deprecated @Parameter(description = "A time range filter relative to the current time", examples = {
|
||||
@Deprecated @Parameter(description = "A string filter", deprecated = true) @Nullable @QueryValue(value = "q") String query,
|
||||
@Deprecated @Parameter(description = "The scope of the executions to include", deprecated = true) @Nullable @QueryValue(value = "scope") List<FlowScope> scope,
|
||||
@Deprecated @Parameter(description = "A namespace filter prefix", deprecated = true) @Nullable @QueryValue String namespace,
|
||||
@Deprecated @Parameter(description = "A flow id filter", deprecated = true) @Nullable @QueryValue String flowId,
|
||||
@Deprecated @Parameter(description = "The start datetime", deprecated = true) @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime startDate,
|
||||
@Deprecated @Parameter(description = "The end datetime", deprecated = true) @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime endDate,
|
||||
@Deprecated @Parameter(description = "A time range filter relative to the current time", deprecated = true, examples = {
|
||||
@ExampleObject(name = "Filter last 5 minutes", value = "PT5M"),
|
||||
@ExampleObject(name = "Filter last 24 hours", value = "P1D")
|
||||
}) @Nullable @QueryValue Duration timeRange,
|
||||
@Deprecated @Parameter(description = "A state filter") @Nullable @QueryValue List<State.Type> state,
|
||||
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'") @Nullable @QueryValue @Format("MULTI") List<String> labels,
|
||||
@Deprecated @Parameter(description = "The trigger execution id") @Nullable @QueryValue String triggerExecutionId,
|
||||
@Deprecated @Parameter(description = "A execution child filter") @Nullable @QueryValue ExecutionRepositoryInterface.ChildFilter childFilter
|
||||
@Deprecated @Parameter(description = "A state filter", deprecated = true) @Nullable @QueryValue List<State.Type> state,
|
||||
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'", deprecated = true) @Nullable @QueryValue @Format("MULTI") List<String> labels,
|
||||
@Deprecated @Parameter(description = "The trigger execution id", deprecated = true) @Nullable @QueryValue String triggerExecutionId,
|
||||
@Deprecated @Parameter(description = "A execution child filter", deprecated = true) @Nullable @QueryValue ExecutionRepositoryInterface.ChildFilter childFilter
|
||||
) throws Exception {
|
||||
filters = RequestUtils.getFiltersOrDefaultToLegacyMapping(
|
||||
filters,
|
||||
@@ -1650,22 +1681,22 @@ public class ExecutionController {
|
||||
@Post(uri = "/pause/by-query")
|
||||
@Operation(tags = {"Executions"}, summary = "Pause executions filter by query parameters")
|
||||
public HttpResponse<?> pauseExecutionsByQuery(
|
||||
@Parameter(description = "Filters") @QueryFilterFormat List<QueryFilter> filters,
|
||||
@Parameter(description = "Filters", in = ParameterIn.QUERY) @QueryFilterFormat List<QueryFilter> filters,
|
||||
|
||||
@Deprecated @Parameter(description = "A string filter") @Nullable @QueryValue(value = "q") String query,
|
||||
@Deprecated @Parameter(description = "The scope of the executions to include") @Nullable @QueryValue(value = "scope") List<FlowScope> scope,
|
||||
@Deprecated @Parameter(description = "A namespace filter prefix") @Nullable @QueryValue String namespace,
|
||||
@Deprecated @Parameter(description = "A flow id filter") @Nullable @QueryValue String flowId,
|
||||
@Deprecated @Parameter(description = "The start datetime") @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime startDate,
|
||||
@Deprecated @Parameter(description = "The end datetime") @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime endDate,
|
||||
@Deprecated @Parameter(description = "A time range filter relative to the current time", examples = {
|
||||
@Deprecated @Parameter(description = "A string filter", deprecated = true) @Nullable @QueryValue(value = "q") String query,
|
||||
@Deprecated @Parameter(description = "The scope of the executions to include", deprecated = true) @Nullable @QueryValue(value = "scope") List<FlowScope> scope,
|
||||
@Deprecated @Parameter(description = "A namespace filter prefix", deprecated = true) @Nullable @QueryValue String namespace,
|
||||
@Deprecated @Parameter(description = "A flow id filter", deprecated = true) @Nullable @QueryValue String flowId,
|
||||
@Deprecated @Parameter(description = "The start datetime", deprecated = true) @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime startDate,
|
||||
@Deprecated @Parameter(description = "The end datetime", deprecated = true) @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime endDate,
|
||||
@Deprecated @Parameter(description = "A time range filter relative to the current time", deprecated = true, examples = {
|
||||
@ExampleObject(name = "Filter last 5 minutes", value = "PT5M"),
|
||||
@ExampleObject(name = "Filter last 24 hours", value = "P1D")
|
||||
}) @Nullable @QueryValue Duration timeRange,
|
||||
@Deprecated @Parameter(description = "A state filter") @Nullable @QueryValue List<State.Type> state,
|
||||
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'") @Nullable @QueryValue @Format("MULTI") List<String> labels,
|
||||
@Deprecated @Parameter(description = "The trigger execution id") @Nullable @QueryValue String triggerExecutionId,
|
||||
@Deprecated @Parameter(description = "A execution child filter") @Nullable @QueryValue ExecutionRepositoryInterface.ChildFilter childFilter
|
||||
@Deprecated @Parameter(description = "A state filter", deprecated = true) @Nullable @QueryValue List<State.Type> state,
|
||||
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'", deprecated = true) @Nullable @QueryValue @Format("MULTI") List<String> labels,
|
||||
@Deprecated @Parameter(description = "The trigger execution id", deprecated = true) @Nullable @QueryValue String triggerExecutionId,
|
||||
@Deprecated @Parameter(description = "A execution child filter", deprecated = true) @Nullable @QueryValue ExecutionRepositoryInterface.ChildFilter childFilter
|
||||
) throws Exception {
|
||||
filters = RequestUtils.getFiltersOrDefaultToLegacyMapping(
|
||||
filters,
|
||||
@@ -1694,22 +1725,22 @@ public class ExecutionController {
|
||||
@Delete(uri = "/kill/by-query")
|
||||
@Operation(tags = {"Executions"}, summary = "Kill executions filter by query parameters")
|
||||
public HttpResponse<?> killExecutionsByQuery(
|
||||
@Parameter(description = "Filters") @QueryFilterFormat List<QueryFilter> filters,
|
||||
@Parameter(description = "Filters", in = ParameterIn.QUERY) @QueryFilterFormat List<QueryFilter> filters,
|
||||
|
||||
@Deprecated @Parameter(description = "A string filter") @Nullable @QueryValue(value = "q") String query,
|
||||
@Deprecated @Parameter(description = "The scope of the executions to include") @Nullable @QueryValue(value = "scope") List<FlowScope> scope,
|
||||
@Deprecated @Parameter(description = "A namespace filter prefix") @Nullable @QueryValue String namespace,
|
||||
@Deprecated @Parameter(description = "A flow id filter") @Nullable @QueryValue String flowId,
|
||||
@Deprecated @Parameter(description = "The start datetime") @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime startDate,
|
||||
@Deprecated @Parameter(description = "The end datetime") @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime endDate,
|
||||
@Deprecated @Parameter(description = "A time range filter relative to the current time", examples = {
|
||||
@Deprecated @Parameter(description = "A string filter", deprecated = true) @Nullable @QueryValue(value = "q") String query,
|
||||
@Deprecated @Parameter(description = "The scope of the executions to include", deprecated = true) @Nullable @QueryValue(value = "scope") List<FlowScope> scope,
|
||||
@Deprecated @Parameter(description = "A namespace filter prefix", deprecated = true) @Nullable @QueryValue String namespace,
|
||||
@Deprecated @Parameter(description = "A flow id filter", deprecated = true) @Nullable @QueryValue String flowId,
|
||||
@Deprecated @Parameter(description = "The start datetime", deprecated = true) @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime startDate,
|
||||
@Deprecated @Parameter(description = "The end datetime", deprecated = true) @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime endDate,
|
||||
@Deprecated @Parameter(description = "A time range filter relative to the current time", deprecated = true, examples = {
|
||||
@ExampleObject(name = "Filter last 5 minutes", value = "PT5M"),
|
||||
@ExampleObject(name = "Filter last 24 hours", value = "P1D")
|
||||
}) @Nullable @QueryValue Duration timeRange,
|
||||
@Deprecated @Parameter(description = "A state filter") @Nullable @QueryValue List<State.Type> state,
|
||||
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'") @Nullable @QueryValue @Format("MULTI") List<String> labels,
|
||||
@Deprecated @Parameter(description = "The trigger execution id") @Nullable @QueryValue String triggerExecutionId,
|
||||
@Deprecated @Parameter(description = "A execution child filter") @Nullable @QueryValue ExecutionRepositoryInterface.ChildFilter childFilter
|
||||
@Deprecated @Parameter(description = "A state filter", deprecated = true) @Nullable @QueryValue List<State.Type> state,
|
||||
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'", deprecated = true) @Nullable @QueryValue @Format("MULTI") List<String> labels,
|
||||
@Deprecated @Parameter(description = "The trigger execution id", deprecated = true) @Nullable @QueryValue String triggerExecutionId,
|
||||
@Deprecated @Parameter(description = "A execution child filter", deprecated = true) @Nullable @QueryValue ExecutionRepositoryInterface.ChildFilter childFilter
|
||||
) throws QueueException {
|
||||
filters = RequestUtils.getFiltersOrDefaultToLegacyMapping(
|
||||
filters,
|
||||
@@ -1738,22 +1769,22 @@ public class ExecutionController {
|
||||
@Post(uri = "/replay/by-query")
|
||||
@Operation(tags = {"Executions"}, summary = "Create new executions from old ones filter by query parameters. Keep the flow revision")
|
||||
public HttpResponse<?> replayExecutionsByQuery(
|
||||
@Parameter(description = "Filters") @QueryFilterFormat List<QueryFilter> filters,
|
||||
@Parameter(description = "Filters", in = ParameterIn.QUERY) @QueryFilterFormat List<QueryFilter> filters,
|
||||
|
||||
@Deprecated @Parameter(description = "A string filter") @Nullable @QueryValue(value = "q") String query,
|
||||
@Deprecated @Parameter(description = "The scope of the executions to include") @Nullable @QueryValue(value = "scope") List<FlowScope> scope,
|
||||
@Deprecated @Parameter(description = "A namespace filter prefix") @Nullable @QueryValue String namespace,
|
||||
@Deprecated @Parameter(description = "A flow id filter") @Nullable @QueryValue String flowId,
|
||||
@Deprecated @Parameter(description = "The start datetime") @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime startDate,
|
||||
@Deprecated @Parameter(description = "The end datetime") @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime endDate,
|
||||
@Deprecated @Parameter(description = "A time range filter relative to the current time", examples = {
|
||||
@Deprecated @Parameter(description = "A string filter", deprecated = true) @Nullable @QueryValue(value = "q") String query,
|
||||
@Deprecated @Parameter(description = "The scope of the executions to include", deprecated = true) @Nullable @QueryValue(value = "scope") List<FlowScope> scope,
|
||||
@Deprecated @Parameter(description = "A namespace filter prefix", deprecated = true) @Nullable @QueryValue String namespace,
|
||||
@Deprecated @Parameter(description = "A flow id filter", deprecated = true) @Nullable @QueryValue String flowId,
|
||||
@Deprecated @Parameter(description = "The start datetime", deprecated = true) @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime startDate,
|
||||
@Deprecated @Parameter(description = "The end datetime", deprecated = true) @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime endDate,
|
||||
@Deprecated @Parameter(description = "A time range filter relative to the current time", deprecated = true, examples = {
|
||||
@ExampleObject(name = "Filter last 5 minutes", value = "PT5M"),
|
||||
@ExampleObject(name = "Filter last 24 hours", value = "P1D")
|
||||
}) @Nullable @QueryValue Duration timeRange,
|
||||
@Deprecated @Parameter(description = "A state filter") @Nullable @QueryValue List<State.Type> state,
|
||||
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'") @Nullable @QueryValue @Format("MULTI") List<String> labels,
|
||||
@Deprecated @Parameter(description = "The trigger execution id") @Nullable @QueryValue String triggerExecutionId,
|
||||
@Deprecated @Parameter(description = "A execution child filter") @Nullable @QueryValue ExecutionRepositoryInterface.ChildFilter childFilter,
|
||||
@Deprecated @Parameter(description = "A state filter", deprecated = true) @Nullable @QueryValue List<State.Type> state,
|
||||
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'", deprecated = true) @Nullable @QueryValue @Format("MULTI") List<String> labels,
|
||||
@Deprecated @Parameter(description = "The trigger execution id", deprecated = true) @Nullable @QueryValue String triggerExecutionId,
|
||||
@Deprecated @Parameter(description = "A execution child filter", deprecated = true) @Nullable @QueryValue ExecutionRepositoryInterface.ChildFilter childFilter,
|
||||
|
||||
@Parameter(description = "If latest revision should be used") @Nullable @QueryValue(defaultValue = "false") Boolean latestRevision
|
||||
) throws Exception {
|
||||
@@ -1829,7 +1860,17 @@ public class ExecutionController {
|
||||
|
||||
@ExecuteOn(TaskExecutors.IO)
|
||||
@Get(uri = "/{executionId}/follow", produces = MediaType.TEXT_EVENT_STREAM)
|
||||
@Operation(tags = {"Executions"}, summary = "Follow an execution")
|
||||
@Operation(
|
||||
tags = {"Executions"},
|
||||
summary = "Follow an execution",
|
||||
extensions = @Extension(
|
||||
name = "x-sdk-customization",
|
||||
properties = {
|
||||
@ExtensionProperty(name = "x-replace-follow-execution", value = "true"),
|
||||
@ExtensionProperty(name = "x-skipped", value = "true")
|
||||
}
|
||||
)
|
||||
)
|
||||
public Flux<Event<Execution>> followExecution(
|
||||
@Parameter(description = "The execution id") @PathVariable String executionId
|
||||
) {
|
||||
@@ -2046,22 +2087,22 @@ public class ExecutionController {
|
||||
@Post(uri = "/labels/by-query")
|
||||
@Operation(tags = {"Executions"}, summary = "Set label on executions filter by query parameters")
|
||||
public HttpResponse<?> setLabelsOnTerminatedExecutionsByQuery(
|
||||
@Parameter(description = "Filters") @QueryFilterFormat List<QueryFilter> filters,
|
||||
@Parameter(description = "Filters", in = ParameterIn.QUERY) @QueryFilterFormat List<QueryFilter> filters,
|
||||
|
||||
@Deprecated @Parameter(description = "A string filter") @Nullable @QueryValue(value = "q") String query,
|
||||
@Deprecated @Parameter(description = "The scope of the executions to include") @Nullable @QueryValue(value = "scope") List<FlowScope> scope,
|
||||
@Deprecated @Parameter(description = "A namespace filter prefix") @Nullable @QueryValue String namespace,
|
||||
@Deprecated @Parameter(description = "A flow id filter") @Nullable @QueryValue String flowId,
|
||||
@Deprecated @Parameter(description = "The start datetime") @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime startDate,
|
||||
@Deprecated @Parameter(description = "The end datetime") @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime endDate,
|
||||
@Deprecated @Parameter(description = "A time range filter relative to the current time", examples = {
|
||||
@Deprecated @Parameter(description = "A string filter", deprecated = true) @Nullable @QueryValue(value = "q") String query,
|
||||
@Deprecated @Parameter(description = "The scope of the executions to include", deprecated = true) @Nullable @QueryValue(value = "scope") List<FlowScope> scope,
|
||||
@Deprecated @Parameter(description = "A namespace filter prefix", deprecated = true) @Nullable @QueryValue String namespace,
|
||||
@Deprecated @Parameter(description = "A flow id filter", deprecated = true) @Nullable @QueryValue String flowId,
|
||||
@Deprecated @Parameter(description = "The start datetime", deprecated = true) @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime startDate,
|
||||
@Deprecated @Parameter(description = "The end datetime", deprecated = true) @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime endDate,
|
||||
@Deprecated @Parameter(description = "A time range filter relative to the current time", deprecated = true, examples = {
|
||||
@ExampleObject(name = "Filter last 5 minutes", value = "PT5M"),
|
||||
@ExampleObject(name = "Filter last 24 hours", value = "P1D")
|
||||
}) @Nullable @QueryValue Duration timeRange,
|
||||
@Deprecated @Parameter(description = "A state filter") @Nullable @QueryValue List<State.Type> state,
|
||||
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'") @Nullable @QueryValue @Format("MULTI") List<String> labels,
|
||||
@Deprecated @Parameter(description = "The trigger execution id") @Nullable @QueryValue String triggerExecutionId,
|
||||
@Deprecated @Parameter(description = "A execution child filter") @Nullable @QueryValue ExecutionRepositoryInterface.ChildFilter childFilter,
|
||||
@Deprecated @Parameter(description = "A state filter", deprecated = true) @Nullable @QueryValue List<State.Type> state,
|
||||
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'", deprecated = true) @Nullable @QueryValue @Format("MULTI") List<String> labels,
|
||||
@Deprecated @Parameter(description = "The trigger execution id", deprecated = true) @Nullable @QueryValue String triggerExecutionId,
|
||||
@Deprecated @Parameter(description = "A execution child filter", deprecated = true) @Nullable @QueryValue ExecutionRepositoryInterface.ChildFilter childFilter,
|
||||
|
||||
@RequestBody(description = "The labels to add to the execution") @Body @NotNull @Valid List<Label> setLabels
|
||||
) {
|
||||
@@ -2163,22 +2204,22 @@ public class ExecutionController {
|
||||
@Post(uri = "/unqueue/by-query")
|
||||
@Operation(tags = {"Executions"}, summary = "Unqueue executions filter by query parameters")
|
||||
public HttpResponse<?> unqueueExecutionsByQuery(
|
||||
@Parameter(description = "Filters") @QueryFilterFormat List<QueryFilter> filters,
|
||||
@Parameter(description = "Filters", in = ParameterIn.QUERY) @QueryFilterFormat List<QueryFilter> filters,
|
||||
|
||||
@Deprecated @Parameter(description = "A string filter") @Nullable @QueryValue(value = "q") String query,
|
||||
@Deprecated @Parameter(description = "The scope of the executions to include") @Nullable @QueryValue(value = "scope") List<FlowScope> scope,
|
||||
@Deprecated @Parameter(description = "A namespace filter prefix") @Nullable @QueryValue String namespace,
|
||||
@Deprecated @Parameter(description = "A flow id filter") @Nullable @QueryValue String flowId,
|
||||
@Deprecated @Parameter(description = "The start datetime") @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime startDate,
|
||||
@Deprecated @Parameter(description = "The end datetime") @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime endDate,
|
||||
@Deprecated @Parameter(description = "A time range filter relative to the current time", examples = {
|
||||
@Deprecated @Parameter(description = "A string filter", deprecated = true) @Nullable @QueryValue(value = "q") String query,
|
||||
@Deprecated @Parameter(description = "The scope of the executions to include", deprecated = true) @Nullable @QueryValue(value = "scope") List<FlowScope> scope,
|
||||
@Deprecated @Parameter(description = "A namespace filter prefix", deprecated = true) @Nullable @QueryValue String namespace,
|
||||
@Deprecated @Parameter(description = "A flow id filter", deprecated = true) @Nullable @QueryValue String flowId,
|
||||
@Deprecated @Parameter(description = "The start datetime", deprecated = true) @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime startDate,
|
||||
@Deprecated @Parameter(description = "The end datetime", deprecated = true) @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime endDate,
|
||||
@Deprecated @Parameter(description = "A time range filter relative to the current time", deprecated = true, examples = {
|
||||
@ExampleObject(name = "Filter last 5 minutes", value = "PT5M"),
|
||||
@ExampleObject(name = "Filter last 24 hours", value = "P1D")
|
||||
}) @Nullable @QueryValue Duration timeRange,
|
||||
@Deprecated @Parameter(description = "A state filter") @Nullable @QueryValue List<State.Type> state,
|
||||
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'") @Nullable @QueryValue @Format("MULTI") List<String> labels,
|
||||
@Deprecated @Parameter(description = "The trigger execution id") @Nullable @QueryValue String triggerExecutionId,
|
||||
@Deprecated @Parameter(description = "A execution child filter") @Nullable @QueryValue ExecutionRepositoryInterface.ChildFilter childFilter,
|
||||
@Deprecated @Parameter(description = "A state filter", deprecated = true) @Nullable @QueryValue List<State.Type> state,
|
||||
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'", deprecated = true) @Nullable @QueryValue @Format("MULTI") List<String> labels,
|
||||
@Deprecated @Parameter(description = "The trigger execution id", deprecated = true) @Nullable @QueryValue String triggerExecutionId,
|
||||
@Deprecated @Parameter(description = "A execution child filter", deprecated = true) @Nullable @QueryValue ExecutionRepositoryInterface.ChildFilter childFilter,
|
||||
@Parameter(description = "The new state of the unqueued executions") @Nullable @QueryValue State.Type newState
|
||||
) throws Exception {
|
||||
filters = RequestUtils.getFiltersOrDefaultToLegacyMapping(
|
||||
@@ -2277,22 +2318,22 @@ public class ExecutionController {
|
||||
@Post(uri = "/force-run/by-query")
|
||||
@Operation(tags = {"Executions"}, summary = "Force run executions filter by query parameters")
|
||||
public HttpResponse<?> forceRunExecutionsByQuery(
|
||||
@Parameter(description = "Filters") @QueryFilterFormat List<QueryFilter> filters,
|
||||
@Parameter(description = "Filters", in = ParameterIn.QUERY) @QueryFilterFormat List<QueryFilter> filters,
|
||||
|
||||
@Deprecated @Parameter(description = "A string filter") @Nullable @QueryValue(value = "q") String query,
|
||||
@Deprecated @Parameter(description = "The scope of the executions to include") @Nullable @QueryValue(value = "scope") List<FlowScope> scope,
|
||||
@Deprecated @Parameter(description = "A namespace filter prefix") @Nullable @QueryValue String namespace,
|
||||
@Deprecated @Parameter(description = "A flow id filter") @Nullable @QueryValue String flowId,
|
||||
@Deprecated @Parameter(description = "The start datetime") @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime startDate,
|
||||
@Deprecated @Parameter(description = "The end datetime") @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime endDate,
|
||||
@Deprecated @Parameter(description = "A time range filter relative to the current time", examples = {
|
||||
@Deprecated @Parameter(description = "A string filter", deprecated = true) @Nullable @QueryValue(value = "q") String query,
|
||||
@Deprecated @Parameter(description = "The scope of the executions to include", deprecated = true) @Nullable @QueryValue(value = "scope") List<FlowScope> scope,
|
||||
@Deprecated @Parameter(description = "A namespace filter prefix", deprecated = true) @Nullable @QueryValue String namespace,
|
||||
@Deprecated @Parameter(description = "A flow id filter", deprecated = true) @Nullable @QueryValue String flowId,
|
||||
@Deprecated @Parameter(description = "The start datetime", deprecated = true) @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime startDate,
|
||||
@Deprecated @Parameter(description = "The end datetime", deprecated = true) @Nullable @Format("yyyy-MM-dd'T'HH:mm[:ss][.SSS][XXX]") @QueryValue ZonedDateTime endDate,
|
||||
@Deprecated @Parameter(description = "A time range filter relative to the current time", deprecated = true, examples = {
|
||||
@ExampleObject(name = "Filter last 5 minutes", value = "PT5M"),
|
||||
@ExampleObject(name = "Filter last 24 hours", value = "P1D")
|
||||
}) @Nullable @QueryValue Duration timeRange,
|
||||
@Deprecated @Parameter(description = "A state filter") @Nullable @QueryValue List<State.Type> state,
|
||||
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'") @Nullable @QueryValue @Format("MULTI") List<String> labels,
|
||||
@Deprecated @Parameter(description = "The trigger execution id") @Nullable @QueryValue String triggerExecutionId,
|
||||
@Deprecated @Parameter(description = "A execution child filter") @Nullable @QueryValue ExecutionRepositoryInterface.ChildFilter childFilter
|
||||
@Deprecated @Parameter(description = "A state filter", deprecated = true) @Nullable @QueryValue List<State.Type> state,
|
||||
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'", deprecated = true) @Nullable @QueryValue @Format("MULTI") List<String> labels,
|
||||
@Deprecated @Parameter(description = "The trigger execution id", deprecated = true) @Nullable @QueryValue String triggerExecutionId,
|
||||
@Deprecated @Parameter(description = "A execution child filter", deprecated = true) @Nullable @QueryValue ExecutionRepositoryInterface.ChildFilter childFilter
|
||||
) throws Exception {
|
||||
filters = RequestUtils.getFiltersOrDefaultToLegacyMapping(
|
||||
filters,
|
||||
@@ -2367,7 +2408,17 @@ public class ExecutionController {
|
||||
|
||||
@ExecuteOn(TaskExecutors.IO)
|
||||
@Get(uri = "/{executionId}/follow-dependencies", produces = MediaType.TEXT_EVENT_STREAM)
|
||||
@Operation(tags = {"Executions"}, summary = "Follow all execution dependencies executions")
|
||||
@Operation(
|
||||
tags = {"Executions"},
|
||||
summary = "Follow all execution dependencies executions",
|
||||
extensions = @Extension(
|
||||
name = "x-sdk-customization",
|
||||
properties = {
|
||||
@ExtensionProperty(name = "x-replace-follow-dependencies-execution", value = "true"),
|
||||
@ExtensionProperty(name = "x-skipped", value = "true")
|
||||
}
|
||||
)
|
||||
)
|
||||
public Flux<Event<ExecutionStatusEvent>> followDependenciesExecutions(
|
||||
@Parameter(description = "The execution id") @PathVariable String executionId,
|
||||
@Parameter(description = "If true, list only destination dependencies, otherwise list also source dependencies") @QueryValue(defaultValue = "false") boolean destinationOnly,
|
||||
|
||||
@@ -46,6 +46,7 @@ import io.micronaut.validation.Validated;
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||
@@ -222,7 +223,7 @@ public class FlowController {
|
||||
@Parameter(description = "The current page") @QueryValue(defaultValue = "1") @Min(1) int page,
|
||||
@Parameter(description = "The current page size") @QueryValue(defaultValue = "10") @Min(1) int size,
|
||||
@Parameter(description = "The sort of current page") @Nullable @QueryValue List<String> sort,
|
||||
@Parameter(description = "Filters") @QueryFilterFormat() List<QueryFilter> filters,
|
||||
@Parameter(description = "Filters", in = ParameterIn.QUERY) @QueryFilterFormat() List<QueryFilter> filters,
|
||||
// Deprecated params
|
||||
@Deprecated @Parameter(description = "A string filter", deprecated = true) @Nullable @QueryValue(value = "q") String query,
|
||||
@Deprecated @Parameter(description = "The scope of the flows to include", deprecated = true) @Nullable @QueryValue List<FlowScope> scope,
|
||||
@@ -277,7 +278,7 @@ public class FlowController {
|
||||
*/
|
||||
@ExecuteOn(TaskExecutors.IO)
|
||||
@Post(consumes = MediaType.ALL)
|
||||
@Operation(tags = {"Flows"}, summary = "Create a flow from json object", deprecated = true)
|
||||
@Operation(tags = {"Flows"}, summary = "Create a flow from json object", deprecated = true, hidden = true)
|
||||
@Deprecated(forRemoval = true, since = "0.18")
|
||||
@Hidden // we hide it otherwise this is the one that will be included in the OpenAPI spec instead of the YAML one.
|
||||
public HttpResponse<Flow> createFlowFromJson(
|
||||
@@ -334,7 +335,8 @@ public class FlowController {
|
||||
summary = "Update a complete namespace from json object",
|
||||
description = "All flow will be created / updated for this namespace.\n" +
|
||||
"Flow that already created but not in `flows` will be deleted if the query delete is `true`",
|
||||
deprecated = true
|
||||
deprecated = true,
|
||||
hidden = true
|
||||
)
|
||||
@Deprecated(forRemoval = true, since = "0.18")
|
||||
@Hidden // we hide it otherwise this is the one that will be included in the OpenAPI spec instead of the YAML one.
|
||||
@@ -437,7 +439,7 @@ public class FlowController {
|
||||
|
||||
@Put(uri = "{namespace}/{id}", consumes = MediaType.APPLICATION_YAML)
|
||||
@ExecuteOn(TaskExecutors.IO)
|
||||
@Operation(tags = {"Flows"}, summary = "Update a flow")
|
||||
@Operation(tags = {"Flows"}, summary = "Update a flow")// force deprecated = false otherwise it is marked as deprecated, dont know why
|
||||
@ApiResponse(responseCode = "200", description = "On success", content = {@Content(schema = @Schema(implementation = FlowWithSource.class))})
|
||||
public HttpResponse<FlowWithSource> updateFlow(
|
||||
@Parameter(description = "The flow namespace") @PathVariable String namespace,
|
||||
@@ -476,9 +478,9 @@ public class FlowController {
|
||||
/**
|
||||
* @deprecated use {@link #updateFlow(String, String, String)} instead
|
||||
*/
|
||||
@Put(uri = "{namespace}/{id}", consumes = MediaType.ALL)
|
||||
@Put(uri = "{namespace}/{id}", consumes = MediaType.APPLICATION_JSON)
|
||||
@ExecuteOn(TaskExecutors.IO)
|
||||
@Operation(tags = {"Flows"}, summary = "Update a flow", deprecated = true)
|
||||
@Operation(tags = {"Flows"}, operationId = "updateFlowFromJson", summary = "Update a flow", deprecated = true, hidden = true)
|
||||
@Deprecated(forRemoval = true, since = "0.18")
|
||||
@Hidden // we hide it otherwise this is the one that will be included in the OpenAPI spec instead of the JSON one.
|
||||
public HttpResponse<Flow> updateFlowFromJson(
|
||||
@@ -666,7 +668,7 @@ public class FlowController {
|
||||
@Post(uri = "/validate/task", consumes = MediaType.APPLICATION_YAML)
|
||||
@Operation(tags = {"Flows"}, summary = "Validate a task")
|
||||
public ValidateConstraintViolation validateTask(
|
||||
@RequestBody(description = "A task definition that can be from tasks or triggers") @Body String task,
|
||||
@RequestBody(description = "A task definition that can be from tasks or triggers") @Schema(implementation = Object.class) @Body String task,
|
||||
@Parameter(description = "The type of task") @QueryValue TaskValidationType section
|
||||
) {
|
||||
ValidateConstraintViolation.ValidateConstraintViolationBuilder<?, ?> validateConstraintViolationBuilder = ValidateConstraintViolation.builder();
|
||||
@@ -703,12 +705,12 @@ public class FlowController {
|
||||
summary = "Export flows as a ZIP archive of yaml sources."
|
||||
)
|
||||
public HttpResponse<byte[]> exportFlowsByQuery(
|
||||
@Parameter(description = "Filters") @QueryFilterFormat() List<QueryFilter> filters,
|
||||
@Parameter(description = "Filters", in = ParameterIn.QUERY) @QueryFilterFormat() List<QueryFilter> filters,
|
||||
|
||||
@Deprecated @Parameter(description = "A string filter") @Nullable @QueryValue(value = "q") String query,
|
||||
@Deprecated @Parameter(description = "The scope of the flows to include") @Nullable @QueryValue List<FlowScope> scope,
|
||||
@Deprecated @Parameter(description = "A namespace filter prefix") @Nullable @QueryValue String namespace,
|
||||
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'") @Nullable @QueryValue @Format("MULTI") List<String> labels
|
||||
@Deprecated @Parameter(description = "A string filter", deprecated = true) @Nullable @QueryValue(value = "q") String query,
|
||||
@Deprecated @Parameter(description = "The scope of the flows to include", deprecated = true) @Nullable @QueryValue List<FlowScope> scope,
|
||||
@Deprecated @Parameter(description = "A namespace filter prefix", deprecated = true) @Nullable @QueryValue String namespace,
|
||||
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'", deprecated = true) @Nullable @QueryValue @Format("MULTI") List<String> labels
|
||||
) throws IOException {
|
||||
filters = mapLegacyQueryParamsToNewFilters(filters, query, scope, namespace, labels);
|
||||
|
||||
@@ -741,12 +743,12 @@ public class FlowController {
|
||||
summary = "Delete flows returned by the query parameters."
|
||||
)
|
||||
public HttpResponse<BulkResponse> deleteFlowsByQuery(
|
||||
@Parameter(description = "Filters") @QueryFilterFormat() List<QueryFilter> filters,
|
||||
@Parameter(description = "Filters", in = ParameterIn.QUERY) @QueryFilterFormat List<QueryFilter> filters,
|
||||
|
||||
@Deprecated @Parameter(description = "A string filter") @Nullable @QueryValue(value = "q") String query,
|
||||
@Deprecated @Parameter(description = "The scope of the flows to include") @Nullable @QueryValue List<FlowScope> scope,
|
||||
@Deprecated @Parameter(description = "A namespace filter prefix") @Nullable @QueryValue String namespace,
|
||||
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'") @Nullable @QueryValue @Format("MULTI") List<String> labels
|
||||
@Deprecated @Parameter(description = "A string filter", deprecated = true) @Nullable @QueryValue(value = "q") String query,
|
||||
@Deprecated @Parameter(description = "The scope of the flows to include", deprecated = true) @Nullable @QueryValue List<FlowScope> scope,
|
||||
@Deprecated @Parameter(description = "A namespace filter prefix", deprecated = true) @Nullable @QueryValue String namespace,
|
||||
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'", deprecated = true) @Nullable @QueryValue @Format("MULTI") List<String> labels
|
||||
) {
|
||||
filters = mapLegacyQueryParamsToNewFilters(filters, query, scope, namespace, labels);
|
||||
|
||||
@@ -784,12 +786,12 @@ public class FlowController {
|
||||
summary = "Disable flows returned by the query parameters."
|
||||
)
|
||||
public HttpResponse<BulkResponse> disableFlowsByQuery(
|
||||
@Parameter(description = "Filters") @QueryFilterFormat() List<QueryFilter> filters,
|
||||
@Parameter(description = "Filters", in = ParameterIn.QUERY) @QueryFilterFormat() List<QueryFilter> filters,
|
||||
|
||||
@Deprecated @Parameter(description = "A string filter") @Nullable @QueryValue(value = "q") String query,
|
||||
@Deprecated @Parameter(description = "The scope of the flows to include") @Nullable @QueryValue List<FlowScope> scope,
|
||||
@Deprecated @Parameter(description = "A namespace filter prefix") @Nullable @QueryValue String namespace,
|
||||
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'") @Nullable @QueryValue @Format("MULTI") List<String> labels
|
||||
@Deprecated @Parameter(description = "A string filter", deprecated = true) @Nullable @QueryValue(value = "q") String query,
|
||||
@Deprecated @Parameter(description = "The scope of the flows to include", deprecated = true) @Nullable @QueryValue List<FlowScope> scope,
|
||||
@Deprecated @Parameter(description = "A namespace filter prefix", deprecated = true) @Nullable @QueryValue String namespace,
|
||||
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'", deprecated = true) @Nullable @QueryValue @Format("MULTI") List<String> labels
|
||||
) {
|
||||
filters = mapLegacyQueryParamsToNewFilters(filters, query, scope, namespace, labels);
|
||||
|
||||
@@ -816,12 +818,12 @@ public class FlowController {
|
||||
summary = "Enable flows returned by the query parameters."
|
||||
)
|
||||
public HttpResponse<BulkResponse> enableFlowsByQuery(
|
||||
@Parameter(description = "Filters") @QueryFilterFormat() List<QueryFilter> filters,
|
||||
@Parameter(description = "Filters", in = ParameterIn.QUERY) @QueryFilterFormat() List<QueryFilter> filters,
|
||||
|
||||
@Deprecated @Parameter(description = "A string filter") @Nullable @QueryValue(value = "q") String query,
|
||||
@Deprecated @Parameter(description = "The scope of the flows to include") @Nullable @QueryValue List<FlowScope> scope,
|
||||
@Deprecated @Parameter(description = "A namespace filter prefix") @Nullable @QueryValue String namespace,
|
||||
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'") @Nullable @QueryValue @Format("MULTI") List<String> labels
|
||||
@Deprecated @Parameter(description = "A string filter", deprecated = true) @Nullable @QueryValue(value = "q") String query,
|
||||
@Deprecated @Parameter(description = "The scope of the flows to include", deprecated = true) @Nullable @QueryValue List<FlowScope> scope,
|
||||
@Deprecated @Parameter(description = "A namespace filter prefix", deprecated = true) @Nullable @QueryValue String namespace,
|
||||
@Deprecated @Parameter(description = "A labels filter as a list of 'key:value'", deprecated = true) @Nullable @QueryValue @Format("MULTI") List<String> labels
|
||||
) {
|
||||
filters = mapLegacyQueryParamsToNewFilters(filters, query, scope, namespace, labels);
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ public class KVController {
|
||||
}
|
||||
|
||||
@ExecuteOn(TaskExecutors.IO)
|
||||
@Put(uri = "/namespaces/{namespace}/kv/{key}", consumes = {MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN})
|
||||
@Put(uri = "/namespaces/{namespace}/kv/{key}", consumes = {MediaType.TEXT_PLAIN})
|
||||
@Operation(tags = {"KV"}, summary = "Puts a key-value pair in store")
|
||||
public void setKeyValue(
|
||||
HttpHeaders httpHeaders,
|
||||
|
||||
@@ -27,6 +27,7 @@ import io.micronaut.scheduling.annotation.ExecuteOn;
|
||||
import io.micronaut.validation.Validated;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import org.slf4j.event.Level;
|
||||
@@ -66,7 +67,7 @@ public class LogController {
|
||||
@Parameter(description = "The current page") @QueryValue(defaultValue = "1") @Min(1) int page,
|
||||
@Parameter(description = "The current page size") @QueryValue(defaultValue = "10") @Min(1) int size,
|
||||
@Parameter(description = "The sort of current page") @Nullable @QueryValue List<String> sort,
|
||||
@Parameter(description = "Filters") @Nullable @QueryFilterFormat List<QueryFilter> filters,
|
||||
@Parameter(description = "Filters", in = ParameterIn.QUERY) @Nullable @QueryFilterFormat List<QueryFilter> filters,
|
||||
// Deprecated params
|
||||
@Parameter(description = "A string filter", deprecated = true) @Nullable @QueryValue(value = "q") String query,
|
||||
@Parameter(description = "A namespace filter prefix",deprecated = true) @Nullable @QueryValue String namespace,
|
||||
|
||||
@@ -70,6 +70,9 @@ public class MiscController {
|
||||
@Inject
|
||||
NamespaceUtils namespaceUtils;
|
||||
|
||||
@io.micronaut.context.annotation.Value("${kestra.ui.charts.default-duration:P30D}")
|
||||
private String chartDefaultDuration;
|
||||
|
||||
@io.micronaut.context.annotation.Value("${kestra.anonymous-usage-report.enabled}")
|
||||
protected Boolean isAnonymousUsageEnabled;
|
||||
|
||||
@@ -128,7 +131,9 @@ public class MiscController {
|
||||
.systemNamespace(namespaceUtils.getSystemFlowNamespace())
|
||||
.hiddenLabelsPrefixes(hiddenLabelsPrefixes)
|
||||
.url(kestraUrl)
|
||||
.pluginsHash(pluginRegistry.hash());
|
||||
.pluginsHash(pluginRegistry.hash())
|
||||
.chartDefaultDuration(this.chartDefaultDuration)
|
||||
;
|
||||
|
||||
if (this.environmentName != null || this.environmentColor != null) {
|
||||
builder.environment(
|
||||
@@ -189,6 +194,8 @@ public class MiscController {
|
||||
|
||||
String commitId;
|
||||
|
||||
String chartDefaultDuration;
|
||||
|
||||
ZonedDateTime commitDate;
|
||||
|
||||
@JsonInclude
|
||||
|
||||
@@ -19,6 +19,7 @@ import io.micronaut.scheduling.annotation.ExecuteOn;
|
||||
import io.micronaut.validation.Validated;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||
import jakarta.inject.Inject;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -45,7 +46,7 @@ public class NamespaceSecretController<META extends ApiSecretMeta> {
|
||||
@Parameter(description = "The current page") @QueryValue(value = "page", defaultValue = "1") int page,
|
||||
@Parameter(description = "The current page size") @QueryValue(value = "size", defaultValue = "10") int size,
|
||||
@Parameter(description = "The sort of current page") @Nullable @QueryValue(value = "sort") List<String> sort,
|
||||
@Parameter(description = "Filters") @QueryFilterFormat List<QueryFilter> filters
|
||||
@Parameter(description = "Filters", in = ParameterIn.QUERY) @QueryFilterFormat List<QueryFilter> filters
|
||||
) throws IllegalArgumentException, IOException {
|
||||
final String tenantId = this.tenantService.resolveTenant();
|
||||
List<String> items = secretService.inheritedSecrets(tenantId, namespace).get(namespace).stream().toList();
|
||||
|
||||
@@ -32,6 +32,7 @@ import io.micronaut.scheduling.annotation.ExecuteOn;
|
||||
import io.micronaut.validation.Validated;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.Min;
|
||||
@@ -82,7 +83,7 @@ public class TriggerController {
|
||||
@Parameter(description = "The current page") @QueryValue(defaultValue = "1") @Min(1) int page,
|
||||
@Parameter(description = "The current page size") @QueryValue(defaultValue = "10") @Min(1) int size,
|
||||
@Parameter(description = "The sort of current page") @Nullable @QueryValue List<String> sort,
|
||||
@Parameter(description = "Filters") @QueryFilterFormat List<QueryFilter> filters,
|
||||
@Parameter(description = "Filters", in = ParameterIn.QUERY) @QueryFilterFormat List<QueryFilter> filters,
|
||||
// Deprecated params
|
||||
@Parameter(description = "A string filter",deprecated = true) @Nullable @QueryValue(value = "q") String query,
|
||||
@Parameter(description = "A namespace filter prefix", deprecated = true) @Nullable @QueryValue String namespace,
|
||||
@@ -205,10 +206,10 @@ public class TriggerController {
|
||||
@Post(uri = "/unlock/by-query")
|
||||
@Operation(tags = {"Triggers"}, summary = "Unlock triggers by query parameters")
|
||||
public MutableHttpResponse<?> unlockTriggersByQuery(
|
||||
@Parameter(description = "Filters") @QueryFilterFormat List<QueryFilter> filters,
|
||||
@Parameter(description = "Filters", in = ParameterIn.QUERY) @QueryFilterFormat List<QueryFilter> filters,
|
||||
|
||||
@Deprecated @Parameter(description = "A string filter") @Nullable @QueryValue(value = "q") String query,
|
||||
@Deprecated @Parameter(description = "A namespace filter prefix") @Nullable @QueryValue String namespace
|
||||
@Deprecated @Parameter(description = "A string filter", deprecated = true) @Nullable @QueryValue(value = "q") String query,
|
||||
@Deprecated @Parameter(description = "A namespace filter prefix", deprecated = true) @Nullable @QueryValue String namespace
|
||||
) {
|
||||
filters = RequestUtils.getFiltersOrDefaultToLegacyMapping(
|
||||
filters,
|
||||
@@ -280,13 +281,13 @@ public class TriggerController {
|
||||
if (abstractTrigger == null) {
|
||||
throw new HttpStatusException(HttpStatus.NOT_FOUND, String.format("Flow %s has no trigger %s", newTrigger.getFlowId(), newTrigger.getTriggerId()));
|
||||
}
|
||||
|
||||
|
||||
if (abstractTrigger instanceof RealtimeTriggerInterface) {
|
||||
throw new IllegalArgumentException("Realtime triggers can not be updated through the API, please edit the trigger from the flow.");
|
||||
}
|
||||
|
||||
|
||||
Trigger updatedTrigger;
|
||||
|
||||
|
||||
if (newTrigger.getBackfill() != null) {
|
||||
try {
|
||||
updatedTrigger = setTriggerBackfill(newTrigger, maybeFlow.get(), abstractTrigger);
|
||||
@@ -296,13 +297,13 @@ public class TriggerController {
|
||||
} else {
|
||||
updatedTrigger = setTriggerDisabled(newTrigger.uid(), newTrigger.getDisabled(), abstractTrigger, maybeFlow.get());
|
||||
}
|
||||
|
||||
|
||||
if (updatedTrigger == null) {
|
||||
return HttpResponse.notFound();
|
||||
}
|
||||
return HttpResponse.ok(updatedTrigger);
|
||||
}
|
||||
|
||||
|
||||
@ExecuteOn(TaskExecutors.IO)
|
||||
@Post(uri = "/{namespace}/{flowId}/{triggerId}/restart")
|
||||
@Operation(tags = {"Triggers"}, summary = "Restart a trigger")
|
||||
@@ -369,10 +370,10 @@ public class TriggerController {
|
||||
@Post(uri = "/backfill/pause/by-query")
|
||||
@Operation(tags = {"Triggers"}, summary = "Pause backfill for given triggers")
|
||||
public MutableHttpResponse<?> pauseBackfillByQuery(
|
||||
@Parameter(description = "Filters") @QueryFilterFormat List<QueryFilter> filters,
|
||||
@Parameter(description = "Filters", in = ParameterIn.QUERY) @QueryFilterFormat List<QueryFilter> filters,
|
||||
|
||||
@Deprecated @Parameter(description = "A string filter") @Nullable @QueryValue(value = "q") String query,
|
||||
@Deprecated @Parameter(description = "A namespace filter prefix") @Nullable @QueryValue String namespace
|
||||
@Deprecated @Parameter(description = "A string filter", deprecated = true) @Nullable @QueryValue(value = "q") String query,
|
||||
@Deprecated @Parameter(description = "A namespace filter prefix", deprecated = true) @Nullable @QueryValue String namespace
|
||||
) throws QueueException {
|
||||
// Updating the backfill within the flux does not works
|
||||
List<Trigger> triggers = triggerRepository
|
||||
@@ -408,10 +409,10 @@ public class TriggerController {
|
||||
@Post(uri = "/backfill/unpause/by-query")
|
||||
@Operation(tags = {"Triggers"}, summary = "Unpause backfill for given triggers")
|
||||
public MutableHttpResponse<?> unpauseBackfillByQuery(
|
||||
@Parameter(description = "Filters") @QueryFilterFormat List<QueryFilter> filters,
|
||||
@Parameter(description = "Filters", in = ParameterIn.QUERY) @QueryFilterFormat List<QueryFilter> filters,
|
||||
|
||||
@Deprecated @Parameter(description = "A string filter") @Nullable @QueryValue(value = "q") String query,
|
||||
@Deprecated @Parameter(description = "A namespace filter prefix") @Nullable @QueryValue String namespace
|
||||
@Deprecated @Parameter(description = "A string filter", deprecated = true) @Nullable @QueryValue(value = "q") String query,
|
||||
@Deprecated @Parameter(description = "A namespace filter prefix", deprecated = true) @Nullable @QueryValue String namespace
|
||||
) throws QueueException {
|
||||
filters = RequestUtils.getFiltersOrDefaultToLegacyMapping(
|
||||
filters,
|
||||
@@ -477,10 +478,10 @@ public class TriggerController {
|
||||
@Post(uri = "/backfill/delete/by-query")
|
||||
@Operation(tags = {"Triggers"}, summary = "Delete backfill for given triggers")
|
||||
public MutableHttpResponse<?> deleteBackfillByQuery(
|
||||
@Parameter(description = "Filters") @QueryFilterFormat List<QueryFilter> filters,
|
||||
@Parameter(description = "Filters", in = ParameterIn.QUERY) @QueryFilterFormat List<QueryFilter> filters,
|
||||
|
||||
@Deprecated @Parameter(description = "A string filter") @Nullable @QueryValue(value = "q") String query,
|
||||
@Deprecated @Parameter(description = "A namespace filter prefix") @Nullable @QueryValue String namespace
|
||||
@Deprecated @Parameter(description = "A string filter", deprecated = true) @Nullable @QueryValue(value = "q") String query,
|
||||
@Deprecated @Parameter(description = "A namespace filter prefix", deprecated = true) @Nullable @QueryValue String namespace
|
||||
) throws QueueException {
|
||||
filters = RequestUtils.getFiltersOrDefaultToLegacyMapping(
|
||||
filters,
|
||||
@@ -521,10 +522,10 @@ public class TriggerController {
|
||||
@Post(uri = "/set-disabled/by-query")
|
||||
@Operation(tags = {"Triggers"}, summary = "Disable/enable triggers by query parameters")
|
||||
public MutableHttpResponse<?> disabledTriggersByQuery(
|
||||
@Parameter(description = "Filters") @QueryFilterFormat List<QueryFilter> filters,
|
||||
@Parameter(description = "Filters", in = ParameterIn.QUERY) @QueryFilterFormat List<QueryFilter> filters,
|
||||
|
||||
@Deprecated @Parameter(description = "A string filter") @Nullable @QueryValue(value = "q") String query,
|
||||
@Deprecated @Parameter(description = "A namespace filter prefix") @Nullable @QueryValue String namespace,
|
||||
@Deprecated @Parameter(description = "A string filter", deprecated = true) @Nullable @QueryValue(value = "q") String query,
|
||||
@Deprecated @Parameter(description = "A namespace filter prefix", deprecated = true) @Nullable @QueryValue String namespace,
|
||||
|
||||
@Parameter(description = "The disabled state") @QueryValue(defaultValue = "true") Boolean disabled
|
||||
) throws QueueException {
|
||||
@@ -557,24 +558,24 @@ public class TriggerController {
|
||||
|
||||
public void setTriggerDisabled(Trigger trigger, Boolean disabled) throws QueueException {
|
||||
Optional<Flow> maybeFlow = this.flowRepository.findById(this.tenantService.resolveTenant(), trigger.getNamespace(), trigger.getFlowId());
|
||||
|
||||
|
||||
if (maybeFlow.isEmpty()) {
|
||||
return; // Flow doesn't exist
|
||||
}
|
||||
|
||||
|
||||
Optional<AbstractTrigger> maybeAbstractTrigger = maybeFlow.flatMap(flow -> flow.getTriggers().stream().filter(t -> t.getId().equals(trigger.getTriggerId())).findFirst());
|
||||
|
||||
|
||||
if (maybeAbstractTrigger.isEmpty()) {
|
||||
return; // Trigger doesn't exist
|
||||
}
|
||||
|
||||
|
||||
if (maybeAbstractTrigger.get() instanceof RealtimeTriggerInterface) {
|
||||
return; // RealTimeTriggers can't be disabled/enabled through API.
|
||||
}
|
||||
|
||||
|
||||
setTriggerDisabled(trigger.uid(), disabled, maybeAbstractTrigger.get(), maybeFlow.get());
|
||||
}
|
||||
|
||||
|
||||
private Trigger setTriggerDisabled(String triggerUID, Boolean disabled, AbstractTrigger triggerDefinition, Flow flow) throws QueueException {
|
||||
return this.triggerRepository.lock(triggerUID, throwFunction(current -> {
|
||||
if (disabled.equals(current.getDisabled())) {
|
||||
@@ -583,46 +584,46 @@ public class TriggerController {
|
||||
return doSetTriggerDisabled(current, disabled, flow, triggerDefinition);
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
private Trigger setTriggerBackfill(Trigger newTrigger, Flow flow, AbstractTrigger abstractTrigger) throws Exception {
|
||||
return this.triggerRepository.lock(newTrigger.uid(), throwFunction(current -> doSetTriggerBackfill(current, newTrigger.getBackfill(), flow, abstractTrigger)));
|
||||
}
|
||||
|
||||
|
||||
protected Trigger doSetTriggerDisabled(Trigger currentState, Boolean disabled, Flow flow, AbstractTrigger trigger) throws QueueException {
|
||||
Trigger.TriggerBuilder<?, ?> builder = currentState.toBuilder().disabled(disabled);
|
||||
|
||||
|
||||
if (disabled) {
|
||||
builder = builder.nextExecutionDate(null);
|
||||
}
|
||||
|
||||
|
||||
Trigger updated = builder.build();
|
||||
triggerQueue.emit(updated);
|
||||
return updated;
|
||||
}
|
||||
|
||||
|
||||
protected Trigger doSetTriggerBackfill(Trigger currentState, Backfill backfill, Flow flow, AbstractTrigger trigger) throws Exception {
|
||||
Trigger updated;
|
||||
ZonedDateTime nextExecutionDate = null;
|
||||
|
||||
|
||||
RunContext runContext = runContextFactory.of(flow, trigger);
|
||||
ConditionContext conditionContext = conditionService.conditionContext(runContext, flow, null);
|
||||
|
||||
|
||||
// We must set up the backfill before the update to calculate the next execution date
|
||||
updated = currentState.withBackfill(backfill);
|
||||
|
||||
|
||||
if (trigger instanceof PollingTriggerInterface pollingTriggerInterface) {
|
||||
nextExecutionDate = pollingTriggerInterface.nextEvaluationDate(conditionContext, Optional.of(updated));
|
||||
}
|
||||
|
||||
|
||||
updated = updated
|
||||
.toBuilder()
|
||||
.nextExecutionDate(nextExecutionDate)
|
||||
.build();
|
||||
|
||||
|
||||
triggerQueue.emit(updated);
|
||||
return updated;
|
||||
}
|
||||
|
||||
|
||||
public int backfillsAction(List<Trigger> triggers, BACKFILL_ACTION action) throws QueueException {
|
||||
AtomicInteger count = new AtomicInteger();
|
||||
triggers.forEach(throwConsumer(trigger -> {
|
||||
|
||||
@@ -24,7 +24,7 @@ import io.kestra.core.repositories.FlowRepositoryInterface;
|
||||
import io.kestra.core.runners.FlowInputOutput;
|
||||
import io.kestra.core.runners.InputsTest;
|
||||
import io.kestra.core.runners.LocalPath;
|
||||
import io.kestra.core.runners.RunnerUtils;
|
||||
import io.kestra.core.runners.TestRunnerUtils;
|
||||
import io.kestra.core.serializers.JacksonMapper;
|
||||
import io.kestra.core.storages.StorageContext;
|
||||
import io.kestra.core.storages.StorageInterface;
|
||||
@@ -121,7 +121,7 @@ class ExecutionControllerRunnerTest {
|
||||
private JdbcTestUtils jdbcTestUtils;
|
||||
|
||||
@Inject
|
||||
protected RunnerUtils runnerUtils;
|
||||
protected TestRunnerUtils runnerUtils;
|
||||
|
||||
@Inject
|
||||
private StorageInterface storageInterface;
|
||||
@@ -412,30 +412,30 @@ class ExecutionControllerRunnerTest {
|
||||
assertThat(flow.isPresent()).isTrue();
|
||||
|
||||
// Run child execution starting from a specific task and wait until it finishes
|
||||
Thread.sleep(100);
|
||||
|
||||
Execution createdChidExec = client.toBlocking().retrieve(
|
||||
HttpRequest
|
||||
.POST("/api/v1/main/executions/" + parentExecution.getId() + "/replay?taskRunId=" + parentExecution.findTaskRunByTaskIdAndValue(referenceTaskId, List.of()).getId(), ImmutableMap.of()),
|
||||
Execution.class
|
||||
);
|
||||
|
||||
assertThat(createdChidExec).isNotNull();
|
||||
assertThat(createdChidExec.getParentId()).isEqualTo(parentExecution.getId());
|
||||
assertThat(createdChidExec.getTaskRunList().size()).isEqualTo(4);
|
||||
assertThat(createdChidExec.getState().getCurrent()).isEqualTo(State.Type.RESTARTED);
|
||||
|
||||
IntStream
|
||||
.range(0, 3)
|
||||
.mapToObj(value -> createdChidExec.getTaskRunList().get(value))
|
||||
.forEach(taskRun -> assertThat(taskRun.getState().getCurrent()).isEqualTo(State.Type.SUCCESS));
|
||||
|
||||
assertThat(createdChidExec.getTaskRunList().get(3).getState().getCurrent()).isEqualTo(State.Type.RESTARTED);
|
||||
assertThat(createdChidExec.getTaskRunList().get(3).getAttempts().size()).isEqualTo(1);
|
||||
Execution finishedChildExecution = runnerUtils.awaitChildExecution(
|
||||
flow.get(),
|
||||
parentExecution, throwRunnable(() -> {
|
||||
Thread.sleep(100);
|
||||
|
||||
Execution createdChidExec = client.toBlocking().retrieve(
|
||||
HttpRequest
|
||||
.POST("/api/v1/main/executions/" + parentExecution.getId() + "/replay?taskRunId=" + parentExecution.findTaskRunByTaskIdAndValue(referenceTaskId, List.of()).getId(), ImmutableMap.of()),
|
||||
Execution.class
|
||||
);
|
||||
|
||||
assertThat(createdChidExec).isNotNull();
|
||||
assertThat(createdChidExec.getParentId()).isEqualTo(parentExecution.getId());
|
||||
assertThat(createdChidExec.getTaskRunList().size()).isEqualTo(4);
|
||||
assertThat(createdChidExec.getState().getCurrent()).isEqualTo(State.Type.RESTARTED);
|
||||
|
||||
IntStream
|
||||
.range(0, 3)
|
||||
.mapToObj(value -> createdChidExec.getTaskRunList().get(value))
|
||||
.forEach(taskRun -> assertThat(taskRun.getState().getCurrent()).isEqualTo(State.Type.SUCCESS));
|
||||
|
||||
assertThat(createdChidExec.getTaskRunList().get(3).getState().getCurrent()).isEqualTo(State.Type.RESTARTED);
|
||||
assertThat(createdChidExec.getTaskRunList().get(3).getAttempts().size()).isEqualTo(1);
|
||||
}),
|
||||
parentExecution,
|
||||
createdChidExec,
|
||||
Duration.ofSeconds(15));
|
||||
|
||||
assertThat(finishedChildExecution).isNotNull();
|
||||
@@ -465,26 +465,26 @@ class ExecutionControllerRunnerTest {
|
||||
assertThat(flow.isPresent()).isTrue();
|
||||
|
||||
// Run child execution starting from a specific task and wait until it finishes
|
||||
Thread.sleep(100);
|
||||
|
||||
MultipartBody multipartBody = MultipartBody.builder()
|
||||
.addPart("condition", "success")
|
||||
.build();
|
||||
|
||||
Execution replay = client.toBlocking().retrieve(
|
||||
HttpRequest
|
||||
.POST("/api/v1/main/executions/" + parentExecution.getId() + "/replay-with-inputs", multipartBody)
|
||||
.contentType(MediaType.MULTIPART_FORM_DATA_TYPE),
|
||||
Execution.class
|
||||
);
|
||||
|
||||
assertThat(replay).isNotNull();
|
||||
assertThat(replay.getParentId()).isEqualTo(parentExecution.getId());
|
||||
assertThat(replay.getState().getCurrent()).isEqualTo(Type.CREATED);
|
||||
Execution finishedChildExecution = runnerUtils.awaitChildExecution(
|
||||
flow.get(),
|
||||
parentExecution, throwRunnable(() -> {
|
||||
Thread.sleep(100);
|
||||
|
||||
MultipartBody multipartBody = MultipartBody.builder()
|
||||
.addPart("condition", "success")
|
||||
.build();
|
||||
|
||||
Execution replay = client.toBlocking().retrieve(
|
||||
HttpRequest
|
||||
.POST("/api/v1/main/executions/" + parentExecution.getId() + "/replay-with-inputs", multipartBody)
|
||||
.contentType(MediaType.MULTIPART_FORM_DATA_TYPE),
|
||||
Execution.class
|
||||
);
|
||||
|
||||
assertThat(replay).isNotNull();
|
||||
assertThat(replay.getParentId()).isEqualTo(parentExecution.getId());
|
||||
assertThat(replay.getState().getCurrent()).isEqualTo(Type.CREATED);
|
||||
}),
|
||||
parentExecution,
|
||||
replay,
|
||||
Duration.ofSeconds(15));
|
||||
|
||||
assertThat(finishedChildExecution).isNotNull();
|
||||
@@ -515,27 +515,27 @@ class ExecutionControllerRunnerTest {
|
||||
assertThat(flow.isPresent()).isTrue();
|
||||
|
||||
// Run child execution starting from a specific task and wait until it finishes
|
||||
Thread.sleep(100);
|
||||
|
||||
MultipartBody multipartBody = MultipartBody.builder()
|
||||
.addPart("condition", "success")
|
||||
.build();
|
||||
|
||||
Execution replay = client.toBlocking().retrieve(
|
||||
HttpRequest
|
||||
.POST("/api/v1/main/executions/" + parentExecution.getId() + "/replay-with-inputs?taskRunId=" + parentExecution.findTaskRunByTaskIdAndValue(referenceTaskId, List.of()).getId(), multipartBody)
|
||||
.contentType(MediaType.MULTIPART_FORM_DATA_TYPE),
|
||||
Execution.class
|
||||
);
|
||||
|
||||
assertThat(replay).isNotNull();
|
||||
assertThat(replay.getParentId()).isEqualTo(parentExecution.getId());
|
||||
assertThat(replay.getTaskRunList().size()).isEqualTo(2);
|
||||
assertThat(replay.getState().getCurrent()).isEqualTo(State.Type.RESTARTED);
|
||||
Execution finishedChildExecution = runnerUtils.awaitChildExecution(
|
||||
flow.get(),
|
||||
parentExecution, throwRunnable(() -> {
|
||||
Thread.sleep(100);
|
||||
|
||||
MultipartBody multipartBody = MultipartBody.builder()
|
||||
.addPart("condition", "success")
|
||||
.build();
|
||||
|
||||
Execution replay = client.toBlocking().retrieve(
|
||||
HttpRequest
|
||||
.POST("/api/v1/main/executions/" + parentExecution.getId() + "/replay-with-inputs?taskRunId=" + parentExecution.findTaskRunByTaskIdAndValue(referenceTaskId, List.of()).getId(), multipartBody)
|
||||
.contentType(MediaType.MULTIPART_FORM_DATA_TYPE),
|
||||
Execution.class
|
||||
);
|
||||
|
||||
assertThat(replay).isNotNull();
|
||||
assertThat(replay.getParentId()).isEqualTo(parentExecution.getId());
|
||||
assertThat(replay.getTaskRunList().size()).isEqualTo(2);
|
||||
assertThat(replay.getState().getCurrent()).isEqualTo(State.Type.RESTARTED);
|
||||
}),
|
||||
parentExecution,
|
||||
replay,
|
||||
Duration.ofSeconds(15));
|
||||
|
||||
assertThat(finishedChildExecution).isNotNull();
|
||||
@@ -563,23 +563,23 @@ class ExecutionControllerRunnerTest {
|
||||
assertThat(flow.isPresent()).isTrue();
|
||||
|
||||
// Run child execution starting from a specific task and wait until it finishes
|
||||
Thread.sleep(100);
|
||||
|
||||
Execution createdChidExec = client.toBlocking().retrieve(
|
||||
HttpRequest
|
||||
.POST("/api/v1/main/executions/" + parentExecution.getId() + "/replay?taskRunId=" + parentExecution.findTaskRunByTaskIdAndValue(referenceTaskId, List.of()).getId(), ImmutableMap.of()),
|
||||
Execution.class
|
||||
);
|
||||
|
||||
assertThat(createdChidExec.getState().getCurrent()).isEqualTo(State.Type.RESTARTED);
|
||||
assertThat(createdChidExec.getState().getHistories()).hasSize(4);
|
||||
assertThat(createdChidExec.getTaskRunList()).hasSize(20);
|
||||
|
||||
assertThat(createdChidExec.getId()).isNotEqualTo(parentExecution.getId());
|
||||
runnerUtils.awaitChildExecution(
|
||||
flow.get(),
|
||||
parentExecution, throwRunnable(() -> {
|
||||
Thread.sleep(100);
|
||||
|
||||
Execution createdChidExec = client.toBlocking().retrieve(
|
||||
HttpRequest
|
||||
.POST("/api/v1/main/executions/" + parentExecution.getId() + "/replay?taskRunId=" + parentExecution.findTaskRunByTaskIdAndValue(referenceTaskId, List.of()).getId(), ImmutableMap.of()),
|
||||
Execution.class
|
||||
);
|
||||
|
||||
assertThat(createdChidExec.getState().getCurrent()).isEqualTo(State.Type.RESTARTED);
|
||||
assertThat(createdChidExec.getState().getHistories()).hasSize(4);
|
||||
assertThat(createdChidExec.getTaskRunList()).hasSize(20);
|
||||
|
||||
assertThat(createdChidExec.getId()).isNotEqualTo(parentExecution.getId());
|
||||
}),
|
||||
parentExecution,
|
||||
createdChidExec,
|
||||
Duration.ofSeconds(30));
|
||||
}
|
||||
|
||||
@@ -599,33 +599,31 @@ class ExecutionControllerRunnerTest {
|
||||
assertThat(flow.isPresent()).isTrue();
|
||||
|
||||
// Restart execution and wait until it finishes
|
||||
Execution restartedExec = client.toBlocking().retrieve(
|
||||
HttpRequest
|
||||
.POST("/api/v1/main/executions/" + firstExecution.getId() + "/restart", ImmutableMap.of()),
|
||||
Execution.class
|
||||
);
|
||||
|
||||
assertThat(restartedExec).isNotNull();
|
||||
assertThat(restartedExec.getId()).isEqualTo(firstExecution.getId());
|
||||
assertThat(restartedExec.getParentId()).isNull();
|
||||
assertThat(restartedExec.getTaskRunList().size()).isEqualTo(3);
|
||||
assertThat(restartedExec.getState().getCurrent()).isEqualTo(State.Type.RESTARTED);
|
||||
|
||||
IntStream
|
||||
.range(0, 2)
|
||||
.mapToObj(value -> restartedExec.getTaskRunList().get(value)).forEach(taskRun -> {
|
||||
assertThat(taskRun.getState().getCurrent()).isEqualTo(State.Type.SUCCESS);
|
||||
assertThat(taskRun.getAttempts().size()).isEqualTo(1);
|
||||
|
||||
assertThat(restartedExec.getTaskRunList().get(2).getState().getCurrent()).isEqualTo(State.Type.RESTARTED);
|
||||
assertThat(restartedExec.getTaskRunList().get(2).getAttempts().size()).isEqualTo(1);
|
||||
});
|
||||
|
||||
Execution finishedRestartedExecution = runnerUtils.awaitExecution(
|
||||
execution -> execution.getId().equals(firstExecution.getId()) &&
|
||||
execution.getTaskRunList().size() == 4 &&
|
||||
execution.getState().isTerminated(),
|
||||
() -> {
|
||||
Execution restartedExec = client.toBlocking().retrieve(
|
||||
HttpRequest
|
||||
.POST("/api/v1/main/executions/" + firstExecution.getId() + "/restart", ImmutableMap.of()),
|
||||
Execution.class
|
||||
);
|
||||
|
||||
assertThat(restartedExec).isNotNull();
|
||||
assertThat(restartedExec.getId()).isEqualTo(firstExecution.getId());
|
||||
assertThat(restartedExec.getParentId()).isNull();
|
||||
assertThat(restartedExec.getTaskRunList().size()).isEqualTo(3);
|
||||
assertThat(restartedExec.getState().getCurrent()).isEqualTo(State.Type.RESTARTED);
|
||||
|
||||
IntStream
|
||||
.range(0, 2)
|
||||
.mapToObj(value -> restartedExec.getTaskRunList().get(value)).forEach(taskRun -> {
|
||||
assertThat(taskRun.getState().getCurrent()).isEqualTo(State.Type.SUCCESS);
|
||||
assertThat(taskRun.getAttempts().size()).isEqualTo(1);
|
||||
|
||||
assertThat(restartedExec.getTaskRunList().get(2).getState().getCurrent()).isEqualTo(State.Type.RESTARTED);
|
||||
assertThat(restartedExec.getTaskRunList().get(2).getAttempts().size()).isEqualTo(1);
|
||||
});
|
||||
},
|
||||
execution -> execution.getTaskRunList().size() == 4 && execution.getState().isTerminated(),
|
||||
restartedExec,
|
||||
Duration.ofSeconds(15)
|
||||
);
|
||||
|
||||
@@ -662,35 +660,33 @@ class ExecutionControllerRunnerTest {
|
||||
assertThat(flow.isPresent()).isTrue();
|
||||
|
||||
// Restart execution and wait until it finishes
|
||||
Execution restartedExec = client.toBlocking().retrieve(
|
||||
HttpRequest
|
||||
.POST("/api/v1/main/executions/" + firstExecution.getId() + "/restart", ImmutableMap.of()),
|
||||
Execution.class
|
||||
);
|
||||
|
||||
assertThat(restartedExec).isNotNull();
|
||||
assertThat(restartedExec.getId()).isEqualTo(firstExecution.getId());
|
||||
assertThat(restartedExec.getParentId()).isNull();
|
||||
assertThat(restartedExec.getTaskRunList().size()).isEqualTo(4);
|
||||
assertThat(restartedExec.getState().getCurrent()).isEqualTo(State.Type.RESTARTED);
|
||||
|
||||
IntStream
|
||||
.range(0, 2)
|
||||
.mapToObj(value -> restartedExec.getTaskRunList().get(value)).forEach(taskRun -> {
|
||||
assertThat(taskRun.getState().getCurrent()).isEqualTo(State.Type.SUCCESS);
|
||||
assertThat(taskRun.getAttempts().size()).isEqualTo(1);
|
||||
|
||||
assertThat(restartedExec.getTaskRunList().get(2).getState().getCurrent()).isEqualTo(State.Type.RUNNING);
|
||||
assertThat(restartedExec.getTaskRunList().get(3).getState().getCurrent()).isEqualTo(State.Type.RESTARTED);
|
||||
assertThat(restartedExec.getTaskRunList().get(2).getAttempts()).isNotNull();
|
||||
assertThat(restartedExec.getTaskRunList().get(3).getAttempts().size()).isEqualTo(1);
|
||||
});
|
||||
|
||||
Execution finishedRestartedExecution = runnerUtils.awaitExecution(
|
||||
execution -> execution.getId().equals(firstExecution.getId()) &&
|
||||
execution.getTaskRunList().size() == 5 &&
|
||||
execution.getState().isTerminated(),
|
||||
() -> {
|
||||
Execution restartedExec = client.toBlocking().retrieve(
|
||||
HttpRequest
|
||||
.POST("/api/v1/main/executions/" + firstExecution.getId() + "/restart", ImmutableMap.of()),
|
||||
Execution.class
|
||||
);
|
||||
|
||||
assertThat(restartedExec).isNotNull();
|
||||
assertThat(restartedExec.getId()).isEqualTo(firstExecution.getId());
|
||||
assertThat(restartedExec.getParentId()).isNull();
|
||||
assertThat(restartedExec.getTaskRunList().size()).isEqualTo(4);
|
||||
assertThat(restartedExec.getState().getCurrent()).isEqualTo(State.Type.RESTARTED);
|
||||
|
||||
IntStream
|
||||
.range(0, 2)
|
||||
.mapToObj(value -> restartedExec.getTaskRunList().get(value)).forEach(taskRun -> {
|
||||
assertThat(taskRun.getState().getCurrent()).isEqualTo(State.Type.SUCCESS);
|
||||
assertThat(taskRun.getAttempts().size()).isEqualTo(1);
|
||||
|
||||
assertThat(restartedExec.getTaskRunList().get(2).getState().getCurrent()).isEqualTo(State.Type.RUNNING);
|
||||
assertThat(restartedExec.getTaskRunList().get(3).getState().getCurrent()).isEqualTo(State.Type.RESTARTED);
|
||||
assertThat(restartedExec.getTaskRunList().get(2).getAttempts()).isNotNull();
|
||||
assertThat(restartedExec.getTaskRunList().get(3).getAttempts().size()).isEqualTo(1);
|
||||
});
|
||||
},
|
||||
execution -> execution.getTaskRunList().size() == 5 && execution.getState().isTerminated(),
|
||||
restartedExec,
|
||||
Duration.ofSeconds(15)
|
||||
);
|
||||
|
||||
@@ -1748,7 +1744,7 @@ class ExecutionControllerRunnerTest {
|
||||
void shouldUnqueueExecutionAQueuedFlow() throws QueueException, TimeoutException {
|
||||
// run a first flow so the second is queued
|
||||
runnerUtils.runOneUntilRunning(TENANT_ID, TESTS_FLOW_NS, "flow-concurrency-queue");
|
||||
Execution result = runUntilQueued(TESTS_FLOW_NS, "flow-concurrency-queue");
|
||||
Execution result = runnerUtils.runOneUntil(TENANT_ID, TESTS_FLOW_NS, "flow-concurrency-queue", exec -> exec.getState().isQueued());
|
||||
|
||||
var response = client.toBlocking().exchange(HttpRequest.POST("/api/v1/main/executions/" + result.getId() + "/unqueue", null));
|
||||
assertThat(response.getStatus().getCode()).isEqualTo(HttpStatus.OK.getCode());
|
||||
@@ -1756,7 +1752,7 @@ class ExecutionControllerRunnerTest {
|
||||
// waiting for the flow to complete successfully
|
||||
runnerUtils.awaitExecution(
|
||||
execution -> execution.getId().equals(result.getId()) && execution.getState().isSuccess(),
|
||||
() -> {},
|
||||
result,
|
||||
Duration.ofSeconds(10)
|
||||
);
|
||||
|
||||
@@ -1777,7 +1773,7 @@ class ExecutionControllerRunnerTest {
|
||||
void shouldUnqueueAQueuedFlowToCancelledState() throws QueueException, TimeoutException {
|
||||
// run a first flow so the second is queued
|
||||
runnerUtils.runOneUntilRunning(TENANT_ID, "io.kestra.tests", "flow-concurrency-queue");
|
||||
Execution result1 = runUntilQueued("io.kestra.tests", "flow-concurrency-queue");
|
||||
Execution result1 = runnerUtils.runOneUntil(TENANT_ID, TESTS_FLOW_NS, "flow-concurrency-queue", exec -> exec.getState().isQueued());
|
||||
|
||||
var cancelResponse = client.toBlocking().exchange(
|
||||
HttpRequest.POST("/api/v1/executions/" + result1.getId() + "/unqueue?state=CANCELLED", null)
|
||||
@@ -1794,9 +1790,9 @@ class ExecutionControllerRunnerTest {
|
||||
void shouldUnqueueExecutionByIdsQueuedFlows() throws TimeoutException, QueueException {
|
||||
// run a first flow so the others are queued
|
||||
runnerUtils.runOneUntilRunning(TENANT_ID, TESTS_FLOW_NS, "flow-concurrency-queue");
|
||||
Execution result1 = runUntilQueued(TESTS_FLOW_NS, "flow-concurrency-queue");
|
||||
Execution result2 = runUntilQueued(TESTS_FLOW_NS, "flow-concurrency-queue");
|
||||
Execution result3 = runUntilQueued(TESTS_FLOW_NS, "flow-concurrency-queue");
|
||||
Execution result1 = runnerUtils.runOneUntil(TENANT_ID, TESTS_FLOW_NS, "flow-concurrency-queue", exec -> exec.getState().isQueued());
|
||||
Execution result2 = runnerUtils.runOneUntil(TENANT_ID, TESTS_FLOW_NS, "flow-concurrency-queue", exec -> exec.getState().isQueued());
|
||||
Execution result3 = runnerUtils.runOneUntil(TENANT_ID, TESTS_FLOW_NS, "flow-concurrency-queue", exec -> exec.getState().isQueued());
|
||||
|
||||
BulkResponse response = client.toBlocking().retrieve(
|
||||
HttpRequest.POST("/api/v1/main/executions/unqueue/by-ids", List.of(result1.getId(), result2.getId(), result3.getId())),
|
||||
@@ -1810,7 +1806,7 @@ class ExecutionControllerRunnerTest {
|
||||
void shouldForceRunExecutionAQueuedFlow() throws QueueException, TimeoutException {
|
||||
// run a first flow so the second is queued
|
||||
runnerUtils.runOneUntilRunning(TENANT_ID, TESTS_FLOW_NS, "flow-concurrency-queue");
|
||||
Execution result = runUntilQueued(TESTS_FLOW_NS, "flow-concurrency-queue");
|
||||
Execution result = runnerUtils.runOneUntil(TENANT_ID, TESTS_FLOW_NS, "flow-concurrency-queue", exec -> exec.getState().isQueued());
|
||||
|
||||
var response = client.toBlocking().exchange(HttpRequest.POST("/api/v1/main/executions/" + result.getId() + "/force-run", null));
|
||||
assertThat(response.getStatus().getCode()).isEqualTo(HttpStatus.OK.getCode());
|
||||
@@ -1821,7 +1817,7 @@ class ExecutionControllerRunnerTest {
|
||||
// waiting for the flow to complete successfully
|
||||
runnerUtils.awaitExecution(
|
||||
execution -> execution.getId().equals(result.getId()) && execution.getState().isSuccess(),
|
||||
() -> {},
|
||||
result,
|
||||
Duration.ofSeconds(10)
|
||||
);
|
||||
}
|
||||
@@ -2021,22 +2017,11 @@ class ExecutionControllerRunnerTest {
|
||||
.build();
|
||||
}
|
||||
|
||||
private Execution runUntilQueued(String namespace, String flowId) throws TimeoutException, QueueException {
|
||||
return runUntilState(namespace, flowId, State.Type.QUEUED);
|
||||
}
|
||||
|
||||
private Execution createExecution(String namespace, String flowId) {
|
||||
Flow flow = flowRepositoryInterface.findById(TENANT_ID, namespace, flowId).orElseThrow();
|
||||
return Execution.newExecution(flow, null);
|
||||
}
|
||||
|
||||
private Execution runUntilState(String namespace, String flowId, State.Type state) throws TimeoutException, QueueException {
|
||||
Execution execution = this.createExecution(namespace, flowId);
|
||||
return runnerUtils.awaitExecution(
|
||||
it -> execution.getId().equals(it.getId()) && it.getState().getCurrent() == state,
|
||||
throwRunnable(() -> this.executionQueue.emit(execution)),
|
||||
Duration.ofSeconds(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
@LoadFlows({"flows/valids/minimal.yaml"})
|
||||
@@ -2113,7 +2098,7 @@ class ExecutionControllerRunnerTest {
|
||||
// wait for the exec to be terminated
|
||||
Execution terminated = runnerUtils.awaitExecution(
|
||||
it -> execution.getId().equals(it.getId()) && it.getState().isTerminated(),
|
||||
() -> {},
|
||||
execution,
|
||||
Duration.ofSeconds(10));
|
||||
assertThat(terminated.getState().getCurrent()).isEqualTo(State.Type.SUCCESS);
|
||||
assertThat(terminated.getTaskRunList()).hasSize(1);
|
||||
|
||||
@@ -531,7 +531,7 @@ class FlowControllerTest {
|
||||
List<String> namespaces = client.toBlocking().retrieve(
|
||||
HttpRequest.GET("/api/v1/main/flows/distinct-namespaces"), Argument.listOf(String.class));
|
||||
|
||||
assertThat(namespaces.size()).isEqualTo(10);
|
||||
assertThat(namespaces.size()).isEqualTo(11);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
package io.kestra.webserver.controllers.api;
|
||||
|
||||
import static io.kestra.core.tenant.TenantService.MAIN_TENANT;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.BDDAssertions.within;
|
||||
|
||||
import io.kestra.core.exceptions.ResourceExpiredException;
|
||||
import io.kestra.core.junit.annotations.KestraTest;
|
||||
import io.kestra.core.models.kv.KVType;
|
||||
@@ -26,6 +22,14 @@ import io.micronaut.http.client.exceptions.HttpClientResponseException;
|
||||
import io.micronaut.reactor.http.client.ReactorHttpClient;
|
||||
import jakarta.inject.Inject;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.net.URI;
|
||||
@@ -39,12 +43,9 @@ import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import static io.kestra.core.tenant.TenantService.MAIN_TENANT;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.BDDAssertions.within;
|
||||
|
||||
@KestraTest(resolveParameters = false)
|
||||
class KVControllerTest {
|
||||
@@ -201,16 +202,16 @@ class KVControllerTest {
|
||||
|
||||
static Stream<Arguments> kvSetKeyValueArgs() {
|
||||
return Stream.of(
|
||||
Arguments.of(MediaType.APPLICATION_JSON, "{\"hello\":\"world\"}", Map.class),
|
||||
Arguments.of(MediaType.APPLICATION_JSON, "[\"hello\",\"world\"]", List.class),
|
||||
Arguments.of(MediaType.APPLICATION_JSON, "\"hello\"", String.class),
|
||||
Arguments.of(MediaType.APPLICATION_JSON, "1", Integer.class),
|
||||
Arguments.of(MediaType.APPLICATION_JSON, "1.0", BigDecimal.class),
|
||||
Arguments.of(MediaType.APPLICATION_JSON, "true", Boolean.class),
|
||||
Arguments.of(MediaType.APPLICATION_JSON, "false", Boolean.class),
|
||||
Arguments.of(MediaType.APPLICATION_JSON, "2021-09-01", LocalDate.class),
|
||||
Arguments.of(MediaType.APPLICATION_JSON, "2021-09-01T01:02:03Z", Instant.class),
|
||||
Arguments.of(MediaType.APPLICATION_JSON, "\"PT5S\"", Duration.class)
|
||||
Arguments.of(MediaType.TEXT_PLAIN, "{\"hello\":\"world\"}", Map.class),
|
||||
Arguments.of(MediaType.TEXT_PLAIN, "[\"hello\",\"world\"]", List.class),
|
||||
Arguments.of(MediaType.TEXT_PLAIN, "\"hello\"", String.class),
|
||||
Arguments.of(MediaType.TEXT_PLAIN, "1", Integer.class),
|
||||
Arguments.of(MediaType.TEXT_PLAIN, "1.0", BigDecimal.class),
|
||||
Arguments.of(MediaType.TEXT_PLAIN, "true", Boolean.class),
|
||||
Arguments.of(MediaType.TEXT_PLAIN, "false", Boolean.class),
|
||||
Arguments.of(MediaType.TEXT_PLAIN, "2021-09-01", LocalDate.class),
|
||||
Arguments.of(MediaType.TEXT_PLAIN, "2021-09-01T01:02:03Z", Instant.class),
|
||||
Arguments.of(MediaType.TEXT_PLAIN, "\"PT5S\"", Duration.class)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -293,7 +294,7 @@ class KVControllerTest {
|
||||
assertThat(httpClientResponseException.getStatus().getCode()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY.getCode());
|
||||
assertThat(httpClientResponseException.getMessage()).isEqualTo(expectedErrorMessage);
|
||||
|
||||
httpClientResponseException = Assertions.assertThrows(HttpClientResponseException.class, () -> client.toBlocking().exchange(HttpRequest.PUT("/api/v1/main/namespaces/" + NAMESPACE + "/kv/bad$key", "\"content\"").contentType(MediaType.APPLICATION_JSON)));
|
||||
httpClientResponseException = Assertions.assertThrows(HttpClientResponseException.class, () -> client.toBlocking().exchange(HttpRequest.PUT("/api/v1/main/namespaces/" + NAMESPACE + "/kv/bad$key", "\"content\"").contentType(MediaType.TEXT_PLAIN)));
|
||||
assertThat(httpClientResponseException.getStatus().getCode()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY.getCode());
|
||||
assertThat(httpClientResponseException.getMessage()).isEqualTo(expectedErrorMessage);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user