Compare commits

..

2 Commits

Author SHA1 Message Date
Nicolas K.
35aef41622 Merge branch 'develop' into feat/use_tenant_on_execution_controller_test 2025-12-19 17:28:44 +01:00
nKwiatkowski
15443d80df feat: use tenants to remove flaky in execution controllers tests 2025-12-19 17:27:43 +01:00
63 changed files with 2262 additions and 2887 deletions

View File

@@ -63,9 +63,9 @@ You can also build it from a terminal using `./gradlew build`, the Gradle wrappe
- Configure the following environment variables:
- `MICRONAUT_ENVIRONMENTS`: can be set to any string and will load a custom configuration file in `cli/src/main/resources/application-{env}.yml`.
- `KESTRA_PLUGINS_PATH`: is the path where you will save plugins as Jar and will be load on startup.
- See the screenshot below for an example: ![Intellij IDEA Configuration ](./assets/run-app.png)
- See the screenshot below for an example: ![Intellij IDEA Configuration ](run-app.png)
- If you encounter **JavaScript memory heap out** error during startup, configure `NODE_OPTIONS` environment variable with some large value.
- Example `NODE_OPTIONS: --max-old-space-size=4096` or `NODE_OPTIONS: --max-old-space-size=8192` ![Intellij IDEA Configuration ](./assets/node_option_env_var.png)
- Example `NODE_OPTIONS: --max-old-space-size=4096` or `NODE_OPTIONS: --max-old-space-size=8192` ![Intellij IDEA Configuration ](node_option_env_var.png)
- The server starts by default on port 8080 and is reachable on `http://localhost:8080`
If you want to launch all tests, you need Python and some packages installed on your machine, on Ubuntu you can install them with:

View File

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 130 KiB

View File

@@ -12,7 +12,7 @@ _Example: Replaces legacy scroll directive with the new API._
### 🔗 Related Issue
Which issue does this PR resolve? Use [GitHub Keywords](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests#linking-a-pull-request-to-an-issue) to automatically link the pull request to the issue.
_Example: Closes https://github.com/kestra-io/kestra/issues/ISSUE_NUMBER._
_Example: Closes https://github.com/kestra-io/kestra/issues/12345._
### 🎨 Frontend Checklist

View File

Before

Width:  |  Height:  |  Size: 210 KiB

After

Width:  |  Height:  |  Size: 210 KiB

View File

@@ -171,22 +171,13 @@ allprojects {
subprojects {subProj ->
if (subProj.name != 'platform' && subProj.name != 'jmh-benchmarks') {
apply plugin: "com.adarshr.test-logger"
apply plugin: 'jacoco'
java {
sourceCompatibility = targetJavaVersion
targetCompatibility = targetJavaVersion
}
configurations {
agent {
canBeResolved = true
canBeConsumed = true
}
}
dependencies {
// Platform
testAnnotationProcessor enforcedPlatform(project(":platform"))
@@ -213,17 +204,9 @@ subprojects {subProj ->
//assertj
testImplementation 'org.assertj:assertj-core'
agent "org.aspectj:aspectjweaver:1.9.25.1"
testImplementation platform("io.qameta.allure:allure-bom")
testImplementation "io.qameta.allure:allure-junit5"
}
def commonTestConfig = { Test t ->
t.ignoreFailures = true
t.finalizedBy jacocoTestReport
// set Xmx for test workers
t.maxHeapSize = '4g'
@@ -249,52 +232,6 @@ subprojects {subProj ->
// }
}
tasks.register('integrationTest', Test) { Test t ->
description = 'Runs integration tests'
group = 'verification'
useJUnitPlatform {
includeTags 'integration'
}
testClassesDirs = sourceSets.test.output.classesDirs
classpath = sourceSets.test.runtimeClasspath
reports {
junitXml.required = true
junitXml.outputPerTestCase = true
junitXml.mergeReruns = true
junitXml.includeSystemErrLog = true
junitXml.outputLocation = layout.buildDirectory.dir("test-results/test")
}
// Integration tests typically not parallel (but you can enable)
maxParallelForks = 1
commonTestConfig(t)
}
tasks.register('unitTest', Test) { Test t ->
description = 'Runs unit tests'
group = 'verification'
useJUnitPlatform {
excludeTags 'flaky', 'integration'
}
testClassesDirs = sourceSets.test.output.classesDirs
classpath = sourceSets.test.runtimeClasspath
reports {
junitXml.required = true
junitXml.outputPerTestCase = true
junitXml.mergeReruns = true
junitXml.includeSystemErrLog = true
junitXml.outputLocation = layout.buildDirectory.dir("test-results/test")
}
commonTestConfig(t)
}
tasks.register('flakyTest', Test) { Test t ->
group = 'verification'
description = 'Runs tests tagged @Flaky but does not fail the build.'
@@ -302,6 +239,7 @@ subprojects {subProj ->
useJUnitPlatform {
includeTags 'flaky'
}
ignoreFailures = true
reports {
junitXml.required = true
@@ -311,13 +249,10 @@ subprojects {subProj ->
junitXml.outputLocation = layout.buildDirectory.dir("test-results/flakyTest")
}
commonTestConfig(t)
}
// test task (default)
tasks.named('test', Test) { Test t ->
group = 'verification'
description = 'Runs all non-flaky tests.'
test {
useJUnitPlatform {
excludeTags 'flaky'
}
@@ -328,12 +263,10 @@ subprojects {subProj ->
junitXml.includeSystemErrLog = true
junitXml.outputLocation = layout.buildDirectory.dir("test-results/test")
}
commonTestConfig(t)
jvmArgs = ["-javaagent:${configurations.agent.singleFile}"]
}
commonTestConfig(it)
tasks.named('check') {
dependsOn(tasks.named('test'))// default behaviour
finalizedBy(tasks.named('flakyTest'))
}
testlogger {
@@ -349,25 +282,83 @@ subprojects {subProj ->
}
}
/**********************************************************************************************************************\
* End-to-End Tests
**********************************************************************************************************************/
def e2eTestsCheck = tasks.register('e2eTestsCheck') {
group = 'verification'
description = "Runs the 'check' task for all e2e-tests modules"
doFirst {
project.ext.set("e2e-tests", true)
}
}
subprojects {
// Add e2e-tests modules check tasks to e2eTestsCheck
if (project.name.startsWith("e2e-tests")) {
test {
onlyIf {
project.hasProperty("e2e-tests")
}
}
}
afterEvaluate {
// Add e2e-tests modules check tasks to e2eTestsCheck
if (project.name.startsWith("e2e-tests")) {
e2eTestsCheck.configure {
finalizedBy(check)
}
}
}
}
/**********************************************************************************************************************\
* Allure Reports
**********************************************************************************************************************/
subprojects {
if (it.name != 'platform' && it.name != 'jmh-benchmarks') {
dependencies {
testImplementation platform("io.qameta.allure:allure-bom")
testImplementation "io.qameta.allure:allure-junit5"
}
configurations {
agent {
canBeResolved = true
canBeConsumed = true
}
}
dependencies {
agent "org.aspectj:aspectjweaver:1.9.25.1"
}
test {
jvmArgs = ["-javaagent:${configurations.agent.singleFile}"]
}
}
}
/**********************************************************************************************************************\
* Jacoco
**********************************************************************************************************************/
subprojects {
if (it.name != 'platform' && it.name != 'jmh-benchmarks') {
apply plugin: 'jacoco'
test {
finalizedBy jacocoTestReport
}
jacocoTestReport {
dependsOn test
}
}
}
tasks.named('check') {
dependsOn tasks.named('testCodeCoverageReport', JacocoReport)
finalizedBy jacocoTestReport
}
tasks.register('unitTest') {
// No jacocoTestReport here, because it depends by default on :test,
// and that would make :test being run twice in our CI.
// In practice the report will be generated later in the CI by :check.
}
tasks.register('integrationTest') {
dependsOn tasks.named('testCodeCoverageReport', JacocoReport)
finalizedBy jacocoTestReport
}
tasks.register('flakyTest') {
dependsOn tasks.named('testCodeCoverageReport', JacocoReport)
finalizedBy jacocoTestReport
}
tasks.named('testCodeCoverageReport') {

View File

@@ -4,6 +4,7 @@ import io.micronaut.configuration.picocli.PicocliRunner;
import io.micronaut.context.ApplicationContext;
import io.micronaut.context.env.Environment;
import org.apache.commons.io.FileUtils;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import java.io.File;
@@ -14,6 +15,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
@@ -23,8 +25,7 @@ class PluginDocCommandTest {
@Test
void run() throws IOException, URISyntaxException {
var testDirectoryName = PluginListCommandTest.class.getSimpleName();
Path pluginsPath = Files.createTempDirectory(testDirectoryName + "_pluginsPath_");
Path pluginsPath = Files.createTempDirectory(PluginListCommandTest.class.getSimpleName());
pluginsPath.toFile().deleteOnExit();
FileUtils.copyFile(
@@ -33,7 +34,7 @@ class PluginDocCommandTest {
new File(URI.create("file://" + pluginsPath.toAbsolutePath() + "/" + PLUGIN_TEMPLATE_TEST))
);
Path docPath = Files.createTempDirectory(testDirectoryName + "_docPath_");
Path docPath = Files.createTempDirectory(PluginInstallCommandTest.class.getSimpleName());
docPath.toFile().deleteOnExit();
try (ApplicationContext ctx = ApplicationContext.run(Environment.CLI, Environment.TEST)) {
@@ -42,9 +43,9 @@ class PluginDocCommandTest {
List<Path> files = Files.list(docPath).toList();
assertThat(files.stream().map(path -> path.getFileName().toString())).contains("plugin-template-test");
// don't know why, but sometimes there is an addition "plugin-notifications" directory present
var directory = files.stream().filter(path -> "plugin-template-test".equals(path.getFileName().toString())).findFirst().get().toFile();
assertThat(files.size()).isEqualTo(1);
assertThat(files.getFirst().getFileName().toString()).isEqualTo("plugin-template-test");
var directory = files.getFirst().toFile();
assertThat(directory.isDirectory()).isTrue();
assertThat(directory.listFiles().length).isEqualTo(3);

View File

@@ -42,12 +42,13 @@ import io.kestra.core.plugins.PluginRegistry;
import io.kestra.core.plugins.RegisteredPlugin;
import io.kestra.core.serializers.JacksonMapper;
import io.micronaut.core.annotation.Nullable;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.*;
import java.time.*;
@@ -298,9 +299,7 @@ public class JsonSchemaGenerator {
}
// default value
builder.forFields()
.withIgnoreCheck(fieldScope -> fieldScope.getAnnotation(Hidden.class) != null)
.withDefaultResolver(this::defaults);
builder.forFields().withDefaultResolver(this::defaults);
// def name
builder.forTypesInGeneral()
@@ -810,9 +809,9 @@ public class JsonSchemaGenerator {
// we don't return base properties unless specified with @PluginProperty and hidden is false
builder
.forFields()
.withIgnoreCheck(fieldScope -> (base != null &&
.withIgnoreCheck(fieldScope -> base != null &&
(fieldScope.getAnnotation(PluginProperty.class) == null || fieldScope.getAnnotation(PluginProperty.class).hidden()) &&
fieldScope.getDeclaringType().getTypeName().equals(base.getName())) || fieldScope.getAnnotation(Hidden.class) != null
fieldScope.getDeclaringType().getTypeName().equals(base.getName())
);
SchemaGeneratorConfig schemaGeneratorConfig = builder.build();

View File

@@ -151,12 +151,6 @@ public record QueryFilter(
return List.of(Op.EQUALS, Op.NOT_EQUALS, Op.CONTAINS, Op.STARTS_WITH, Op.ENDS_WITH, Op.IN, Op.NOT_IN);
}
},
TRIGGER_STATE("triggerState"){
@Override
public List<Op> supportedOp() {
return List.of(Op.EQUALS, Op.NOT_EQUALS);
}
},
EXECUTION_ID("executionId") {
@Override
public List<Op> supportedOp() {
@@ -277,7 +271,7 @@ public record QueryFilter(
@Override
public List<Field> supportedField() {
return List.of(Field.QUERY, Field.SCOPE, Field.NAMESPACE, Field.WORKER_ID, Field.FLOW_ID,
Field.START_DATE, Field.END_DATE, Field.TRIGGER_ID, Field.TRIGGER_STATE
Field.START_DATE, Field.END_DATE, Field.TRIGGER_ID
);
}
},

View File

@@ -1,25 +0,0 @@
package io.kestra.core.plugins.notifications;
import io.kestra.core.models.property.Property;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.Map;
public interface ExecutionInterface {
@Schema(
title = "The execution id to use",
description = "Default is the current execution, " +
"change it to {{ trigger.executionId }} if you use this task with a Flow trigger to use the original execution."
)
Property<String> getExecutionId();
@Schema(
title = "Custom fields to be added on notification"
)
Property<Map<String, Object>> getCustomFields();
@Schema(
title = "Custom message to be added on notification"
)
Property<String> getCustomMessage();
}

View File

@@ -1,140 +0,0 @@
package io.kestra.core.plugins.notifications;
import io.kestra.core.exceptions.IllegalVariableEvaluationException;
import io.kestra.core.models.executions.Execution;
import io.kestra.core.models.executions.TaskRun;
import io.kestra.core.models.flows.State;
import io.kestra.core.models.property.Property;
import io.kestra.core.models.tasks.retrys.Exponential;
import io.kestra.core.repositories.ExecutionRepositoryInterface;
import io.kestra.core.runners.DefaultRunContext;
import io.kestra.core.runners.RunContext;
import io.kestra.core.serializers.JacksonMapper;
import io.kestra.core.utils.ListUtils;
import io.kestra.core.utils.RetryUtils;
import io.kestra.core.utils.UriProvider;
import java.time.Duration;
import java.util.*;
public final class ExecutionService {
private ExecutionService() {}
public static Execution findExecution(RunContext runContext, Property<String> executionId) throws IllegalVariableEvaluationException, NoSuchElementException {
ExecutionRepositoryInterface executionRepository = ((DefaultRunContext) runContext).getApplicationContext().getBean(ExecutionRepositoryInterface.class);
RetryUtils.Instance<Execution, NoSuchElementException> retryInstance = RetryUtils
.of(Exponential.builder()
.delayFactor(2.0)
.interval(Duration.ofSeconds(1))
.maxInterval(Duration.ofSeconds(15))
.maxAttempts(-1)
.maxDuration(Duration.ofMinutes(10))
.build(),
runContext.logger()
);
var executionRendererId = runContext.render(executionId).as(String.class).orElse(null);
var flowTriggerExecutionState = getOptionalFlowTriggerExecutionState(runContext);
var flowVars = (Map<String, String>) runContext.getVariables().get("flow");
var isCurrentExecution = isCurrentExecution(runContext, executionRendererId);
if (isCurrentExecution) {
runContext.logger().info("Loading execution data for the current execution.");
}
return retryInstance.run(
NoSuchElementException.class,
() -> executionRepository.findById(flowVars.get("tenantId"), executionRendererId)
.filter(foundExecution -> isExecutionInTheWantedState(foundExecution, isCurrentExecution, flowTriggerExecutionState))
.orElseThrow(() -> new NoSuchElementException("Unable to find execution '" + executionRendererId + "'"))
);
}
/**
* ExecutionRepository can be out of sync in ElasticSearch stack, with this filter we try to mitigate that
*
* @param execution the Execution we fetched from ExecutionRepository
* @param isCurrentExecution true if this *Execution Task is configured to send a notification for the current Execution
* @param flowTriggerExecutionState the Execution State that triggered the Flow trigger, if any
* @return true if we think we fetched the right Execution data for our usecase
*/
public static boolean isExecutionInTheWantedState(Execution execution, boolean isCurrentExecution, Optional<String> flowTriggerExecutionState) {
if (isCurrentExecution) {
// we don't wait for current execution to be terminated as it could not be possible as long as this task is running
return true;
}
if (flowTriggerExecutionState.isPresent()) {
// we were triggered by a Flow trigger that can be, for example: PAUSED
if (flowTriggerExecutionState.get().equals(State.Type.RUNNING.toString())) {
// RUNNING special case: we take the first state we got
return true;
} else {
// to handle the case where the ExecutionRepository is out of sync in ElasticSearch stack,
// we try to match an Execution with the same state
return execution.getState().getCurrent().name().equals(flowTriggerExecutionState.get());
}
} else {
return execution.getState().getCurrent().isTerminated();
}
}
public static Map<String, Object> executionMap(RunContext runContext, ExecutionInterface executionInterface) throws IllegalVariableEvaluationException {
Execution execution = findExecution(runContext, executionInterface.getExecutionId());
UriProvider uriProvider = ((DefaultRunContext) runContext).getApplicationContext().getBean(UriProvider.class);
Map<String, Object> templateRenderMap = new HashMap<>();
templateRenderMap.put("duration", execution.getState().humanDuration());
templateRenderMap.put("startDate", execution.getState().getStartDate());
templateRenderMap.put("link", uriProvider.executionUrl(execution));
templateRenderMap.put("execution", JacksonMapper.toMap(execution));
runContext.render(executionInterface.getCustomMessage())
.as(String.class)
.ifPresent(s -> templateRenderMap.put("customMessage", s));
final Map<String, Object> renderedCustomFields = runContext.render(executionInterface.getCustomFields()).asMap(String.class, Object.class);
if (!renderedCustomFields.isEmpty()) {
templateRenderMap.put("customFields", renderedCustomFields);
}
var isCurrentExecution = isCurrentExecution(runContext, execution.getId());
List<TaskRun> taskRuns;
if (isCurrentExecution) {
taskRuns = execution.getTaskRunList();
} else {
taskRuns = execution.getTaskRunList().stream()
.filter(t -> (execution.hasFailed() ? State.Type.FAILED : State.Type.SUCCESS).equals(t.getState().getCurrent()))
.toList();
}
if (!ListUtils.isEmpty(taskRuns)) {
TaskRun lastTaskRun = taskRuns.getLast();
templateRenderMap.put("firstFailed", State.Type.FAILED.equals(lastTaskRun.getState().getCurrent()) ? lastTaskRun : false);
templateRenderMap.put("lastTask", lastTaskRun);
}
return templateRenderMap;
}
/**
* if there is a state, we assume this is a Flow trigger with type: {@link io.kestra.plugin.core.trigger.Flow.Output}
*
* @return the state of the execution that triggered the Flow trigger, or empty if another usecase/trigger
*/
private static Optional<String> getOptionalFlowTriggerExecutionState(RunContext runContext) {
var triggerVar = Optional.ofNullable(
runContext.getVariables().get("trigger")
);
return triggerVar.map(trigger -> ((Map<String, String>) trigger).get("state"));
}
private static boolean isCurrentExecution(RunContext runContext, String executionId) {
var executionVars = (Map<String, String>) runContext.getVariables().get("execution");
return executionId.equals(executionVars.get("id"));
}
}

View File

@@ -12,7 +12,6 @@ import io.kestra.core.models.dashboards.charts.DataChart;
import io.kestra.core.plugins.DefaultPluginRegistry;
import io.kestra.core.plugins.PluginRegistry;
import io.kestra.core.serializers.JacksonMapper;
import io.micronaut.context.exceptions.NoSuchBeanException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -73,7 +72,7 @@ public final class PluginDeserializer<T extends Plugin> extends JsonDeserializer
// By default, if no plugin-registry is configured retrieve
// the one configured from the static Kestra's context.
pluginRegistry = KestraContext.getContext().getPluginRegistry();
} catch (IllegalStateException | NoSuchBeanException ignore) {
} catch (IllegalStateException ignore) {
// This error can only happen if the KestraContext is not initialized (i.e. in unit tests).
log.error("No plugin registry was initialized. Use default implementation.");
pluginRegistry = DefaultPluginRegistry.getOrCreate();

View File

@@ -1,3 +0,0 @@
ALTER TABLE triggers
ADD COLUMN "disabled" BOOL
GENERATED ALWAYS AS (JQ_BOOLEAN("value", '.disabled')) NOT NULL;

View File

@@ -1,3 +0,0 @@
ALTER TABLE triggers
ADD COLUMN `disabled` BOOL
GENERATED ALWAYS AS (value ->> '$.disabled' = 'true') STORED NOT NULL

View File

@@ -1,4 +0,0 @@
ALTER TABLE triggers
ADD COLUMN "disabled" BOOL
GENERATED ALWAYS AS (CAST(value ->> 'disabled' AS BOOL)) STORED NOT NULL;

View File

@@ -324,10 +324,6 @@ public abstract class AbstractJdbcRepository {
}
}
if(field == QueryFilter.Field.TRIGGER_STATE){
return applyTriggerStateCondition(value, operation);
}
// Convert the field name to lowercase and quote it
Name columnName = getColumnName(field);
@@ -345,7 +341,7 @@ public abstract class AbstractJdbcRepository {
case CONTAINS -> DSL.field(columnName).like("%" + value + "%");
case REGEX -> DSL.field(columnName).likeRegex((String) value);
case PREFIX -> DSL.field(columnName).like(value + "%")
.or(DSL.field(columnName).eq(value));
.or(DSL.field(columnName).eq(value));
default -> throw new InvalidQueryFiltersException("Unsupported operation: " + operation);
};
}
@@ -473,23 +469,6 @@ public abstract class AbstractJdbcRepository {
};
}
private Condition applyTriggerStateCondition(Object value, QueryFilter.Op operation) {
String triggerState = value.toString();
Boolean isDisabled = switch (triggerState) {
case "disabled" -> true;
case "enabled" -> false;
default -> null;
};
if (isDisabled == null) {
return DSL.noCondition();
}
return switch (operation) {
case EQUALS -> field("disabled").eq(isDisabled);
case NOT_EQUALS -> field("disabled").ne(isDisabled);
default -> throw new InvalidQueryFiltersException("Unsupported operation for Trigger State: " + operation);
};
}
protected Field<Date> formatDateField(String dateField, DateUtils.GroupType groupType) {
throw new UnsupportedOperationException("formatDateField() not implemented");
}

View File

@@ -7,12 +7,10 @@ import io.micronaut.context.annotation.Factory;
import io.micronaut.context.annotation.Requires;
import io.micronaut.test.annotation.TransactionMode;
import io.micronaut.test.condition.TestActiveCondition;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.extension.ExtendWith;
import java.lang.annotation.*;
@Tag("integration")
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE, ElementType.TYPE})
@ExtendWith(KestraTestExtension.class)

View File

@@ -8,31 +8,25 @@ import io.micronaut.test.extensions.junit5.MicronautJunit5Extension;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.platform.commons.support.AnnotationSupport;
import java.util.Set;
public class KestraTestExtension extends MicronautJunit5Extension {
@Override
protected MicronautTestValue buildMicronautTestValue(Class<?> testClass) {
testProperties.put("kestra.jdbc.executor.thread-count", Runtime.getRuntime().availableProcessors() * 4);
return AnnotationSupport
.findAnnotation(testClass, KestraTest.class)
.map(kestraTestAnnotation -> {
var envsSet = new java.util.HashSet<>(Set.of(kestraTestAnnotation.environments()));
envsSet.add("test");// add test env if not already present
return new MicronautTestValue(
kestraTestAnnotation.application(),
envsSet.toArray(new String[0]),
kestraTestAnnotation.packages(),
kestraTestAnnotation.propertySources(),
kestraTestAnnotation.rollback(),
kestraTestAnnotation.transactional(),
kestraTestAnnotation.rebuildContext(),
kestraTestAnnotation.contextBuilder(),
kestraTestAnnotation.transactionMode(),
kestraTestAnnotation.startApplication(),
kestraTestAnnotation.resolveParameters()
);
})
.map(kestraTestAnnotation -> new MicronautTestValue(
kestraTestAnnotation.application(),
kestraTestAnnotation.environments(),
kestraTestAnnotation.packages(),
kestraTestAnnotation.propertySources(),
kestraTestAnnotation.rollback(),
kestraTestAnnotation.transactional(),
kestraTestAnnotation.rebuildContext(),
kestraTestAnnotation.contextBuilder(),
kestraTestAnnotation.transactionMode(),
kestraTestAnnotation.startApplication(),
kestraTestAnnotation.resolveParameters()
))
.orElse(null);
}

3114
ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -27,7 +27,7 @@
"@kestra-io/ui-libs": "^0.0.268",
"@vue-flow/background": "^1.3.2",
"@vue-flow/controls": "^1.1.2",
"@vue-flow/core": "^1.48.1",
"@vue-flow/core": "^1.48.0",
"@vueuse/core": "^14.1.0",
"ansi-to-html": "^0.7.2",
"axios": "^1.13.2",
@@ -39,7 +39,7 @@
"cytoscape": "^3.33.0",
"dagre": "^0.8.5",
"dotenv": "^17.2.3",
"element-plus": "2.13.0",
"element-plus": "2.12.0",
"humanize-duration": "^3.33.2",
"js-yaml": "^4.1.1",
"lodash": "^4.17.21",
@@ -59,15 +59,15 @@
"path-browserify": "^1.0.1",
"pdfjs-dist": "^5.4.449",
"pinia": "^3.0.4",
"posthog-js": "^1.310.1",
"posthog-js": "^1.308.0",
"rapidoc": "^9.3.8",
"semver": "^7.7.3",
"shiki": "^3.20.0",
"vue": "^3.5.26",
"vue": "^3.5.25",
"vue-axios": "^3.5.2",
"vue-chartjs": "^5.3.3",
"vue-gtag": "^3.6.3",
"vue-i18n": "^11.2.7",
"vue-i18n": "^11.2.2",
"vue-material-design-icons": "^5.3.1",
"vue-router": "^4.6.4",
"vue-sidebar-menu": "^5.9.1",
@@ -97,9 +97,9 @@
"@types/semver": "^7.7.1",
"@types/testing-library__jest-dom": "^6.0.0",
"@types/testing-library__user-event": "^4.2.0",
"@typescript-eslint/parser": "^8.50.1",
"@typescript-eslint/parser": "^8.50.0",
"@vitejs/plugin-vue": "^6.0.3",
"@vitejs/plugin-vue-jsx": "^5.1.3",
"@vitejs/plugin-vue-jsx": "^5.1.2",
"@vitest/browser": "^3.2.4",
"@vitest/coverage-v8": "^3.2.4",
"@vue/eslint-config-prettier": "^10.2.0",
@@ -120,29 +120,29 @@
"playwright": "^1.55.0",
"prettier": "^3.7.4",
"rimraf": "^6.1.2",
"rolldown-vite": "^7.3.0",
"rolldown-vite": "^7.2.11",
"rollup-plugin-copy": "^3.5.0",
"sass": "^1.97.1",
"storybook": "^9.1.17",
"sass": "^1.97.0",
"storybook": "^9.1.16",
"storybook-vue3-router": "^6.0.2",
"ts-node": "^10.9.2",
"typescript": "^5.9.3",
"typescript-eslint": "^8.50.1",
"typescript-eslint": "^8.50.0",
"uuid": "^13.0.0",
"vite": "npm:rolldown-vite@latest",
"vitest": "^3.2.4",
"vue-tsc": "^3.2.1"
"vue-tsc": "^3.1.8"
},
"optionalDependencies": {
"@esbuild/darwin-arm64": "^0.27.2",
"@esbuild/darwin-x64": "^0.27.2",
"@esbuild/linux-x64": "^0.27.2",
"@rollup/rollup-darwin-arm64": "^4.54.0",
"@rollup/rollup-darwin-x64": "^4.54.0",
"@rollup/rollup-linux-x64-gnu": "^4.54.0",
"@swc/core-darwin-arm64": "^1.15.7",
"@swc/core-darwin-x64": "^1.15.7",
"@swc/core-linux-x64-gnu": "^1.15.7"
"@rollup/rollup-darwin-arm64": "^4.53.5",
"@rollup/rollup-darwin-x64": "^4.53.5",
"@rollup/rollup-linux-x64-gnu": "^4.53.5",
"@swc/core-darwin-arm64": "^1.15.5",
"@swc/core-darwin-x64": "^1.15.5",
"@swc/core-linux-x64-gnu": "^1.15.5"
},
"overrides": {
"bootstrap": {

View File

@@ -43,7 +43,7 @@
REF_PATH_INJECTION_KEY,
ROOT_SCHEMA_INJECTION_KEY,
SCHEMA_DEFINITIONS_INJECTION_KEY,
UPDATE_YAML_FUNCTION_INJECTION_KEY
UPDATE_TASK_FUNCTION_INJECTION_KEY
} from "../../no-code/injectionKeys";
import {NoCodeProps} from "../../flows/noCodeTypes";
import {deepEqual} from "../../../utils/utils";
@@ -68,7 +68,7 @@
dashboardStore.sourceCode = YAML_UTILS.stringify(app);
}
provide(UPDATE_YAML_FUNCTION_INJECTION_KEY, (yaml) => {
provide(UPDATE_TASK_FUNCTION_INJECTION_KEY, (yaml) => {
editorUpdate(yaml)
})

View File

@@ -128,9 +128,9 @@ $placeholder-font-size: 12px;
font-size: $input-font-size;
background-color: transparent;
width: 100%;
padding-inline: 0.5em;
padding-block: 0.7em;
border: none;
display: flex;
align-items: center;
&:focus {
outline: none;
@@ -139,7 +139,6 @@ $placeholder-font-size: 12px;
&::placeholder {
color: $placeholder-color;
font-size: $placeholder-font-size;
line-height: 1;
}
&:not(:placeholder-shown)~.reset {

View File

@@ -103,10 +103,6 @@ export function useValues(label: string | undefined, t?: ReturnType<typeof useI1
STATUSES: buildFromArray(["PENDING", "ACCEPTED", "EXPIRED"]),
AGGREGATIONS: buildFromArray(["SUM", "AVG", "MIN", "MAX"]),
RELATIVE_DATE,
TRIGGER_STATES:[
{label: t("filter.triggerState.enabled"), value: "enabled"},
{label: t("filter.triggerState.disabled"), value: "disabled"}
]
};
return {VALUES, getRelativeDateLabel};

View File

@@ -41,9 +41,9 @@ export const useTriggerFilter = (): ComputedRef<FilterConfiguration> => {
return [...current, `${(previousCombination ? previousCombination + "." : "")}${part}`];
}, []);
}))].map(namespace => ({
label: namespace,
value: namespace
}));
label: namespace,
value: namespace
}));
}
return [];
},
@@ -116,22 +116,8 @@ export const useTriggerFilter = (): ComputedRef<FilterConfiguration> => {
],
valueType: "text",
searchable: true,
},
{
key: "triggerState",
label: t("filter.triggerState.label"),
description: t("filter.triggerState.description"),
comparators: [
Comparators.EQUALS,
Comparators.NOT_EQUALS
],
valueType: "select",
valueProvider: async () => {
const {VALUES} = useValues("triggers");
return VALUES.TRIGGER_STATES;
}
}
]
};
});
};
};

View File

@@ -13,16 +13,18 @@
@on-edit="(event) => emit('on-edit', event, true)"
/>
<el-alert v-else type="warning" :closable="false">
{{ $t("unable to generate graph") }}
{{ t("unable to generate graph") }}
</el-alert>
</div>
</el-card>
</template>
<script setup lang="ts">
import {onBeforeUnmount} from "vue";
import {useI18n} from "vue-i18n";
import {useFlowStore} from "../../stores/flow";
import LowCodeEditor from "../inputs/LowCodeEditor.vue";
const {t} = useI18n();
defineProps<{
isReadOnly?: boolean;
expandedSubflows?: any[];

View File

@@ -1,13 +1,16 @@
<template>
<el-tooltip placement="bottom" :content="$t('playground.tooltip_persistence')">
<el-switch v-model="playgroundStore.enabled" :activeText="$t('playground.toggle')" class="toggle" :class="{'is-active': playgroundStore.enabled}" />
<el-tooltip placement="bottom" :content="t('playground.tooltip_persistence')">
<el-switch v-model="playgroundStore.enabled" :activeText="t('playground.toggle')" class="toggle" :class="{'is-active': playgroundStore.enabled}" />
</el-tooltip>
</template>
<script setup lang="ts">
import {useI18n} from "vue-i18n";
import {usePlaygroundStore} from "../../stores/playground";
const {t} = useI18n();
const playgroundStore = usePlaygroundStore();
</script>
<style scoped lang="scss">

View File

@@ -6,7 +6,7 @@
<Keyboard />
</el-icon>
<span class="fs-6">
{{ $t("editor_shortcuts.label") }}
{{ t("editor_shortcuts.label") }}
</span>
</div>
</template>
@@ -27,7 +27,7 @@
</template>
</div>
<div class="text-break">
{{ $t(command.description) }}
{{ t(command.description) }}
</div>
</div>
</div>
@@ -35,9 +35,11 @@
</template>
<script setup lang="ts">
import {useI18n} from "vue-i18n";
import Keyboard from "vue-material-design-icons/Keyboard.vue";
import {useKeyShortcuts} from "../../utils/useKeyShortcuts";
const {t} = useI18n();
const {isKeyShortcutsDialogShown} = useKeyShortcuts();
const commands = [

View File

@@ -97,6 +97,7 @@
<script setup lang="ts">
import {
computed,
getCurrentInstance,
h,
inject,
onBeforeUnmount,
@@ -125,7 +126,7 @@
import uniqBy from "lodash/uniqBy";
import {useI18n} from "vue-i18n";
import {ElDatePicker} from "element-plus";
import moment, {Moment} from "moment";
import {Moment} from "moment";
import PlaceholderContentWidget from "../../composables/monaco/PlaceholderContentWidget";
import Utils from "../../utils/utils";
import {hashCode} from "../../utils/global";
@@ -136,6 +137,7 @@
import EditorType = editor.EditorType;
import {useRoute} from "vue-router";
const currentInstance = getCurrentInstance()!;
const {t} = useI18n();
const textAreaValue = computed({
@@ -369,7 +371,8 @@
}
}, {deep: true});
const nowMoment: Moment = moment().startOf("day");
const nowMoment: Moment = currentInstance.appContext.config.globalProperties.$moment().startOf("day");
function addedSuggestRows(mutations: MutationRecord[]) {
return mutations.flatMap(({addedNodes}) => {
const nodes = [...addedNodes];
@@ -458,7 +461,7 @@
endColumn: wordAtPosition?.endColumn ?? position?.column
},
// We don't use the selectedDate directly because if user modifies the input value directly it doesn't work otherwise
text: `${moment(
text: `${currentInstance.appContext.config.globalProperties.$moment(
datePicker.value!.$el.nextElementSibling.querySelector("input").value
).toISOString(true)} `,
forceMoveMarkers: true

View File

@@ -4,7 +4,7 @@
id="side-menu"
:menu
@update:collapsed="onToggleCollapse"
width="280px"
width="268px"
:collapsed="collapsed"
linkComponentName="LeftMenuLink"
hideToggle

View File

@@ -1,11 +1,11 @@
<template>
<nav class="d-flex align-items-center w-100 gap-3 top-bar">
<SidebarToggleButton
v-if="layoutStore.sideMenuCollapsed"
@toggle="layoutStore.setSideMenuCollapsed(false)"
/>
<div class="d-flex flex-column flex-grow-1 flex-shrink-1 overflow-hidden top-title">
<div class="d-flex align-items-end gap-2">
<SidebarToggleButton
v-if="layoutStore.sideMenuCollapsed"
@toggle="layoutStore.setSideMenuCollapsed(false)"
/>
<div class="d-flex flex-column gap-2">
<el-breadcrumb v-if="breadcrumb">
<el-breadcrumb-item v-for="(item, x) in breadcrumb" :key="x" :class="{'pe-none': item.disabled}">

View File

@@ -43,9 +43,7 @@
BLOCK_SCHEMA_PATH_INJECTION_KEY,
CLOSE_TASK_FUNCTION_INJECTION_KEY,
CREATE_TASK_FUNCTION_INJECTION_KEY,
CREATING_FLOW_INJECTION_KEY,
CREATING_TASK_INJECTION_KEY,
DEFAULT_NAMESPACE_INJECTION_KEY,
EDIT_TASK_FUNCTION_INJECTION_KEY,
EDITING_TASK_INJECTION_KEY,
FIELDNAME_INJECTION_KEY,
@@ -57,7 +55,7 @@
REF_PATH_INJECTION_KEY,
ROOT_SCHEMA_INJECTION_KEY,
SCHEMA_DEFINITIONS_INJECTION_KEY,
UPDATE_YAML_FUNCTION_INJECTION_KEY,
UPDATE_TASK_FUNCTION_INJECTION_KEY,
} from "./injectionKeys";
import {useFlowFields, SECTIONS_IDS} from "./utils/useFlowFields";
import debounce from "lodash/debounce";
@@ -67,7 +65,6 @@
import {useKeyboardSave} from "./utils/useKeyboardSave";
import {deepEqual} from "../../utils/utils";
import {useScrollMemory} from "../../composables/useScrollMemory";
import {defaultNamespace} from "../../composables/useNamespaces";
const props = defineProps<NoCodeProps>();
@@ -169,8 +166,6 @@
provide(REF_PATH_INJECTION_KEY, props.refPath);
provide(PANEL_INJECTION_KEY, panel)
provide(POSITION_INJECTION_KEY, props.position ?? "after");
provide(CREATING_FLOW_INJECTION_KEY, flowStore.isCreating ?? false);
provide(DEFAULT_NAMESPACE_INJECTION_KEY, computed(() => flowStore.flow?.namespace ?? defaultNamespace() ?? "company.team"));
provide(CREATING_TASK_INJECTION_KEY, props.creatingTask);
provide(EDITING_TASK_INJECTION_KEY, props.editingTask);
provide(FIELDNAME_INJECTION_KEY, props.fieldName);
@@ -189,7 +184,7 @@
emit("closeTask")
})
provide(UPDATE_YAML_FUNCTION_INJECTION_KEY, (yaml) => {
provide(UPDATE_TASK_FUNCTION_INJECTION_KEY, (yaml) => {
editorUpdate(yaml)
})

View File

@@ -7,7 +7,7 @@
<template #label>
<div class="type-div">
<span class="asterisk">*</span>
<code>{{ $t("type") }}</code>
<code>{{ t("type") }}</code>
</div>
</template>
<PluginSelect
@@ -32,6 +32,7 @@
<script setup lang="ts">
import {computed, inject, onActivated, provide, ref, toRaw, watch} from "vue";
import {useI18n} from "vue-i18n";
import * as YAML_UTILS from "@kestra-io/ui-libs/flow-yaml-utils";
import TaskObject from "./tasks/TaskObject.vue";
import PluginSelect from "../../plugins/PluginSelect.vue";
@@ -52,6 +53,8 @@
import isEqual from "lodash/isEqual";
import {useMiscStore} from "../../../override/stores/misc";
const {t} = useI18n();
defineOptions({
name: "TaskEditor",
inheritAttrs: false,

View File

@@ -44,6 +44,7 @@
<script setup lang="ts">
import {computed, inject, ref} from "vue";
import {BLOCK_SCHEMA_PATH_INJECTION_KEY} from "../../injectionKeys";
import {useFlowStore} from "../../../../stores/flow";
import Creation from "./taskList/buttons/Creation.vue";
import Element from "./taskList/Element.vue";
import * as YAML_UTILS from "@kestra-io/ui-libs/flow-yaml-utils";
@@ -52,7 +53,7 @@
import {
CREATING_TASK_INJECTION_KEY, FULL_SCHEMA_INJECTION_KEY, FULL_SOURCE_INJECTION_KEY,
PARENT_PATH_INJECTION_KEY, REF_PATH_INJECTION_KEY, UPDATE_YAML_FUNCTION_INJECTION_KEY,
PARENT_PATH_INJECTION_KEY, REF_PATH_INJECTION_KEY,
} from "../../injectionKeys";
import {SECTIONS_MAP} from "../../../../utils/constants";
import {getValueAtJsonPath} from "../../../../utils/utils";
@@ -82,6 +83,8 @@
inheritAttrs: false
});
const flowStore = useFlowStore();
interface Task {
id:string,
type:string
@@ -147,8 +150,6 @@
const movedIndex = ref(-1);
const updateYaml = inject(UPDATE_YAML_FUNCTION_INJECTION_KEY, () => {});
const moveElement = (
items: Record<string, any>[] | undefined,
elementID: string,
@@ -170,7 +171,7 @@
movedIndex.value = -1;
}, 200);
updateYaml(
flowStore.flowYaml =
YAML_UTILS.swapBlocks({
source:flow.value,
section: SECTIONS_MAP[section.value.toLowerCase() as keyof typeof SECTIONS_MAP],
@@ -178,7 +179,6 @@
key2:items[newIndex][keyName],
keyName,
})
);
};
const fullSchema = inject(FULL_SCHEMA_INJECTION_KEY, ref<Record<string, any>>({}));

View File

@@ -8,17 +8,18 @@
</template>
<script lang="ts" setup>
import {onMounted, inject, computed, provide} from "vue";
import {computed, onMounted} from "vue";
import {useFlowStore} from "../../../../stores/flow";
import NamespaceSelect from "../../../namespaces/components/NamespaceSelect.vue";
import {CREATING_FLOW_INJECTION_KEY, DEFAULT_NAMESPACE_INJECTION_KEY} from "../../injectionKeys";
const modelValue = defineModel<string>();
const isCreating = inject(CREATING_FLOW_INJECTION_KEY, false);
const defaultNamespace = inject(DEFAULT_NAMESPACE_INJECTION_KEY, computed(() => ""));
provide(DEFAULT_NAMESPACE_INJECTION_KEY, computed(() => modelValue.value || defaultNamespace.value));
const flowStore = useFlowStore();
const isCreating = computed(() => flowStore.isCreating);
onMounted(() => {
const flowNamespace = defaultNamespace.value;
const flowNamespace = flowStore.flow?.namespace;
if (!modelValue.value && flowNamespace) {
modelValue.value = flowNamespace;
}

View File

@@ -24,7 +24,6 @@ export const POSITION_INJECTION_KEY = Symbol("position-injection-key") as Inject
* NOTE: different from the `isCreating` flag coming from the store. `isCreating` refers to the Complete flow being in creation
*/
export const CREATING_TASK_INJECTION_KEY = Symbol("creating-injection-key") as InjectionKey<boolean>
export const CREATING_FLOW_INJECTION_KEY = Symbol("creating-flow-injection-key") as InjectionKey<boolean>
/**
* When creating anew task, allows to specify a field where the new task should be injected.
* @example
@@ -52,9 +51,9 @@ export const EDIT_TASK_FUNCTION_INJECTION_KEY = Symbol("edit-function-injection-
*/
export const CLOSE_TASK_FUNCTION_INJECTION_KEY = Symbol("close-function-injection-key") as InjectionKey<() => void>
/**
* Call this function to update the full Yaml content
* We call this function when a task is changed, as soon as the first click or type is done
*/
export const UPDATE_YAML_FUNCTION_INJECTION_KEY = Symbol("update-function-injection-key") as InjectionKey<(yaml: string) => void>
export const UPDATE_TASK_FUNCTION_INJECTION_KEY = Symbol("update-function-injection-key") as InjectionKey<(yaml: string) => void>
/**
* Set this to override the contents of the no-code editor with a component of your choice
* This is used to display the metadata edition inputs
@@ -93,6 +92,4 @@ export const SCHEMA_DEFINITIONS_INJECTION_KEY = Symbol("schema-definitions-injec
export const DATA_TYPES_MAP_INJECTION_KEY = Symbol("data-types-injection-key") as InjectionKey<ComputedRef<Record<string, string[] | undefined>>>
export const ON_TASK_EDITOR_CLICK_INJECTION_KEY = Symbol("on-task-editor-click-injection-key") as InjectionKey<(elt?: Partial<NoCodeElement>) => void>;
export const DEFAULT_NAMESPACE_INJECTION_KEY = Symbol("default-namespace-injection-key") as InjectionKey<ComputedRef<string>>;
export const ON_TASK_EDITOR_CLICK_INJECTION_KEY = Symbol("on-task-editor-click-injection-key") as InjectionKey<(elt?: Partial<NoCodeElement>) => void>;

View File

@@ -16,7 +16,7 @@
import {PLUGIN_DEFAULTS_SECTION, SECTIONS_MAP} from "../../../utils/constants";
import {
CLOSE_TASK_FUNCTION_INJECTION_KEY,
UPDATE_YAML_FUNCTION_INJECTION_KEY,
UPDATE_TASK_FUNCTION_INJECTION_KEY,
FULL_SOURCE_INJECTION_KEY, CREATING_TASK_INJECTION_KEY,
PARENT_PATH_INJECTION_KEY, POSITION_INJECTION_KEY,
REF_PATH_INJECTION_KEY, EDIT_TASK_FUNCTION_INJECTION_KEY,
@@ -37,7 +37,7 @@
const fieldName = inject(FIELDNAME_INJECTION_KEY, undefined);
const blockSchemaPath = inject(BLOCK_SCHEMA_PATH_INJECTION_KEY, ref(""));
const updateTask = inject(UPDATE_YAML_FUNCTION_INJECTION_KEY, () => {})
const updateTask = inject(UPDATE_TASK_FUNCTION_INJECTION_KEY, () => {})
const closeTaskAddition = inject(
CLOSE_TASK_FUNCTION_INJECTION_KEY,

View File

@@ -3,7 +3,7 @@
<template #additional-right>
<ul>
<li>
<el-button v-if="canCreate" tag="router-link" :to="{name: 'flows/create', query: {namespace: $route.query.namespace}}" :icon="Plus" type="secondary">
<el-button v-if="canCreate" tag="router-link" :to="{name: 'flows/create', query: {namespace: $route.query.namespace}}" :icon="Plus" type="primary">
{{ $t('create_flow') }}
</el-button>
</li>
@@ -30,7 +30,7 @@
<el-button
v-if="isOSS"
@click="startTour"
:icon="Compass"
:icon="Plus"
size="large"
type="primary"
class="px-3 p-4 section-1-link product-link"
@@ -39,7 +39,7 @@
</el-button>
<el-button
v-else
:icon="Compass"
:icon="Plus"
tag="router-link"
:to="{name: 'flows/create'}"
size="large"
@@ -74,7 +74,6 @@
import {useCoreStore} from "../../stores/core";
import {useI18n} from "vue-i18n";
import Plus from "vue-material-design-icons/Plus.vue";
import Compass from "vue-material-design-icons/Compass.vue";
import Play from "vue-material-design-icons/Play.vue";
import OnboardingBottom from "override/components/OnboardingBottom.vue";
import kestraWelcome from "../../assets/onboarding/kestra_welcome.svg";

View File

@@ -156,7 +156,7 @@
<el-form-item :label="$t('secret.key')" prop="key">
<el-input v-model="secret.key" :disabled="secret.update" required />
</el-form-item>
<el-form-item v-if="!secret.update" :label="$t('secret.name')" prop="value" required>
<el-form-item v-if="!secret.update" :label="$t('secret.name')" prop="value">
<MultilineSecret v-model="secret.value" :placeholder="secretModalTitle" />
</el-form-item>
<el-form-item v-if="secret.update" :label="$t('secret.name')" prop="value">

View File

@@ -355,7 +355,7 @@
autoRefreshInterval: 10
},
defaultPreferences: {
theme: "syncWithSystem",
theme: "light",
logsFontSize: 12,
editorFontFamily: "'Source Code Pro', monospace",
editorFontSize: 12,

View File

@@ -1,4 +1,3 @@
import {computed, watch} from "vue";
import {useI18n} from "vue-i18n";
import {configureMonacoYaml} from "monaco-yaml";
import * as monaco from "monaco-editor/esm/vs/editor/editor.api";
@@ -22,7 +21,6 @@ import {
registerPebbleAutocompletion
} from "./pebbleLanguageConfigurator";
import {usePluginsStore} from "../../../stores/plugins";
import {useBlueprintsStore} from "../../../stores/blueprints";
import {languages} from "monaco-editor/esm/vs/editor/editor.api";
import CompletionItem = languages.CompletionItem;
@@ -36,14 +34,11 @@ export class YamlLanguageConfigurator extends AbstractLanguageConfigurator {
}
async configureLanguage(pluginsStore: ReturnType<typeof usePluginsStore>) {
const validateYAML = computed(() => useBlueprintsStore().validateYAML);
watch(validateYAML, (shouldValidate) => configureMonacoYaml(monaco, {validate: shouldValidate}));
configureMonacoYaml(monaco, {
enableSchemaRequest: true,
hover: localStorage.getItem("hoverTextEditor") === "true",
completion: true,
validate: validateYAML.value ?? true,
validate: true,
format: true,
schemas: yamlSchemas()
});

View File

@@ -1,11 +1,5 @@
<template>
<SideBar
v-if="menu"
:menu
:showLink
@menu-collapse="onCollapse"
:class="{overlay: verticalLayout}"
>
<SideBar v-if="menu" :menu :showLink="showLink" @menu-collapse="onCollapse">
<template #footer>
<Auth />
</template>
@@ -17,9 +11,6 @@
import SideBar from "../../components/layout/SideBar.vue";
import Auth from "../../override/components/auth/Auth.vue";
import {useBreakpoints, breakpointsElement} from "@vueuse/core";
const verticalLayout = useBreakpoints(breakpointsElement).smallerOrEqual("sm");
withDefaults(defineProps<{
showLink?: boolean
}>(), {

View File

@@ -56,10 +56,7 @@
<div v-if="!system && blueprint.tags?.length > 0" class="tags-section">
<span v-for="tag in processedTags(blueprint.tags)" :key="tag.original" class="tag-item">{{ tag.display }}</span>
</div>
<div v-if="blueprint.template" class="tags-section">
<span class="tag-item">{{ $t('template') }}</span>
</div>
<div class="text-section">
<div class="text-section">
<h3 class="title">
{{ blueprint.title ?? blueprint.id }}
</h3>
@@ -69,7 +66,7 @@
<TaskIcon v-for="task in [...new Set(blueprint.includedTasks)]" :key="task" :cls="task" :icons="pluginsStore.icons" />
</div>
<div class="d-flex align-items-center gap-2">
<div class="action-button">
<el-tooltip v-if="embed && !system" trigger="click" content="Copied" placement="left" :autoClose="2000" effect="light">
<el-button
type="primary"
@@ -154,7 +151,6 @@
id: string;
tags: string[];
title?: string;
template?: Record<string, any>;
}[] | undefined>(undefined);
const error = ref(false);
const icon = {ContentCopy};

View File

@@ -6,20 +6,20 @@
<Action
v-if="tab === 'flows'"
:label="$t('create_flow')"
:label="t('create_flow')"
:to="{name: 'flows/create', query: {namespace}}"
/>
<Action
v-if="tab === 'kv'"
:label="$t('kv.inherited')"
:label="t('kv.inherited')"
:icon="FamilyTree"
@click="namespacesStore.inheritedKVModalVisible = true"
/>
<Action
v-if="tab === 'kv'"
:label="$t('kv.add')"
:label="t('kv.add')"
@click="namespacesStore.addKvModalVisible = true"
/>
</template>
@@ -27,6 +27,7 @@
<script setup lang="ts">
import {computed, Ref} from "vue";
import {useRoute, useRouter} from "vue-router";
import {useI18n} from "vue-i18n";
import {useNamespacesStore} from "override/stores/namespaces";
import Action from "../../../components/namespaces/components/buttons/Action.vue";
import Dashboards from "../../../components/dashboard/components/selector/Selector.vue";
@@ -35,6 +36,7 @@
const route = useRoute();
const router = useRouter();
const {t} = useI18n({useScope: "global"});
const namespacesStore = useNamespacesStore();
const onSelectDashboard = (value: any) => {

View File

@@ -1,70 +1,45 @@
import {computed} from "vue";
import {useRoute, useRouter, type RouteRecordNameGeneric} from "vue-router";
import {useRoute, useRouter} from "vue-router";
import {useI18n} from "vue-i18n";
import {useMiscStore} from "override/stores/misc";
import {getDashboard} from "../../components/dashboard/composables/useDashboards";
// Main icons
import ChartLineVariant from "vue-material-design-icons/ChartLineVariant.vue";
import FileTreeOutline from "vue-material-design-icons/FileTreeOutline.vue";
import LayersTripleOutline from "vue-material-design-icons/LayersTripleOutline.vue";
import ContentCopy from "vue-material-design-icons/ContentCopy.vue";
import PlayOutline from "vue-material-design-icons/PlayOutline.vue";
import FileDocumentOutline from "vue-material-design-icons/FileDocumentOutline.vue";
import FlaskOutline from "vue-material-design-icons/FlaskOutline.vue";
// import PackageVariantClosed from "vue-material-design-icons/PackageVariantClosed.vue";
import FolderOpenOutline from "vue-material-design-icons/FolderOpenOutline.vue";
import PuzzleOutline from "vue-material-design-icons/PuzzleOutline.vue";
import ShapePlusOutline from "vue-material-design-icons/ShapePlusOutline.vue";
import OfficeBuildingOutline from "vue-material-design-icons/OfficeBuildingOutline.vue";
import ServerNetworkOutline from "vue-material-design-icons/ServerNetworkOutline.vue";
// Blueprints icons
import Wrench from "vue-material-design-icons/Wrench.vue";
// Tenant Administration icons
import Monitor from "vue-material-design-icons/Monitor.vue";
import TimelineClockOutline from "vue-material-design-icons/TimelineClockOutline.vue";
import TimelineTextOutline from "vue-material-design-icons/TimelineTextOutline.vue";
import BallotOutline from "vue-material-design-icons/BallotOutline.vue";
import ShieldAccountVariantOutline from "vue-material-design-icons/ShieldAccountVariantOutline.vue";
import ViewDashboardVariantOutline from "vue-material-design-icons/ViewDashboardVariantOutline.vue";
import Connection from "vue-material-design-icons/Connection.vue";
import DotsSquare from "vue-material-design-icons/DotsSquare.vue";
import FormatListGroupPlus from "vue-material-design-icons/FormatListGroupPlus.vue";
import DatabaseOutline from "vue-material-design-icons/DatabaseOutline.vue";
import LockOutline from "vue-material-design-icons/LockOutline.vue";
import LightningBolt from "vue-material-design-icons/LightningBolt.vue";
import Battery40 from "vue-material-design-icons/Battery40.vue";
import ShieldAccount from "vue-material-design-icons/ShieldAccount.vue";
import ShieldKeyOutline from "vue-material-design-icons/ShieldKeyOutline.vue";
import FlaskOutline from "vue-material-design-icons/FlaskOutline.vue";
export type MenuItem = {
title: string;
routes?: RouteRecordNameGeneric[];
href?: {
name: string;
params?: Record<string, any>;
query?: Record<string, any>;
};
icon?: {
element?: any;
class?: any;
};
child?: MenuItem[];
attributes?: {
locked?: boolean;
};
hidden?: boolean;
path?: string,
name: string,
params?: Record<string, any>,
query?: Record<string, any>
},
child?: MenuItem[],
disabled?: boolean,
};
export function useLeftMenu() {
const {t} = useI18n({useScope: "global"});
const $route = useRoute();
const $router = useRouter();
const {t} = useI18n({useScope: "global"});
const configs = useMiscStore().configs;
const miscStore = useMiscStore();
/**
* Returns the names of all registered routes whose name starts with the given prefix.
*
* @param route - The route name prefix to match against.
* @returns An array of route names starting with the provided prefix.
* Returns all route names that start with the given route
* @param route
* @returns
*/
function routeStartWith(route: string) {
return $router
@@ -75,145 +50,140 @@ export function useLeftMenu() {
.map((r) => r.name);
}
const menu = computed<MenuItem[]>(() => {
return [
const flatMenuItems = (items: MenuItem[]): MenuItem[] => {
return items.flatMap(item => item.child ? [item, ...flatMenuItems(item.child)] : [item])
}
const menu = computed(() => {
const generatedMenu = [
{
title: t("dashboards.labels.plural"),
href: {
name: "home",
params: {
dashboard: getDashboard($route, "id"),
},
params: {dashboard: getDashboard($route, "id")},
},
title: t("dashboards.labels.plural"),
icon: {
element: ChartLineVariant,
element: ViewDashboardVariantOutline,
class: "menu-icon",
},
},
{
title: t("flows"),
href: {name: "flows/list"},
routes: routeStartWith("flows"),
href: {
name: "flows/list",
},
title: t("flows"),
icon: {
element: FileTreeOutline,
class: "menu-icon",
},
exact: false,
},
{
title: t("apps"),
href: {name: "apps/list"},
routes: routeStartWith("apps"),
href: {
name: "apps/list",
},
title: t("apps"),
icon: {
element: LayersTripleOutline,
element: FormatListGroupPlus,
class: "menu-icon",
},
attributes: {
locked: true,
},
},
{
title: t("executions"),
routes: routeStartWith("executions"),
href: {
name: "executions/list",
},
icon: {
element: PlayOutline,
},
},
{
title: t("logs"),
routes: routeStartWith("logs"),
href: {
name: "logs/list",
},
icon: {
element: FileDocumentOutline,
},
},
{
title: t("demos.tests.label"),
routes: routeStartWith("tests"),
href: {
name: "tests/list",
},
icon: {
element: FlaskOutline,
},
attributes: {
locked: true,
},
},
// TODO: To add Assets entry here in future release
// Uncomment PackageVariantClosed on line 25 and use as the icon
{
title: t("namespaces"),
routes: routeStartWith("namespaces"),
href: {
name: "namespaces/list",
},
icon: {
element: FolderOpenOutline,
},
},
{
title: t("templates"),
href: {name: "templates/list"},
routes: routeStartWith("templates"),
href: {
name: "templates/list",
},
title: t("templates"),
icon: {
element: ContentCopy,
class: "menu-icon",
},
hidden: !configs?.isTemplateEnabled,
hidden: !miscStore.configs?.isTemplateEnabled,
},
{
title: t("plugins.names"),
routes: routeStartWith("plugins"),
href: {
name: "plugins/list",
},
href: {name: "executions/list"},
routes: routeStartWith("executions"),
title: t("executions"),
icon: {
element: PuzzleOutline,
element: TimelineClockOutline,
class: "menu-icon",
},
},
{
href: {name: "logs/list"},
routes: routeStartWith("logs"),
title: t("logs"),
icon: {
element: TimelineTextOutline,
class: "menu-icon",
},
},
{
href: {name: "tests/list"},
routes: routeStartWith("tests"),
title: t("demos.tests.label"),
icon: {
element: FlaskOutline,
class: "menu-icon"
},
attributes: {
locked: true,
},
},
{
href: {name: "namespaces/list"},
routes: routeStartWith("namespaces"),
title: t("namespaces"),
icon: {
element: DotsSquare,
class: "menu-icon",
},
},
{
href: {name: "kv/list"},
routes: routeStartWith("kv"),
title: t("kv.name"),
icon: {
element: DatabaseOutline,
class: "menu-icon",
},
},
{
href: {name: "secrets/list"},
routes: routeStartWith("secrets"),
title: t("secret.names"),
icon: {
element: ShieldKeyOutline,
class: "menu-icon",
},
attributes: {
locked: true,
},
},
{
title: t("blueprints.title"),
routes: routeStartWith("blueprints"),
title: t("blueprints.title"),
icon: {
element: ShapePlusOutline,
element: BallotOutline,
class: "menu-icon",
},
child: [
{
title: t("blueprints.custom"),
routes: routeStartWith("blueprints/flow/custom"),
href: {
name: "blueprints",
params: {
kind: "flow",
tab: "custom",
},
},
icon: {
element: Wrench,
},
routes: routeStartWith("blueprints/flow"),
attributes: {
locked: true,
},
href: {
name: "blueprints",
params: {kind: "flow", tab: "custom"},
},
},
{
title: t("blueprints.flows"),
routes: routeStartWith("blueprints/flow/community"),
routes: routeStartWith("blueprints/flow"),
href: {
name: "blueprints",
params: {
kind: "flow",
tab: "community",
},
},
icon: {
element: FileTreeOutline,
params: {kind: "flow", tab: "community"},
},
},
{
@@ -221,144 +191,91 @@ export function useLeftMenu() {
routes: routeStartWith("blueprints/dashboard"),
href: {
name: "blueprints",
params: {
kind: "dashboard",
tab: "community",
},
},
icon: {
element: ChartLineVariant,
params: {kind: "dashboard", tab: "community"},
},
},
],
},
{
title: t("tenant_administration"),
routes: [
"admin/stats",
"kv",
"secrets",
"admin/triggers",
"admin/auditlogs",
"admin/iam",
"admin/concurrency-limits",
]
.map(routeStartWith)
.find((routes) => routes.length > 0),
href: {name: "plugins/list"},
routes: routeStartWith("plugins"),
title: t("plugins.names"),
icon: {
element: OfficeBuildingOutline,
element: Connection,
class: "menu-icon",
},
},
{
title: t("administration"),
routes: routeStartWith("admin"),
icon: {
element: ShieldAccountVariantOutline,
class: "menu-icon",
},
child: [
{
title: t("system overview"),
routes: routeStartWith("admin/stats"),
href: {
name: "admin/stats",
},
icon: {
element: Monitor,
},
},
{
title: t("kv.name"),
routes: routeStartWith("kv"),
href: {
name: "kv/list",
},
icon: {
element: DatabaseOutline,
},
},
{
title: t("secret.names"),
routes: routeStartWith("secrets"),
href: {
name: "secrets/list",
},
icon: {
element: LockOutline,
},
attributes: {
locked: true,
},
},
{
title: t("triggers"),
routes: routeStartWith("admin/triggers"),
href: {
name: "admin/triggers",
},
icon: {
element: LightningBolt,
},
},
{
title: t("auditlogs"),
routes: routeStartWith("admin/auditlogs"),
href: {
name: "admin/auditlogs/list",
},
icon: {
element: FileDocumentOutline,
},
attributes: {
locked: true,
},
},
{
title: t("concurrency limits"),
routes: routeStartWith("admin/concurrency-limits"),
href: {
name: "admin/concurrency-limits",
},
icon: {
element: Battery40,
},
hidden: !configs?.isConcurrencyViewEnabled,
},
{
title: t("iam"),
href: {name: "admin/iam"},
routes: routeStartWith("admin/iam"),
href: {
name: "admin/iam",
},
icon: {
element: ShieldAccount,
},
title: t("iam"),
attributes: {
locked: true,
},
},
{
href: {name: "admin/auditlogs/list"},
routes: routeStartWith("admin/auditlogs"),
title: t("auditlogs"),
attributes: {
locked: true,
},
},
{
href: {name: "admin/triggers"},
routes: routeStartWith("admin/triggers"),
title: t("triggers"),
},
{
href: {name: "admin/instance"},
routes: routeStartWith("admin/instance"),
title: t("instance"),
attributes: {
locked: true,
},
},
{
href: {name: "admin/tenants/list"},
routes: routeStartWith("admin/tenants"),
title: t("tenant.names"),
attributes: {
locked: true,
},
},
{
href: {name: "admin/concurrency-limits"},
routes: routeStartWith("admin/concurrency-limits"),
title: t("concurrency limits"),
hidden: !miscStore.configs?.isConcurrencyViewEnabled,
},
{
href: {name: "admin/stats"},
routes: routeStartWith("admin/stats"),
title: t("system overview"),
},
],
},
{
title: t("instance_administration"),
routes: routeStartWith("admin/instance"),
href: {
name: "admin/instance",
},
icon: {
element: ServerNetworkOutline,
},
attributes: {
locked: true,
},
},
].map((item: MenuItem) => {
if (item.icon?.element) {
item.icon.class = "menu-icon"; // Add default class to all menu icons
}
];
if (item.href && item.href?.name === $route.name) {
item.href.query = {
...$route.query,
...item.href?.query,
};
flatMenuItems(generatedMenu).forEach(menuItem => {
if (menuItem.href !== undefined && menuItem.href?.name === $route.name) {
menuItem.href.query = {...$route.query, ...menuItem.href?.query};
}
return item;
});
return generatedMenu;
});
return {menu};
return {
routeStartWith,
menu
};
}

View File

@@ -110,7 +110,7 @@ export default [
//Admin
{name: "admin/triggers", path: "/:tenant?/admin/triggers", component: () => import("../components/admin/Triggers.vue")},
{name: "admin/stats", path: "/:tenant?/admin/stats/:type?", component: () => import("override/components/admin/stats/Stats.vue")},
{name: "admin/stats", path: "/:tenant?/admin/stats", component: () => import("override/components/admin/stats/Stats.vue")},
{name: "admin/concurrency-limits", path: "/:tenant?/admin/concurrency-limits", component: () => import("../components/admin/ConcurrencyLimits.vue")},
//Setup

View File

@@ -25,8 +25,6 @@ interface Blueprint {
[key: string]: any;
}
export type TemplateArgument = Record<string, Input>;
export interface BlueprintTemplate {
source: string;
templateArguments: Record<string, Input>;
@@ -57,8 +55,6 @@ export const useBlueprintsStore = defineStore("blueprints", () => {
const source = ref<string | undefined>(undefined);
const graph = ref<any | undefined>(undefined);
const validateYAML = ref<boolean>(true); // Used to enable/disable YAML validation in Monaco editor, for the purpose of Templated Blueprints
const getBlueprints = async (options: Options) => {
const PARAMS = {params: options.params, ...VALIDATE};
@@ -170,8 +166,6 @@ export const useBlueprintsStore = defineStore("blueprints", () => {
source,
graph,
validateYAML,
getBlueprints,
getBlueprint,
getBlueprintSource,

View File

@@ -1,237 +1,208 @@
@import "@kestra-io/ui-libs/src/scss/variables.scss";
#app {
.v-sidebar-menu.vsm_expanded.overlay {
position: absolute;
}
.vsm--item {
padding: 0 30px;
transition: padding 0.2s ease;
}
.vsm--icon {
width: 20px;
margin-right: calc($spacer / 2);
transition: left 0.2s ease;
background-color: transparent !important;
padding-bottom: 15px;
svg {
height: 20px !important;
width: 20px !important;
position: relative;
margin-top: 13px;
}
}
.vsm--title {
font-size: $font-size-sm;
&>span {
width: 100%;
}
}
.vsm--child {
.vsm--item {
padding: 0;
.vsm--title {
font-size: $font-size-xs;
}
}
#app {
.vsm--icon {
width: 1rem;
transition: left 0.2s ease;
font-size: 1.5em;
background-color: transparent !important;
padding-bottom: 15px;
width: 30px !important;
z-index: 20; // in collapsed menu, keep the icon above the opening menu
svg {
height: 1rem !important;
width: 1rem !important;
}
}
}
.vsm--link {
height: 30px;
padding: 0.25rem 0.5rem;
margin-bottom: 0.3rem;
border-radius: .25rem;
transition: padding 0.2s ease;
color: var(--ks-content-primary);
box-shadow: none;
&_active,
body &_active:hover {
background-color: var(--ks-button-background-primary) !important;
color: var(--ks-button-content-primary);
font-weight: normal;
}
&.vsm--link_open,
&.vsm--link_open:hover {
background-color: var(--ks-background-left-menu);
color: var(--ks-content-primary);
}
&_disabled {
pointer-events: auto;
opacity: 1;
}
&:hover,
body &_hover {
background-color: var(--ks-button-background-secondary-hover);
}
.el-tooltip__trigger {
display: flex;
}
&>span {
max-width: 100%;
}
}
.vsm--link_open {
position: relative !important;
z-index: 3;
}
.vsm_collapsed .vsm--link_open {
position: static !important;
}
.vsm--child .vsm--link {
padding: 0 0.2rem;
position: relative !important;
margin-left: 1.8rem;
&.vsm--link_level-3 {
margin-left: 3.6rem;
& span {
margin-left: calc($spacer / 4);
position: relative;
margin-top: 13px;
}
}
.vsm--icon {
margin-left: calc($spacer / 2);
color: var(--ks-content-secondary);
// Make Plugins icon appear as outline
.vsm--link[href*="plugins"] .vsm--icon svg {
fill: none !important;
stroke: currentColor !important;
stroke-width: 1.5 !important;
}
&.vsm--link_active .vsm--icon {
color: var(--ks-button-content-primary);
.vsm--item {
padding: 0 30px;
transition: padding 0.2s ease;
}
&:before {
content: "";
position: absolute;
left: -.8rem;
height: 150%;
border: 2px solid var(--ks-border-primary);
border-top: 0;
border-right: 0;
z-index: 2;
// mask the right half of the object and the top border
clip-path: polygon(50% 8px, 50% 100%, 0 100%, 0 8px);
}
}
.vsm--title span:first-child {
flex-grow: 0;
}
.vsm--link_open.vsm--link_active {
.vsm--title,
.vsm--icon {
color: var(--ks-button-content-primary);
}
}
.vsm--arrow_default {
width: 8px;
&:before {
border-left-width: 1px;
border-bottom-width: 1px;
height: 4px;
width: 4px;
top: 3px;
}
}
a.vsm--link_active[href="#"] {
cursor: initial !important;
}
.vsm--dropdown {
background-color: var(--ks-background-left-menu);
border-radius: 4px;
margin-bottom: .5rem;
.vsm--title {
top: 3px;
}
}
.vsm--scroll-thumb {
background: var(--ks-border-primary) !important;
border-radius: 8px;
}
.vsm--mobile-bg {
border-radius: 0 var(--bs-border-radius) var(--bs-border-radius) 0;
}
.vsm_collapsed {
.logo {
>* {
left: 10px;
span.img {
background-size: 207px 55px;
.vsm--child {
.vsm--item {
padding: 0;
.vsm--title {
padding-left: 10px;
}
}
}
.vsm--link {
padding-left: 13px;
padding: 0.3rem 0.5rem;
margin-bottom: 0.3rem;
border-radius: .25rem;
transition: padding 0.2s ease;
color: var(--ks-content-primary);
box-shadow: none;
&.vsm--link_hover {
background-color: var(--ks-button-background-primary);
&_active, body &_active:hover {
background-color: var(--ks-button-background-primary) !important;
color: var(--ks-button-content-primary);
font-weight: normal;
}
&.vsm--link_open, &.vsm--link_open:hover {
background-color: var(--ks-background-left-menu);
color: var(--ks-content-primary);
}
&_disabled {
pointer-events: auto;
opacity: 1;
}
&:hover, body &_hover {
background-color: var(--ks-button-background-secondary-hover);
}
.el-tooltip__trigger {
display: flex;
}
& > span{
max-width: 100%;
}
}
.vsm--link_open{
position: relative !important;
z-index: 3;
}
.vsm_collapsed .vsm--link_open{
position: static !important;
}
.vsm--child .vsm--link{
padding: 0 0.2rem;
position: relative!important;
font-size: 14px;
margin-left: 1.8rem;
.vsm--icon {
margin-right:4px;
color: var(--ks-content-secondary);
}
&.vsm--link_active .vsm--icon{
color: var(--ks-button-content-primary);
}
&:before{
content: "";
position: absolute;
left: -.8rem;
top: -2.5rem;
border-radius: 8px;
width: 1.6rem;
height: 170%;
border: 2px solid var(--ks-border-primary);
border-top:0;
border-right:0;
z-index: 2;
// mask the right half of the object and the top border
clip-path: polygon(50% 8px, 50% 100%, 0 100%, 0 8px);
}
}
.vsm--title span:first-child{
flex-grow: 0;
}
.vsm--link_open.vsm--link_active {
.vsm--title, .vsm--icon {
color: var(--ks-button-content-primary);
}
}
.vsm--arrow_default{
width: 8px;
&:before{
border-left-width: 1px;
border-bottom-width: 1px;
height: 4px;
width: 4px;
top: 3px;
}
}
a.vsm--link_active[href="#"] {
cursor: initial !important;
}
.vsm--dropdown {
background-color: var(--ks-background-left-menu);
border-radius: 4px;
margin-bottom: .5rem;
.vsm--title {
top: 3px;
}
}
.vsm--scroll-thumb {
background: var(--ks-border-primary) !important;
border-radius: 8px;
}
.vsm--mobile-bg {
border-radius: 0 var(--bs-border-radius) var(--bs-border-radius) 0;
}
.vsm_collapsed {
.logo {
> * {
left: 10px;
span.img {
background-size: 207px 55px;
}
}
}
.vsm--link {
padding-left: 13px;
&.vsm--link_hover {
background-color: var(--ks-button-background-primary);
color: var(--ks-button-content-primary);
}
}
.vsm--item {
padding: 0 5px;
}
.el-button {
margin-right: 0;
}
}
.el-tooltip__trigger .lock-icon.material-design-icon > .material-design-icon__svg {
bottom: 0 !important;
margin-left: 5px;
}
.vsm--item {
padding: 0 5px;
position: relative;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 10px;
height: 1.25rem;
z-index: 5;
background: linear-gradient(to top, var(--ks-background-left-menu), transparent);
opacity: 0.18;
}
}
.el-button {
margin-right: 0;
}
}
.el-tooltip__trigger .lock-icon.material-design-icon>.material-design-icon__svg {
bottom: 0 !important;
margin-left: 5px;
}
.vsm--item {
position: relative;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 10px;
height: 1.25rem;
z-index: 5;
background: linear-gradient(to top, var(--ks-background-left-menu), transparent);
opacity: 0.18;
}
}
}
}

View File

@@ -883,12 +883,6 @@
"description": "Nach Trigger-ID filtern",
"label": "Trigger-ID"
},
"triggerState": {
"description": "Nach Trigger-Zustand filtern",
"disabled": "Deaktiviert",
"enabled": "Aktiviert",
"label": "Trigger-Zustand"
},
"update": "Aktualisieren",
"value": "Wert",
"workerId": {
@@ -1011,7 +1005,6 @@
"input_custom_duration": "oder benutzerdefinierte Dauer eingeben:",
"inputs": "Inputs",
"instance": "Instanz",
"instance_administration": "Instanzverwaltung",
"invalid bulk delete": "Ausführungen konnten nicht gelöscht werden",
"invalid bulk force run": "Konnte Ausführungen nicht erzwingen",
"invalid bulk kill": "Ausführungen konnten nicht beendet werden",
@@ -1314,7 +1307,7 @@
"next": "Weiter",
"no_flows": "Keine Flows unter der tutorial Namespace verfügbar.",
"previous": "Zurück",
"skip": "Produkt-Tour überspringen",
"skip": "Tutorial überspringen",
"steps": {
"0": {
"content": "Wir freuen uns, dass Sie hier sind.<br />Lassen Sie uns Ihren ersten Flow erstellen.",
@@ -1744,7 +1737,6 @@
"names": "Mandanten"
},
"tenantId": "Mandanten-ID",
"tenant_administration": "Mandantenverwaltung",
"test-badge-text": "Test",
"test-badge-tooltip": "Diese Ausführung wurde durch einen Test erstellt",
"theme": "Modus",
@@ -1863,7 +1855,7 @@
},
"welcome aboard": "🚀 Willkommen bei Kestra!",
"welcome aboard content": "Nutzen Sie unsere geführte Tour, um Ihren ersten Flow zu erstellen, und schauen Sie sich Blueprints an, um weitere Beispiele zu finden.",
"welcome button create": "Produkt-Tour starten",
"welcome button create": "Meinen ersten Flow erstellen",
"welcome display require": "Führen Sie Ihren <strong>ersten Flow</strong> aus, um loszulegen",
"welcome_page": {
"guide": "Benötigen Sie Unterstützung, um Ihren ersten flow auszuführen?",

View File

@@ -391,8 +391,6 @@
"conditions": "Conditions",
"triggerId": "Trigger ID",
"tenantId": "Tenant ID",
"tenant_administration": "Tenant Administration",
"instance_administration": "Instance Administration",
"codeDisabled": "Disabled in Flow",
"paused": "Paused",
"Fold auto": "Editor: automatic fold of multi-lines",
@@ -540,7 +538,7 @@
"welcome aboard": "\uD83D\uDE80 Welcome to Kestra!",
"welcome aboard content": "Use our Guided Tour to create your first flow and check Blueprints to find more examples.",
"welcome display require": "Run your <strong>first flow</strong> to get started",
"welcome button create": "Start Product Tour",
"welcome button create": "Create my first flow",
"live help": "Live help",
"show task documentation": "Show task documentation",
"hide task documentation": "Hide task documentation",
@@ -947,7 +945,7 @@
"next": "Next",
"previous": "Previous",
"finish": "Finish",
"skip": "Skip Product Tour",
"skip": "Skip Tutorial",
"no_flows": "No flows available under tutorial namespace.",
"steps": {
"0": {
@@ -1729,12 +1727,6 @@
"label": "Trigger Execution ID",
"description": "Filter by trigger execution ID"
},
"triggerState":{
"label": " Trigger State",
"description": "Filter by trigger state",
"enabled": "Enabled",
"disabled": "Disabled"
},
"scope_flow": {
"label": "Scope",
"description": "Filter by flow scope"

View File

@@ -883,12 +883,6 @@
"description": "Filtrar por trigger ID",
"label": "ID de Trigger"
},
"triggerState": {
"description": "Filtrar por estado del trigger",
"disabled": "Desactivado",
"enabled": "Habilitado",
"label": "Estado del Trigger"
},
"update": "Actualizar",
"value": "Valor",
"workerId": {
@@ -1011,7 +1005,6 @@
"input_custom_duration": "o ingrese duración personalizada:",
"inputs": "Entradas",
"instance": "Instancia",
"instance_administration": "Administración de Instancia",
"invalid bulk delete": "No se pudieron eliminar las ejecuciones",
"invalid bulk force run": "No se pudo forzar la ejecución de ejecuciones",
"invalid bulk kill": "No se pudieron matar las ejecuciones",
@@ -1314,7 +1307,7 @@
"next": "Siguiente",
"no_flows": "No hay flows disponibles bajo el namespace del tutorial.",
"previous": "Anterior",
"skip": "Omitir recorrido del producto",
"skip": "Saltar Tutorial",
"steps": {
"0": {
"content": "Estamos encantados de tenerte aquí.<br />Vamos a crear tu primer flow.",
@@ -1744,7 +1737,6 @@
"names": "Arrendatarios"
},
"tenantId": "ID de Mandante",
"tenant_administration": "Administración de Mandantes",
"test-badge-text": "Prueba",
"test-badge-tooltip": "Esta ejecución fue creada por una prueba",
"theme": "Tema",
@@ -1863,7 +1855,7 @@
},
"welcome aboard": "🚀 ¡Bienvenido a Kestra!",
"welcome aboard content": "Usa nuestro Tour Guiado para crear tu primer flow y revisa los Blueprints para encontrar más ejemplos.",
"welcome button create": "Iniciar Tour del Producto",
"welcome button create": "Crear mi primer flow",
"welcome display require": "Ejecuta tu <strong>primer flow</strong> para comenzar",
"welcome_page": {
"guide": "¿Necesitas orientación para ejecutar tu primer flow?",

View File

@@ -883,12 +883,6 @@
"description": "Filtrer par trigger ID",
"label": "ID du trigger"
},
"triggerState": {
"description": "Filtrer par état du trigger",
"disabled": "Désactivé",
"enabled": "Activé",
"label": "État du Trigger"
},
"update": "Mettre à jour",
"value": "Valeur",
"workerId": {
@@ -1011,7 +1005,6 @@
"input_custom_duration": "ou saisir une durée personnalisée :",
"inputs": "Entrées",
"instance": "Instance",
"instance_administration": "Administration de l'Instance",
"invalid bulk delete": "Impossible de supprimer les exécutions",
"invalid bulk force run": "Impossible de forcer l'exécution des exécutions",
"invalid bulk kill": "Impossible d'arrêter les exécutions",
@@ -1314,7 +1307,7 @@
"next": "Suivant",
"no_flows": "Aucun flux disponible dans l'espace de noms du tutoriel.",
"previous": "Précédent",
"skip": "Passer la visite guidée du produit",
"skip": "Passer le tutoriel",
"steps": {
"0": {
"content": "Nous sommes ravis de vous avoir ici.<br />Créons votre premier flux.",
@@ -1744,7 +1737,6 @@
"names": "Mandants"
},
"tenantId": "ID du mandant",
"tenant_administration": "Administration des Mandants",
"test-badge-text": "Test",
"test-badge-tooltip": "Cette exécution a été créée par un Test",
"theme": "Thème",
@@ -1863,7 +1855,7 @@
},
"welcome aboard": "🚀 Bienvenue à bord !",
"welcome aboard content": "Tout est prêt pour Kestra, commencez la création de votre flow et admirez la magie !",
"welcome button create": "Démarrer la visite guidée du produit",
"welcome button create": "Créer mon premier flow",
"welcome display require": "Prêt à commencer à utiliser Kestra ? Créons ensemble <strong>votre premier flow</strong> !",
"welcome_page": {
"guide": "Besoin d'aide pour exécuter votre premier flow ?",

View File

@@ -883,12 +883,6 @@
"description": "ट्रिगर ID द्वारा फ़िल्टर करें",
"label": "ट्रिगर ID"
},
"triggerState": {
"description": "ट्रिगर स्थिति द्वारा फ़िल्टर करें",
"disabled": "अक्षम",
"enabled": "सक्रिय",
"label": "ट्रिगर स्थिति"
},
"update": "अपडेट",
"value": "मान",
"workerId": {
@@ -1011,7 +1005,6 @@
"input_custom_duration": "या कस्टम अवधि दर्ज करें:",
"inputs": "इनपुट्स",
"instance": "इंस्टेंस",
"instance_administration": "इंस्टेंस प्रशासन",
"invalid bulk delete": "निष्पादन हटाने में असमर्थ",
"invalid bulk force run": "निष्पादन को जबरन चलाने में असमर्थ",
"invalid bulk kill": "निष्पादन kill करने में असमर्थ",
@@ -1314,7 +1307,7 @@
"next": "अगला",
"no_flows": "ट्यूटोरियल namespace के अंतर्गत कोई flows उपलब्ध नहीं हैं।",
"previous": "पिछला",
"skip": "उत्पाद टूर छोड़ें",
"skip": "ट्यूटोरियल छोड़ें",
"steps": {
"0": {
"content": "हमें खुशी है कि आप यहाँ हैं।<br />आइए अपना पहला flow बनाएं।",
@@ -1744,7 +1737,6 @@
"names": "मंडल"
},
"tenantId": "टेनेंट ID",
"tenant_administration": "किरायेदार प्रशासन",
"test-badge-text": "परीक्षण",
"test-badge-tooltip": "यह execution एक Test द्वारा बनाया गया था",
"theme": "थीम",
@@ -1863,7 +1855,7 @@
},
"welcome aboard": "🚀 केस्ट्रा में आपका स्वागत है!",
"welcome aboard content": "अपना पहला flow बनाने के लिए हमारे Guided Tour का उपयोग करें और अधिक उदाहरण खोजने के लिए Blueprints देखें।",
"welcome button create": "उत्पाद यात्रा शुरू करें",
"welcome button create": "मेरा पहला flow बनाएं",
"welcome display require": "शुरू करने के लिए अपना <strong>पहला flow</strong> चलाएँ",
"welcome_page": {
"guide": "क्या आपको अपना पहला flow निष्पादित करने के लिए मार्गदर्शन चाहिए?",

View File

@@ -883,12 +883,6 @@
"description": "Filtra per trigger ID",
"label": "ID del trigger"
},
"triggerState": {
"description": "Filtra per stato del trigger",
"disabled": "Disabilitato",
"enabled": "Abilitato",
"label": "Stato del Trigger"
},
"update": "Aggiorna",
"value": "Valore",
"workerId": {
@@ -1011,7 +1005,6 @@
"input_custom_duration": "oppure inserisci durata personalizzata:",
"inputs": "Inputs",
"instance": "Istanza",
"instance_administration": "Amministrazione dell'istanza",
"invalid bulk delete": "Impossibile eliminare le esecuzioni",
"invalid bulk force run": "Impossibile forzare l'esecuzione delle esecuzioni",
"invalid bulk kill": "Impossibile kill le esecuzioni",
@@ -1314,7 +1307,7 @@
"next": "Successivo",
"no_flows": "Nessun flow disponibile sotto il namespace del tutorial.",
"previous": "Precedente",
"skip": "Salta il Tour del Prodotto",
"skip": "Salta Tutorial",
"steps": {
"0": {
"content": "Siamo entusiasti di averti qui.<br />Creiamo il tuo primo flow.",
@@ -1744,7 +1737,6 @@
"names": "Mandanti"
},
"tenantId": "ID del Mandante",
"tenant_administration": "Amministrazione del Mandante",
"test-badge-text": "Test",
"test-badge-tooltip": "Questa esecuzione è stata creata da un Test",
"theme": "Tema",
@@ -1863,7 +1855,7 @@
},
"welcome aboard": "🚀 Benvenuto su Kestra!",
"welcome aboard content": "Usa il nostro Tour Guidato per creare il tuo primo flow e controlla i Blueprints per trovare altri esempi.",
"welcome button create": "Inizia il Tour del Prodotto",
"welcome button create": "Crea il mio primo flow",
"welcome display require": "Esegui il tuo <strong>primo flow</strong> per iniziare",
"welcome_page": {
"guide": "Hai bisogno di assistenza per eseguire il tuo primo flow?",

View File

@@ -883,12 +883,6 @@
"description": "トリガーIDでフィルター",
"label": "トリガーID"
},
"triggerState": {
"description": "トリガー状態でフィルター",
"disabled": "無効",
"enabled": "有効",
"label": "トリガー状態"
},
"update": "更新",
"value": "値",
"workerId": {
@@ -1011,7 +1005,6 @@
"input_custom_duration": "またはカスタム期間を入力してください:",
"inputs": "Inputs",
"instance": "インスタンス",
"instance_administration": "インスタンス管理",
"invalid bulk delete": "実行を削除できませんでした",
"invalid bulk force run": "実行を強制的に開始できませんでした",
"invalid bulk kill": "実行をkillできませんでした",
@@ -1314,7 +1307,7 @@
"next": "次へ",
"no_flows": "チュートリアルnamespaceに利用可能なflowはありません。",
"previous": "前へ",
"skip": "プロダクトツアーをスキップ",
"skip": "チュートリアルをスキップ",
"steps": {
"0": {
"content": "ここに来てくれてとても嬉しいです。<br />最初のflowを作成しましょう。",
@@ -1744,7 +1737,6 @@
"names": "テナント"
},
"tenantId": "テナントID",
"tenant_administration": "テナント管理",
"test-badge-text": "テスト",
"test-badge-tooltip": "この実行はテストによって作成されました",
"theme": "テーマ",
@@ -1863,7 +1855,7 @@
},
"welcome aboard": "🚀 Kestraへようこそ",
"welcome aboard content": "ガイド付きツアーを利用して最初のflowを作成し、Blueprintsでさらに多くの例を見つけてください。",
"welcome button create": "プロダクトツアーを開始",
"welcome button create": "最初のflowを作成",
"welcome display require": "<strong>最初のflow</strong>を実行して始めましょう",
"welcome_page": {
"guide": "最初のflowを実行するためのガイダンスが必要ですか",

View File

@@ -883,12 +883,6 @@
"description": "트리거 ID로 필터링",
"label": "트리거 ID"
},
"triggerState": {
"description": "트리거 상태별 필터링",
"disabled": "비활성화됨",
"enabled": "사용 가능",
"label": "트리거 상태"
},
"update": "업데이트",
"value": "값",
"workerId": {
@@ -1011,7 +1005,6 @@
"input_custom_duration": "또는 사용자 지정 기간 입력:",
"inputs": "Inputs",
"instance": "인스턴스",
"instance_administration": "인스턴스 관리",
"invalid bulk delete": "실행을 삭제할 수 없습니다",
"invalid bulk force run": "실행을 강제로 실행할 수 없습니다.",
"invalid bulk kill": "실행을 강제 종료할 수 없습니다",
@@ -1314,7 +1307,7 @@
"next": "다음",
"no_flows": "튜토리얼 namespace에 사용할 수 있는 flow가 없습니다.",
"previous": "이전",
"skip": "제품 투어 건너뛰기",
"skip": "튜토리얼 건너뛰기",
"steps": {
"0": {
"content": "여기 오신 것을 환영합니다.<br />첫 번째 flow를 만들어 봅시다.",
@@ -1744,7 +1737,6 @@
"names": "테넌트"
},
"tenantId": "테넌트 ID",
"tenant_administration": "테넌트 관리",
"test-badge-text": "테스트",
"test-badge-tooltip": "이 실행은 테스트에 의해 생성되었습니다.",
"theme": "테마",
@@ -1863,7 +1855,7 @@
},
"welcome aboard": "🚀 Kestra에 오신 것을 환영합니다!",
"welcome aboard content": "가이드 투어를 사용하여 첫 번째 flow를 만들고 Blueprints에서 더 많은 예제를 확인하세요.",
"welcome button create": "제품 투어 시작",
"welcome button create": "첫 번째 flow 만들기",
"welcome display require": "<strong>첫 번째 flow</strong>를 실행하여 시작하세요",
"welcome_page": {
"guide": "첫 번째 flow를 실행하는 데 도움이 필요하신가요?",

View File

@@ -883,12 +883,6 @@
"description": "Filtruj według trigger ID",
"label": "Identyfikator Trigger"
},
"triggerState": {
"description": "Filtruj według stanu triggera",
"disabled": "Wyłączone",
"enabled": "Włączone",
"label": "Stan Trigger"
},
"update": "Aktualizuj",
"value": "Wartość",
"workerId": {
@@ -1011,7 +1005,6 @@
"input_custom_duration": "lub wprowadź niestandardowy czas trwania:",
"inputs": "Inputs",
"instance": "Instancja",
"instance_administration": "Administracja Instancji",
"invalid bulk delete": "Nie można usunąć wykonań",
"invalid bulk force run": "Nie można wymusić uruchomienia wykonania",
"invalid bulk kill": "Nie można zabić wykonań",
@@ -1314,7 +1307,7 @@
"next": "Następny",
"no_flows": "Brak flowów w namespace o nazwie tutorial.",
"previous": "Poprzedni",
"skip": "Pomiń Przewodnik po Produkcie",
"skip": "Pomiń samouczek",
"steps": {
"0": {
"content": "Jesteśmy zachwyceni, że jesteś tutaj.<br />Stwórzmy twój pierwszy flow.",
@@ -1744,7 +1737,6 @@
"names": "Najemcy"
},
"tenantId": "Identyfikator Mandanta",
"tenant_administration": "Administracja Mandanta",
"test-badge-text": "Test",
"test-badge-tooltip": "To wykonanie zostało utworzone przez Test.",
"theme": "Motyw",
@@ -1863,7 +1855,7 @@
},
"welcome aboard": "🚀 Witamy w Kestra!",
"welcome aboard content": "Skorzystaj z naszego Przewodnika, aby stworzyć swój pierwszy flow i sprawdź Blueprints, aby znaleźć więcej przykładów.",
"welcome button create": "Rozpocznij Przewodnik po Produkcie",
"welcome button create": "Stwórz mój pierwszy flow",
"welcome display require": "Uruchom swój <strong>pierwszy flow</strong>, aby rozpocząć",
"welcome_page": {
"guide": "Potrzebujesz wskazówek, jak uruchomić swój pierwszy flow?",

View File

@@ -883,12 +883,6 @@
"description": "Filtrar por trigger ID",
"label": "ID do Trigger"
},
"triggerState": {
"description": "Filtrar por estado do trigger",
"disabled": "Desativado",
"enabled": "Habilitado",
"label": "Estado do Trigger"
},
"update": "Atualizar",
"value": "Valor",
"workerId": {
@@ -1011,7 +1005,6 @@
"input_custom_duration": "ou insira uma duração personalizada:",
"inputs": "Inputs",
"instance": "Instância",
"instance_administration": "Administração da Instância",
"invalid bulk delete": "Não foi possível deletar execuções",
"invalid bulk force run": "Não foi possível forçar a execução das execuções",
"invalid bulk kill": "Não foi possível matar execuções",
@@ -1314,7 +1307,7 @@
"next": "Próximo",
"no_flows": "Não há flows disponíveis no namespace do tutorial.",
"previous": "Anterior",
"skip": "Pular Tour do Produto",
"skip": "Pular Tutorial",
"steps": {
"0": {
"content": "Estamos entusiasmados em tê-lo aqui.<br />Vamos criar seu primeiro flow.",
@@ -1744,7 +1737,6 @@
"names": "Mandantes"
},
"tenantId": "ID do Mandante",
"tenant_administration": "Administração do Mandante",
"test-badge-text": "Teste",
"test-badge-tooltip": "Esta execução foi criada por um Teste",
"theme": "Tema",
@@ -1863,7 +1855,7 @@
},
"welcome aboard": "🚀 Bem-vindo ao Kestra!",
"welcome aboard content": "Use nosso Tour Guiado para criar seu primeiro flow e confira os Blueprints para encontrar mais exemplos.",
"welcome button create": "Iniciar Tour do Produto",
"welcome button create": "Criar meu primeiro flow",
"welcome display require": "Execute seu <strong>primeiro flow</strong> para começar",
"welcome_page": {
"guide": "Precisa de orientação para executar seu primeiro flow?",

View File

@@ -883,12 +883,6 @@
"description": "Filtrar por trigger ID",
"label": "ID do Trigger"
},
"triggerState": {
"description": "Filtrar por estado do trigger",
"disabled": "Desativado",
"enabled": "Habilitado",
"label": "Estado do Trigger"
},
"update": "Atualizar",
"value": "Valor",
"workerId": {
@@ -1011,7 +1005,6 @@
"input_custom_duration": "ou insira uma duração personalizada:",
"inputs": "Inputs",
"instance": "Instância",
"instance_administration": "Administração da Instância",
"invalid bulk delete": "Não foi possível excluir execuções",
"invalid bulk force run": "Não foi possível forçar a execução das execuções",
"invalid bulk kill": "Não foi possível matar execuções",
@@ -1314,7 +1307,7 @@
"next": "Próximo",
"no_flows": "Não há flows disponíveis no namespace do tutorial.",
"previous": "Anterior",
"skip": "Pular Tour do Produto",
"skip": "Pular Tutorial",
"steps": {
"0": {
"content": "Estamos entusiasmados em tê-lo aqui.<br />Vamos criar seu primeiro flow.",
@@ -1744,7 +1737,6 @@
"names": "Clientes"
},
"tenantId": "ID do Cliente",
"tenant_administration": "Administração de Tenant",
"test-badge-text": "Teste",
"test-badge-tooltip": "Esta execução foi criada por um Teste",
"theme": "Tema",
@@ -1863,7 +1855,7 @@
},
"welcome aboard": "🚀 Bem-vindo ao Kestra!",
"welcome aboard content": "Use nosso Tour Guiado para criar seu primeiro flow e confira os Blueprints para encontrar mais exemplos.",
"welcome button create": "Iniciar Tour do Produto",
"welcome button create": "Criar meu primeiro flow",
"welcome display require": "Execute seu <strong>primeiro flow</strong> para começar",
"welcome_page": {
"guide": "Precisa de orientação para executar seu primeiro flow?",

View File

@@ -883,12 +883,6 @@
"description": "Фильтр по trigger ID",
"label": "ID триггера"
},
"triggerState": {
"description": "Фильтр по состоянию trigger",
"disabled": "Отключено",
"enabled": "Включено",
"label": "Состояние Trigger"
},
"update": "Обновить",
"value": "Значение",
"workerId": {
@@ -1011,7 +1005,6 @@
"input_custom_duration": "или введите пользовательскую продолжительность:",
"inputs": "Входные данные",
"instance": "Экземпляр",
"instance_administration": "Администрирование экземпляра",
"invalid bulk delete": "Не удалось удалить выполнения",
"invalid bulk force run": "Не удалось принудительно запустить executions",
"invalid bulk kill": "Не удалось убить выполнения",
@@ -1314,7 +1307,7 @@
"next": "Далее",
"no_flows": "Нет доступных flows в namespace учебника.",
"previous": "Назад",
"skip": "Пропустить ознакомительный тур с продуктом",
"skip": "Пропустить учебник",
"steps": {
"0": {
"content": "Мы рады видеть вас здесь.<br />Давайте создадим ваш первый flow.",
@@ -1744,7 +1737,6 @@
"names": "Арендаторы"
},
"tenantId": "ID арендатора",
"tenant_administration": "Администрирование Манданта",
"test-badge-text": "Тест",
"test-badge-tooltip": "Это выполнение было создано тестом",
"theme": "Тема",
@@ -1863,7 +1855,7 @@
},
"welcome aboard": "🚀 Добро пожаловать в Kestra!",
"welcome aboard content": "Используйте наше Руководство, чтобы создать ваш первый flow и ознакомьтесь с Blueprints для поиска дополнительных примеров.",
"welcome button create": "Начать ознакомительный тур с продуктом",
"welcome button create": "Создать мой первый flow",
"welcome display require": "Запустите ваш <strong>первый flow</strong>, чтобы начать",
"welcome_page": {
"guide": "Нужна помощь в выполнении вашего первого flow?",

View File

@@ -883,12 +883,6 @@
"description": "按 trigger ID 筛选",
"label": "触发器 ID"
},
"triggerState": {
"description": "按触发器状态筛选",
"disabled": "禁用",
"enabled": "启用",
"label": "触发状态"
},
"update": "更新",
"value": "值",
"workerId": {
@@ -1011,7 +1005,6 @@
"input_custom_duration": "或输入自定义持续时间:",
"inputs": "输入",
"instance": "实例",
"instance_administration": "实例管理",
"invalid bulk delete": "无法删除执行",
"invalid bulk force run": "无法强制运行执行",
"invalid bulk kill": "无法终止执行",
@@ -1314,7 +1307,7 @@
"next": "下一步",
"no_flows": "教程命名空间下没有可用的流程。",
"previous": "上一步",
"skip": "跳过产品指南",
"skip": "跳过教程",
"steps": {
"0": {
"content": "我们很高兴你在这里。<br />让我们创建你的第一个流程。",
@@ -1744,7 +1737,6 @@
"names": "租户"
},
"tenantId": "租户 ID",
"tenant_administration": "租户管理",
"test-badge-text": "测试",
"test-badge-tooltip": "此执行由测试创建",
"theme": "主题",
@@ -1863,7 +1855,7 @@
},
"welcome aboard": "🚀 欢迎使用 Kestra!",
"welcome aboard content": "使用我们的引导游览来创建你的第一个流程,并查看蓝图以找到更多示例。",
"welcome button create": "开始产品导览",
"welcome button create": "创建我的第一个流程",
"welcome display require": "运行你的 <strong>第一个流程</strong> 以开始",
"welcome_page": {
"guide": "需要指导来执行您的第一个flow吗",

View File

@@ -25,7 +25,7 @@ public class ConcurrencyLimitController {
@ExecuteOn(TaskExecutors.IO)
@Get(uri = "/search")
@Operation(tags = {"Flows"}, summary = "Search for flow concurrency limits")
@Operation(tags = {"Flows", "Executions"}, summary = "Search for flow concurrency limits")
public PagedResults<ConcurrencyLimit> searchConcurrencyLimits() {
var results = concurrencyLimitService.find(tenantService.resolveTenant());
return PagedResults.of(new ArrayListTotal<>(results, results.size()));
@@ -33,7 +33,7 @@ public class ConcurrencyLimitController {
@ExecuteOn(TaskExecutors.IO)
@Put("/{namespace}/{flowId}")
@Operation(tags = {"Flows"}, summary = "Update a flow concurrency limit")
@Operation(tags = {"Flows", "Executions"}, summary = "Update a flow concurrency limit")
public HttpResponse<ConcurrencyLimit> updateConcurrencyLimit(@Body ConcurrencyLimit concurrencyLimit) {
var existing = concurrencyLimitService.findById(concurrencyLimit.getTenantId(), concurrencyLimit.getNamespace(), concurrencyLimit.getFlowId());
if (existing.isEmpty()) {

View File

@@ -28,6 +28,7 @@ import io.kestra.core.serializers.JacksonMapper;
import io.kestra.core.storages.Namespace;
import io.kestra.core.storages.NamespaceFactory;
import io.kestra.core.storages.StorageInterface;
import io.kestra.core.tenant.TenantService;
import io.kestra.core.utils.Await;
import io.kestra.core.utils.IdUtils;
import io.kestra.core.utils.TestsUtils;
@@ -36,6 +37,7 @@ import io.kestra.plugin.core.trigger.Webhook;
import io.kestra.webserver.responses.BulkErrorResponse;
import io.kestra.webserver.responses.BulkResponse;
import io.kestra.webserver.responses.PagedResults;
import io.kestra.webserver.tenants.TenantValidationFilter;
import io.micronaut.context.annotation.Property;
import io.micronaut.core.type.Argument;
import io.micronaut.data.model.Pageable;
@@ -46,12 +48,13 @@ import io.micronaut.http.client.multipart.MultipartBody;
import io.micronaut.http.sse.Event;
import io.micronaut.reactor.http.client.ReactorHttpClient;
import io.micronaut.reactor.http.client.ReactorSseClient;
import io.micronaut.test.annotation.MockBean;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import lombok.extern.slf4j.Slf4j;
import org.awaitility.Awaitility;
import org.hamcrest.Matcher;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
@@ -85,6 +88,8 @@ import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@Slf4j
@KestraTest(startRunner = true)
@@ -130,6 +135,18 @@ class ExecutionControllerRunnerTest {
@Inject
private NamespaceFactory namespaceFactory;
@MockBean(TenantService.class)
public TenantService getTenantService(){
return mock(TenantService.class);
}
@Inject
private TenantService tenantService;
@MockBean(TenantValidationFilter.class)
public TenantValidationFilter getTenantValidationFilter(){
return mock(TenantValidationFilter.class);
}
public static final String TESTS_FLOW_NS = "io.kestra.tests";
public static final String TENANT_ID = "main";
@@ -151,16 +168,17 @@ class ExecutionControllerRunnerTest {
- values""")
.build();
@AfterEach
protected void setup() {
jdbcTestUtils.drop();
jdbcTestUtils.migrate();
@BeforeEach
public void initMock(){
when(tenantService.resolveTenant()).thenReturn(MAIN_TENANT);
}
@Test
@LoadFlows({"flows/valids/inputs.yaml"})
@LoadFlows(value = {"flows/valids/inputs.yaml"}, tenantId = "triggerexecution")
void triggerExecution() {
Execution result = triggerExecutionInputsFlowExecution(false);
String tenantId = "triggerexecution";
when(tenantService.resolveTenant()).thenReturn(tenantId);
Execution result = triggerExecutionInputsFlowExecution(tenantId, false);
assertThat(result.getState().getCurrent()).isEqualTo(State.Type.CREATED);
assertThat(result.getFlowId()).isEqualTo("inputs");
@@ -181,7 +199,7 @@ class ExecutionControllerRunnerTest {
var notFound = assertThrows(HttpClientResponseException.class, () -> client.toBlocking().exchange(
HttpRequest
.POST("/api/v1/main/executions/foo/bar", createExecutionInputsFlowBody())
.POST("/api/v1/%s/executions/foo/bar".formatted(tenantId), createExecutionInputsFlowBody())
.contentType(MediaType.MULTIPART_FORM_DATA_TYPE),
HttpResponse.class
));
@@ -211,8 +229,10 @@ class ExecutionControllerRunnerTest {
}
@Test
@LoadFlows({"flows/valids/inputs-small-files.yaml"})
@LoadFlows(value = {"flows/valids/inputs-small-files.yaml"}, tenantId = "triggerexecutioninputsmall")
void triggerExecutionInputSmall() {
String tenantId = "triggerexecutioninputsmall";
when(tenantService.resolveTenant()).thenReturn(tenantId);
File applicationFile = new File(Objects.requireNonNull(
ExecutionControllerTest.class.getClassLoader().getResource("application-test.yml")
).getPath());
@@ -221,22 +241,24 @@ class ExecutionControllerRunnerTest {
.addPart("files", "f", MediaType.TEXT_PLAIN_TYPE, applicationFile)
.build();
Execution execution = triggerExecutionExecution(TESTS_FLOW_NS, "inputs-small-files", requestBody, true);
Execution execution = triggerExecutionExecution(tenantId, TESTS_FLOW_NS, "inputs-small-files", requestBody, true);
assertThat(execution.getState().getCurrent()).isEqualTo(State.Type.SUCCESS);
assertThat((String) execution.getOutputs().get("o")).startsWith("kestra://");
}
@Test
@LoadFlows({"flows/valids/inputs.yaml"})
@LoadFlows(value = {"flows/valids/inputs.yaml"}, tenantId = "invalidinputs")
void invalidInputs() {
String tenantId = "invalidinputs";
when(tenantService.resolveTenant()).thenReturn(tenantId);
MultipartBody.Builder builder = MultipartBody.builder()
.addPart("validatedString", "B-failed");
inputs.forEach((s, o) -> builder.addPart(s, o instanceof String str ? str : null));
HttpClientResponseException e = assertThrows(
HttpClientResponseException.class,
() -> triggerExecutionExecution(TESTS_FLOW_NS, "inputs", builder.build(), false)
() -> triggerExecutionExecution(tenantId, TESTS_FLOW_NS, "inputs", builder.build(), false)
);
String response = e.getResponse().getBody(String.class).orElseThrow();
@@ -246,22 +268,26 @@ class ExecutionControllerRunnerTest {
}
@Test
@LoadFlows({"flows/valids/inputs.yaml"})
@LoadFlows(value = {"flows/valids/inputs.yaml"}, tenantId = "triggerexecutionandwait")
void triggerExecutionAndWait() {
Execution result = triggerExecutionInputsFlowExecution(true);
String tenantId = "triggerexecutionandwait";
when(tenantService.resolveTenant()).thenReturn(tenantId);
Execution result = triggerExecutionInputsFlowExecution(tenantId, true);
assertThat(result.getState().getCurrent()).isEqualTo(State.Type.SUCCESS);
assertThat(result.getTaskRunList().size()).isEqualTo(16);
}
@Test
@LoadFlows({"flows/valids/inputs.yaml"})
@LoadFlows(value = {"flows/valids/inputs.yaml"}, tenantId = "getexecution")
void getExecution() {
Execution result = triggerExecutionInputsFlowExecution(false);
String tenantId = "getexecution";
when(tenantService.resolveTenant()).thenReturn(tenantId);
Execution result = triggerExecutionInputsFlowExecution(tenantId, false);
// Get the triggered execution by execution id
Execution foundExecution = client.retrieve(
GET("/api/v1/main/executions/" + result.getId()),
GET("/api/v1/%s/executions/".formatted(tenantId) + result.getId()),
Execution.class
).block();
@@ -272,24 +298,26 @@ class ExecutionControllerRunnerTest {
@SuppressWarnings("unchecked")
@Test
@LoadFlows({"flows/valids/minimal-bis.yaml"})
@LoadFlows(value = {"flows/valids/minimal-bis.yaml"}, tenantId = "searchexecutionsbyflowid")
void searchExecutionsByFlowId() throws TimeoutException {
String tenantId = "searchexecutionsbyflowid";
when(tenantService.resolveTenant()).thenReturn(tenantId);
String namespace = "io.kestra.tests.minimal.bis";
String flowId = "minimal-bis";
PagedResults<Execution> executionsBefore = client.toBlocking().retrieve(
GET("/api/v1/main/executions?namespace=" + namespace + "&flowId=" + flowId),
GET("/api/v1/" + tenantId + "/executions?namespace=" + namespace + "&flowId=" + flowId),
Argument.of(PagedResults.class, Execution.class)
);
assertThat(executionsBefore.getTotal()).isEqualTo(0L);
triggerExecutionExecution(namespace, flowId, MultipartBody.builder().addPart("string", "myString").build(), false);
triggerExecutionExecution(tenantId, namespace, flowId, MultipartBody.builder().addPart("string", "myString").build(), false);
// Wait for execution indexation
Await.until(() -> executionRepositoryInterface.findByFlowId(TENANT_ID, namespace, flowId, Pageable.from(1)).size() == 1, Duration.ofMillis(100), Duration.ofMillis(10));
Await.until(() -> executionRepositoryInterface.findByFlowId(tenantId, namespace, flowId, Pageable.from(1)).size() == 1, Duration.ofMillis(100), Duration.ofMillis(10));
PagedResults<Execution> executionsAfter = client.toBlocking().retrieve(
GET("/api/v1/main/executions?namespace=" + namespace + "&flowId=" + flowId),
GET("/api/v1/" + tenantId + "/executions?namespace=" + namespace + "&flowId=" + flowId),
Argument.of(PagedResults.class, Execution.class)
);
@@ -297,12 +325,14 @@ class ExecutionControllerRunnerTest {
}
@Test
@LoadFlows({"flows/valids/inputs.yaml"})
@LoadFlows(value = {"flows/valids/inputs.yaml"}, tenantId = "triggerexecutionandfollowexecution")
void triggerExecutionAndFollowExecution() {
Execution result = triggerExecutionInputsFlowExecution(false);
String tenantId = "triggerexecutionandfollowexecution";
when(tenantService.resolveTenant()).thenReturn(tenantId);
Execution result = triggerExecutionInputsFlowExecution(tenantId, false);
List<Event<Execution>> results = sseClient
.eventStream("/api/v1/main/executions/" + result.getId() + "/follow", Execution.class)
.eventStream("/api/v1/%s/executions/".formatted(tenantId) + result.getId() + "/follow", Execution.class)
.collectList()
.block();
@@ -314,7 +344,7 @@ class ExecutionControllerRunnerTest {
// check that a second call work: calling follow on an already terminated execution.
results = sseClient
.eventStream("/api/v1/main/executions/" + result.getId() + "/follow", Execution.class)
.eventStream("/api/v1/%s/executions/".formatted(tenantId) + result.getId() + "/follow", Execution.class)
.collectList()
.block();
@@ -346,10 +376,12 @@ class ExecutionControllerRunnerTest {
}
@Test
@LoadFlows({"flows/valids/inputs.yaml",
"flows/valids/encrypted-string.yaml"})
@LoadFlows(value = {"flows/valids/inputs.yaml",
"flows/valids/encrypted-string.yaml"}, tenantId = "evaltaskrunexpressionkeepencryptedvalues")
void evalTaskRunExpressionKeepEncryptedValues() throws TimeoutException, QueueException {
Execution execution = runnerUtils.runOne(TENANT_ID, TESTS_FLOW_NS, "encrypted-string");
String tenantId = "evaltaskrunexpressionkeepencryptedvalues";
when(tenantService.resolveTenant()).thenReturn(tenantId);
Execution execution = runnerUtils.runOne(tenantId, TESTS_FLOW_NS, "encrypted-string");
ExecutionController.EvalResult result = this.evalTaskRunExpression(execution, "{{outputs.hello.value}}", 0);
Map<String, Object> resultMap = null;
@@ -361,24 +393,26 @@ class ExecutionControllerRunnerTest {
assertThat(resultMap.get("type")).isEqualTo("io.kestra.datatype:aes_encrypted");
assertThat(resultMap.get("value")).isNotNull();
execution = runnerUtils.runOne(TENANT_ID, TESTS_FLOW_NS, "inputs", null, (flow, execution1) -> flowIO.readExecutionInputs(flow, execution1, inputs));
execution = runnerUtils.runOne(tenantId, TESTS_FLOW_NS, "inputs", null, (flow, execution1) -> flowIO.readExecutionInputs(flow, execution1, inputs));
result = this.evalTaskRunExpression(execution, "{{inputs.secret}}", 0);
assertThat(result.getResult()).isNotEqualTo(inputs.get("secret"));
}
@Test
@LoadFlows({"flows/valids/restart_with_inputs.yaml"})
@LoadFlows(value = {"flows/valids/restart_with_inputs.yaml"}, tenantId = "restartexecutionfromunknowntaskid")
void restartExecutionFromUnknownTaskId() throws TimeoutException, QueueException {
String tenantId = "restartexecutionfromunknowntaskid";
when(tenantService.resolveTenant()).thenReturn(tenantId);
final String flowId = "restart_with_inputs";
final String referenceTaskId = "unknownTaskId";
// Run execution until it ends
Execution parentExecution = runnerUtils.runOne(TENANT_ID, TESTS_FLOW_NS, flowId, null, (flow, execution1) -> flowIO.readExecutionInputs(flow, execution1, inputs));
Execution parentExecution = runnerUtils.runOne(tenantId, TESTS_FLOW_NS, flowId, null, (flow, execution1) -> flowIO.readExecutionInputs(flow, execution1, inputs));
HttpClientResponseException e = assertThrows(HttpClientResponseException.class, () -> client.toBlocking().retrieve(
HttpRequest
.POST("/api/v1/main/executions/" + parentExecution.getId() + "/replay?taskRunId=" + referenceTaskId, ImmutableMap.of()),
.POST("/api/v1/" + tenantId + "/executions/" + parentExecution.getId() + "/replay?taskRunId=" + referenceTaskId, ImmutableMap.of()),
Execution.class
));
@@ -388,16 +422,18 @@ class ExecutionControllerRunnerTest {
}
@Test
@LoadFlows({"flows/valids/restart_with_inputs.yaml"})
@LoadFlows(value = {"flows/valids/restart_with_inputs.yaml"}, tenantId = "restartexecutionwithnofailure")
void restartExecutionWithNoFailure() throws TimeoutException, QueueException{
String tenantId = "restartexecutionwithnofailure";
when(tenantService.resolveTenant()).thenReturn(tenantId);
final String flowId = "restart_with_inputs";
// Run execution until it ends
Execution parentExecution = runnerUtils.runOne(TENANT_ID, TESTS_FLOW_NS, flowId, null, (flow, execution1) -> flowIO.readExecutionInputs(flow, execution1, inputs));
Execution parentExecution = runnerUtils.runOne(tenantId, TESTS_FLOW_NS, flowId, null, (flow, execution1) -> flowIO.readExecutionInputs(flow, execution1, inputs));
HttpClientResponseException e = assertThrows(HttpClientResponseException.class, () -> client.toBlocking().retrieve(
HttpRequest
.POST("/api/v1/main/executions/" + parentExecution.getId() + "/restart", ImmutableMap.of()),
.POST("/api/v1/" + tenantId + "/executions/" + parentExecution.getId() + "/restart", ImmutableMap.of()),
Execution.class
));
@@ -407,15 +443,17 @@ class ExecutionControllerRunnerTest {
}
@Test
@LoadFlows({"flows/valids/restart_with_inputs.yaml"})
@LoadFlows(value = {"flows/valids/restart_with_inputs.yaml"}, tenantId = "restartexecutionfromtaskid")
void restartExecutionFromTaskId() throws Exception {
String tenantId = "restartexecutionfromtaskid";
when(tenantService.resolveTenant()).thenReturn(tenantId);
final String flowId = "restart_with_inputs";
final String referenceTaskId = "instant";
// Run execution until it ends
Execution parentExecution = runnerUtils.runOne(TENANT_ID, TESTS_FLOW_NS, flowId, null, (flow, execution1) -> flowIO.readExecutionInputs(flow, execution1, inputs));
Execution parentExecution = runnerUtils.runOne(tenantId, TESTS_FLOW_NS, flowId, null, (flow, execution1) -> flowIO.readExecutionInputs(flow, execution1, inputs));
Optional<Flow> flow = flowRepositoryInterface.findById(TENANT_ID, TESTS_FLOW_NS, flowId);
Optional<Flow> flow = flowRepositoryInterface.findById(tenantId, TESTS_FLOW_NS, flowId);
assertThat(flow.isPresent()).isTrue();
@@ -427,7 +465,7 @@ class ExecutionControllerRunnerTest {
Execution createdChidExec = client.toBlocking().retrieve(
HttpRequest
.POST("/api/v1/main/executions/" + parentExecution.getId() + "/replay?taskRunId=" + parentExecution.findTaskRunByTaskIdAndValue(referenceTaskId, List.of()).getId(), ImmutableMap.of()),
.POST("/api/v1/" + tenantId + "/executions/" + parentExecution.getId() + "/replay?taskRunId=" + parentExecution.findTaskRunByTaskIdAndValue(referenceTaskId, List.of()).getId(), ImmutableMap.of()),
Execution.class
);
@@ -722,22 +760,24 @@ class ExecutionControllerRunnerTest {
}
@Test
@LoadFlows({"flows/valids/inputs.yaml"})
@LoadFlows(value = {"flows/valids/inputs.yaml"}, tenantId = "downloadinternalstoragefilefromexecution")
void downloadInternalStorageFileFromExecution() throws TimeoutException, QueueException{
Execution execution = runnerUtils.runOne(TENANT_ID, TESTS_FLOW_NS, "inputs", null, (flow, execution1) -> flowIO.readExecutionInputs(flow, execution1, inputs));
String tenantId = "downloadinternalstoragefilefromexecution";
when(tenantService.resolveTenant()).thenReturn(tenantId);
Execution execution = runnerUtils.runOne(tenantId, TESTS_FLOW_NS, "inputs", null, (flow, execution1) -> flowIO.readExecutionInputs(flow, execution1, inputs));
assertThat(execution.getTaskRunList()).hasSize(16);
String path = (String) execution.getInputs().get("file");
String file = client.toBlocking().retrieve(
GET("/api/v1/main/executions/" + execution.getId() + "/file?path=" + path),
GET("/api/v1/" + tenantId + "/executions/" + execution.getId() + "/file?path=" + path),
String.class
);
assertThat(file).isEqualTo("hello");
FileMetas metas = client.retrieve(
GET("/api/v1/main/executions/" + execution.getId() + "/file/metas?path=" + path),
GET("/api/v1/" + tenantId + "/executions/" + execution.getId() + "/file/metas?path=" + path),
FileMetas.class
).block();
@@ -748,7 +788,7 @@ class ExecutionControllerRunnerTest {
String newExecutionId = IdUtils.create();
HttpClientResponseException e = assertThrows(HttpClientResponseException.class, () -> client.toBlocking().retrieve(
GET("/api/v1/main/executions/" + execution.getId() + "/file?path=" + path.replace(execution.getId(),
GET("/api/v1/" + tenantId + "/executions/" + execution.getId() + "/file?path=" + path.replace(execution.getId(),
newExecutionId
)),
String.class
@@ -760,15 +800,17 @@ class ExecutionControllerRunnerTest {
}
@Test
@LoadFlows({"flows/valids/inputs.yaml"})
@LoadFlows(value = {"flows/valids/inputs.yaml"}, tenantId = "previewinternalstoragefilefromexecution")
void previewInternalStorageFileFromExecution() throws TimeoutException, QueueException{
Execution defaultExecution = runnerUtils.runOne(TENANT_ID, TESTS_FLOW_NS, "inputs", null, (flow, execution1) -> flowIO.readExecutionInputs(flow, execution1, inputs));
String tenantId = "previewinternalstoragefilefromexecution";
when(tenantService.resolveTenant()).thenReturn(tenantId);
Execution defaultExecution = runnerUtils.runOne(tenantId, TESTS_FLOW_NS, "inputs", null, (flow, execution1) -> flowIO.readExecutionInputs(flow, execution1, inputs));
assertThat(defaultExecution.getTaskRunList()).hasSize(16);
String defaultPath = (String) defaultExecution.getInputs().get("file");
String defaultFile = client.toBlocking().retrieve(
GET("/api/v1/main/executions/" + defaultExecution.getId() + "/file/preview?path=" + defaultPath),
GET("/api/v1/" + tenantId + "/executions/" + defaultExecution.getId() + "/file/preview?path=" + defaultPath),
String.class
);
@@ -788,20 +830,20 @@ class ExecutionControllerRunnerTest {
.put("yaml1", "{}")
.build();
Execution latin1Execution = runnerUtils.runOne(TENANT_ID, TESTS_FLOW_NS, "inputs", null, (flow, execution1) -> flowIO.readExecutionInputs(flow, execution1, latin1FileInputs));
Execution latin1Execution = runnerUtils.runOne(tenantId, TESTS_FLOW_NS, "inputs", null, (flow, execution1) -> flowIO.readExecutionInputs(flow, execution1, latin1FileInputs));
assertThat(latin1Execution.getTaskRunList()).hasSize(16);
String latin1Path = (String) latin1Execution.getInputs().get("file");
String latin1File = client.toBlocking().retrieve(
GET("/api/v1/main/executions/" + latin1Execution.getId() + "/file/preview?path=" + latin1Path + "&encoding=ISO-8859-1"),
GET("/api/v1/" + tenantId + "/executions/" + latin1Execution.getId() + "/file/preview?path=" + latin1Path + "&encoding=ISO-8859-1"),
String.class
);
assertThat(latin1File).contains("Düsseldorf");
HttpClientResponseException e = assertThrows(HttpClientResponseException.class, () -> client.toBlocking().retrieve(
GET("/api/v1/main/executions/" + latin1Execution.getId() + "/file/preview?path=" + latin1Path + "&encoding=foo"),
GET("/api/v1/" + tenantId + "/executions/" + latin1Execution.getId() + "/file/preview?path=" + latin1Path + "&encoding=foo"),
String.class
));
@@ -810,14 +852,16 @@ class ExecutionControllerRunnerTest {
}
@Test
@LoadFlows({"flows/valids/inputs.yaml"})
@LoadFlows(value = {"flows/valids/inputs.yaml"}, tenantId = "previewlocalfilefromexecution")
void previewLocalFileFromExecution() throws TimeoutException, QueueException, IOException {
String tenantId = "previewlocalfilefromexecution";
when(tenantService.resolveTenant()).thenReturn(tenantId);
HashMap<String, Object> newInputs = new HashMap<>(InputsTest.inputs);
URI file = createFile();
newInputs.put("file", file);
Execution execution = runnerUtils.runOne(
MAIN_TENANT,
tenantId,
"io.kestra.tests",
"inputs",
null,
@@ -827,7 +871,7 @@ class ExecutionControllerRunnerTest {
// get the metadata of the file
FileMetas metas = client.retrieve(
GET("/api/v1/main/executions/" + execution.getId() + "/file/metas?path=" + file),
GET("/api/v1/" + tenantId + "/executions/" + execution.getId() + "/file/metas?path=" + file),
FileMetas.class
).block();
assertThat(metas).isNotNull();
@@ -835,7 +879,7 @@ class ExecutionControllerRunnerTest {
// preview the file
Map<String, Object> preview = client.toBlocking().retrieve(
GET("/api/v1/main/executions/" + execution.getId() + "/file/preview?path=" + file),
GET("/api/v1/" + tenantId + "/executions/" + execution.getId() + "/file/preview?path=" + file),
Map.class
);
assertThat(preview).isNotNull();
@@ -844,21 +888,23 @@ class ExecutionControllerRunnerTest {
// download the file
String content = client.toBlocking().retrieve(
GET("/api/v1/main/executions/" + execution.getId() + "/file?path=" + file),
GET("/api/v1/" + tenantId + "/executions/" + execution.getId() + "/file?path=" + file),
String.class
);
assertThat(content).isEqualTo("Hello World");
}
@Test
@LoadFlows({"flows/valids/inputs.yaml"})
@LoadFlows(value = {"flows/valids/inputs.yaml"})
void previewNsFileFromExecution() throws TimeoutException, QueueException, IOException, URISyntaxException {
String tenantId = MAIN_TENANT;
when(tenantService.resolveTenant()).thenReturn(tenantId);
HashMap<String, Object> newInputs = new HashMap<>(InputsTest.inputs);
URI file = createNsFile(false);
newInputs.put("file", file);
Execution execution = runnerUtils.runOne(
MAIN_TENANT,
tenantId,
"io.kestra.tests",
"inputs",
null,
@@ -868,7 +914,7 @@ class ExecutionControllerRunnerTest {
// get the metadata of the file
FileMetas metas = client.retrieve(
GET("/api/v1/main/executions/" + execution.getId() + "/file/metas?path=" + file),
GET("/api/v1/" + tenantId + "/executions/" + execution.getId() + "/file/metas?path=" + file),
FileMetas.class
).block();
assertThat(metas).isNotNull();
@@ -876,7 +922,7 @@ class ExecutionControllerRunnerTest {
// preview the file
Map<String, Object> preview = client.toBlocking().retrieve(
GET("/api/v1/main/executions/" + execution.getId() + "/file/preview?path=" + file),
GET("/api/v1/" + tenantId + "/executions/" + execution.getId() + "/file/preview?path=" + file),
Map.class
);
assertThat(preview).isNotNull();
@@ -885,7 +931,7 @@ class ExecutionControllerRunnerTest {
// download the file
String content = client.toBlocking().retrieve(
GET("/api/v1/main/executions/" + execution.getId() + "/file?path=" + file),
GET("/api/v1/" + tenantId + "/executions/" + execution.getId() + "/file?path=" + file),
String.class
);
assertThat(content).isEqualTo("Hello World");
@@ -1109,17 +1155,19 @@ class ExecutionControllerRunnerTest {
}
@Test
@LoadFlows({"flows/valids/pause-test.yaml"})
void resumeExecutionByQuery() throws TimeoutException, InterruptedException, QueueException {
Execution pausedExecution1 = runnerUtils.runOneUntilPaused(TENANT_ID, TESTS_FLOW_NS, "pause-test");
Execution pausedExecution2 = runnerUtils.runOneUntilPaused(TENANT_ID, TESTS_FLOW_NS, "pause-test");
@LoadFlows(value = {"flows/valids/pause-test.yaml"}, tenantId = "resumeexecutionbyquery")
void resumeExecutionByQuery() throws TimeoutException, QueueException {
String tenantId = "resumeexecutionbyquery";
when(tenantService.resolveTenant()).thenReturn(tenantId);
Execution pausedExecution1 = runnerUtils.runOneUntilPaused(tenantId, TESTS_FLOW_NS, "pause-test");
Execution pausedExecution2 = runnerUtils.runOneUntilPaused(tenantId, TESTS_FLOW_NS, "pause-test");
assertThat(pausedExecution1.getState().isPaused()).isTrue();
assertThat(pausedExecution2.getState().isPaused()).isTrue();
// resume executions
BulkResponse resumeResponse = client.toBlocking().retrieve(
HttpRequest.POST("/api/v1/main/executions/resume/by-query?namespace=" + TESTS_FLOW_NS, null),
HttpRequest.POST("/api/v1/" + tenantId + "/executions/resume/by-query?namespace=" + TESTS_FLOW_NS, null),
BulkResponse.class
);
assertThat(resumeResponse.getCount()).isEqualTo(2);
@@ -1132,7 +1180,7 @@ class ExecutionControllerRunnerTest {
HttpClientResponseException e = assertThrows(
HttpClientResponseException.class,
() -> client.toBlocking().retrieve(HttpRequest.POST(
"/api/v1/main/executions/resume/by-query?namespace=" + TESTS_FLOW_NS, null
"/api/v1/" + tenantId + "/executions/resume/by-query?namespace=" + TESTS_FLOW_NS, null
))
);
assertThat(e.getStatus().getCode()).isEqualTo(HttpStatus.BAD_REQUEST.getCode());
@@ -1371,32 +1419,34 @@ class ExecutionControllerRunnerTest {
}
@Test
@LoadFlows({"flows/valids/inputs.yaml"})
@LoadFlows(value = {"flows/valids/inputs.yaml"}, tenantId = "searchexecutions")
void searchExecutions() {
String tenantId = "searchexecutions";
when(tenantService.resolveTenant()).thenReturn(tenantId);
PagedResults<?> executions = client.toBlocking().retrieve(
GET("/api/v1/main/executions/search"), PagedResults.class
GET("/api/v1/" + tenantId + "/executions/search"), PagedResults.class
);
assertThat(executions.getTotal()).isEqualTo(0L);
triggerExecutionInputsFlowExecution(false);
triggerExecutionInputsFlowExecution(tenantId, false);
// + is there to simulate that a space was added (this can be the case from UI autocompletion for eg.)
executions = client.toBlocking().retrieve(
GET("/api/v1/main/executions/search?page=1&size=25&filters[labels][EQUALS][url]="+ENCODED_URL_LABEL_VALUE), PagedResults.class
GET("/api/v1/" + tenantId + "/executions/search?page=1&size=25&filters[labels][EQUALS][url]="+ENCODED_URL_LABEL_VALUE), PagedResults.class
);
assertThat(executions.getTotal()).isEqualTo(1L);
executions = client.toBlocking().retrieve(
GET("/api/v1/main/executions/search?page=1&size=25&labels=url:"+ENCODED_URL_LABEL_VALUE), PagedResults.class
GET("/api/v1/" + tenantId + "/executions/search?page=1&size=25&labels=url:"+ENCODED_URL_LABEL_VALUE), PagedResults.class
);
assertThat(executions.getTotal()).isEqualTo(1L);
HttpClientResponseException e = assertThrows(
HttpClientResponseException.class,
() -> client.toBlocking().retrieve(GET("/api/v1/main/executions/search?filters[startDate][EQUALS]=2024-01-07T18:43:11.248%2B01:00&filters[timeRange][EQUALS]=PT12H"))
() -> client.toBlocking().retrieve(GET("/api/v1/" + tenantId + "/executions/search?filters[startDate][EQUALS]=2024-01-07T18:43:11.248%2B01:00&filters[timeRange][EQUALS]=PT12H"))
);
assertThat(e.getStatus().getCode()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY.getCode());
@@ -1404,39 +1454,33 @@ class ExecutionControllerRunnerTest {
assertThat(e.getResponse().getBody(String.class).get()).contains("are mutually exclusive");
executions = client.toBlocking().retrieve(
GET("/api/v1/main/executions/search?filters[timeRange][EQUALS]=PT12H"), PagedResults.class
GET("/api/v1/" + tenantId + "/executions/search?filters[timeRange][EQUALS]=PT12H"), PagedResults.class
);
assertThat(executions.getTotal()).isEqualTo(1L);
executions = client.toBlocking().retrieve(
GET("/api/v1/main/executions/search?timeRange=PT12H"), PagedResults.class
GET("/api/v1/" + tenantId + "/executions/search?timeRange=PT12H"), PagedResults.class
);
assertThat(executions.getTotal()).isEqualTo(1L);
e = assertThrows(
HttpClientResponseException.class,
() -> client.toBlocking().retrieve(GET("/api/v1/main/executions/search?filters[timeRange][EQUALS]=P1Y"))
() -> client.toBlocking().retrieve(GET("/api/v1/" + tenantId + "/executions/search?filters[timeRange][EQUALS]=P1Y"))
);
assertThat(e.getStatus().getCode()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY.getCode());
e = assertThrows(
HttpClientResponseException.class,
() -> client.toBlocking().retrieve(GET("/api/v1/main/executions/search?timeRange=P1Y"))
);
assertThat(e.getStatus().getCode()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY.getCode());
e = assertThrows(
HttpClientResponseException.class,
() -> client.toBlocking().retrieve(GET("/api/v1/main/executions/search?page=1&size=-1"))
() -> client.toBlocking().retrieve(GET("/api/v1/" + tenantId + "/executions/search?page=1&size=-1"))
);
assertThat(e.getStatus().getCode()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY.getCode());
e = assertThrows(
HttpClientResponseException.class,
() -> client.toBlocking().retrieve(GET("/api/v1/main/executions/search?page=0"))
() -> client.toBlocking().retrieve(GET("/api/v1/" + tenantId + "/executions/search?page=0"))
);
assertThat(e.getStatus().getCode()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY.getCode());
@@ -1537,14 +1581,16 @@ class ExecutionControllerRunnerTest {
}
@Test
@LoadFlows({"flows/valids/minimal.yaml"})
@LoadFlows(value = {"flows/valids/minimal.yaml"}, tenantId = "setlabelsonterminatedexecutionsbyquery")
void setLabelsOnTerminatedExecutionsByQuery() throws TimeoutException, QueueException {
Execution result1 = runnerUtils.runOne(TENANT_ID, TESTS_FLOW_NS, "minimal");
Execution result2 = runnerUtils.runOne(TENANT_ID, TESTS_FLOW_NS, "minimal");
Execution result3 = runnerUtils.runOne(TENANT_ID, TESTS_FLOW_NS, "minimal");
String tenantId = "setlabelsonterminatedexecutionsbyquery";
when(tenantService.resolveTenant()).thenReturn(tenantId);
Execution result1 = runnerUtils.runOne(tenantId, TESTS_FLOW_NS, "minimal");
Execution result2 = runnerUtils.runOne(tenantId, TESTS_FLOW_NS, "minimal");
Execution result3 = runnerUtils.runOne(tenantId, TESTS_FLOW_NS, "minimal");
BulkResponse response = client.toBlocking().retrieve(
HttpRequest.POST("/api/v1/main/executions/labels/by-query?namespace=" + result1.getNamespace(),
HttpRequest.POST("/api/v1/" + tenantId + "/executions/labels/by-query?namespace=" + result1.getNamespace(),
List.of(new Label("key", "value"))
),
BulkResponse.class
@@ -1555,7 +1601,7 @@ class ExecutionControllerRunnerTest {
var exception = assertThrows(
HttpClientResponseException.class,
() -> client.toBlocking().exchange(HttpRequest.POST(
"/api/v1/main/executions/labels/by-query?namespace=" + result1.getNamespace(),
"/api/v1/" + tenantId + "/executions/labels/by-query?namespace=" + result1.getNamespace(),
List.of(new Label(null, null)))
)
);
@@ -1753,14 +1799,16 @@ class ExecutionControllerRunnerTest {
}
@Test
@LoadFlows({"flows/valids/flow-concurrency-queue.yml",
"flows/valids/minimal.yaml"})
@LoadFlows(value = {"flows/valids/flow-concurrency-queue.yml",
"flows/valids/minimal.yaml"}, tenantId = "shouldunqueueexecutionaqueuedflow")
void shouldUnqueueExecutionAQueuedFlow() throws QueueException, TimeoutException {
String tenantId = "shouldunqueueexecutionaqueuedflow";
when(tenantService.resolveTenant()).thenReturn(tenantId);
// 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");
runnerUtils.runOneUntilRunning(tenantId, TESTS_FLOW_NS, "flow-concurrency-queue");
Execution result = runUntilQueued(tenantId, TESTS_FLOW_NS, "flow-concurrency-queue");
var response = client.toBlocking().exchange(HttpRequest.POST("/api/v1/main/executions/" + result.getId() + "/unqueue", null));
var response = client.toBlocking().exchange(HttpRequest.POST("/api/v1/" + tenantId + "/executions/" + result.getId() + "/unqueue", null));
assertThat(response.getStatus().getCode()).isEqualTo(HttpStatus.OK.getCode());
// waiting for the flow to complete successfully
@@ -1771,60 +1819,66 @@ class ExecutionControllerRunnerTest {
);
var notFound = assertThrows(HttpClientResponseException.class, () -> client.toBlocking().exchange(HttpRequest.POST("/api/v1/main/executions/notfound/unqueue", null)));
var notFound = assertThrows(HttpClientResponseException.class, () -> client.toBlocking().exchange(HttpRequest.POST("/api/v1/" + tenantId + "/executions/notfound/unqueue", null)));
assertThat(notFound.getStatus().getCode()).isEqualTo(HttpStatus.NOT_FOUND.getCode());
// pausing an already completed flow will result in errors
Execution completed = runnerUtils.runOne(TENANT_ID, TESTS_FLOW_NS, "minimal");
Execution completed = runnerUtils.runOne(tenantId, TESTS_FLOW_NS, "minimal");
var notRunning = assertThrows(HttpClientResponseException.class, () -> client.toBlocking().exchange(HttpRequest.POST("/api/v1/main/executions/" + completed.getId() + "/unqueue", null)));
var notRunning = assertThrows(HttpClientResponseException.class, () -> client.toBlocking().exchange(HttpRequest.POST("/api/v1/" + tenantId + "/executions/" + completed.getId() + "/unqueue", null)));
assertThat(notRunning.getStatus().getCode()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY.getCode());
}
@Test
@LoadFlows({"flows/valids/flow-concurrency-queue.yml",
"flows/valids/minimal.yaml"})
@LoadFlows(value = {"flows/valids/flow-concurrency-queue.yml",
"flows/valids/minimal.yaml"}, tenantId = "shouldunqueueaqueuedflowtocancelledstate")
void shouldUnqueueAQueuedFlowToCancelledState() throws QueueException, TimeoutException {
String tenantId = "shouldunqueueaqueuedflowtocancelledstate";
when(tenantService.resolveTenant()).thenReturn(tenantId);
// 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");
runnerUtils.runOneUntilRunning(tenantId, "io.kestra.tests", "flow-concurrency-queue");
Execution result1 = runUntilQueued(tenantId, "io.kestra.tests", "flow-concurrency-queue");
var cancelResponse = client.toBlocking().exchange(
HttpRequest.POST("/api/v1/executions/" + result1.getId() + "/unqueue?state=CANCELLED", null)
HttpRequest.POST("/api/v1/" + tenantId + "/executions/" + result1.getId() + "/unqueue?state=CANCELLED", null)
);
assertThat(cancelResponse.getStatus().getCode()).isEqualTo(HttpStatus.OK.getCode());
Optional<Execution> cancelledExecution = executionRepositoryInterface.findById(TENANT_ID, result1.getId());
Optional<Execution> cancelledExecution = executionRepositoryInterface.findById(tenantId, result1.getId());
assertThat(cancelledExecution.isPresent()).isTrue();
assertThat(cancelledExecution.get().getState().getCurrent()).isEqualTo(State.Type.CANCELLED);
}
@Test
@LoadFlows({"flows/valids/flow-concurrency-queue.yml"})
@LoadFlows(value = {"flows/valids/flow-concurrency-queue.yml"}, tenantId = "shouldunqueueexecutionbyidsqueuedflows")
void shouldUnqueueExecutionByIdsQueuedFlows() throws TimeoutException, QueueException {
String tenantId = "shouldunqueueexecutionbyidsqueuedflows";
when(tenantService.resolveTenant()).thenReturn(tenantId);
// 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");
runnerUtils.runOneUntilRunning(tenantId, TESTS_FLOW_NS, "flow-concurrency-queue");
Execution result1 = runUntilQueued(tenantId, TESTS_FLOW_NS, "flow-concurrency-queue");
Execution result2 = runUntilQueued(tenantId, TESTS_FLOW_NS, "flow-concurrency-queue");
Execution result3 = runUntilQueued(tenantId, TESTS_FLOW_NS, "flow-concurrency-queue");
BulkResponse response = client.toBlocking().retrieve(
HttpRequest.POST("/api/v1/main/executions/unqueue/by-ids", List.of(result1.getId(), result2.getId(), result3.getId())),
HttpRequest.POST("/api/v1/" + tenantId + "/executions/unqueue/by-ids", List.of(result1.getId(), result2.getId(), result3.getId())),
BulkResponse.class
);
assertThat(response.getCount()).isEqualTo(3);
}
@Test
@LoadFlows({"flows/valids/flow-concurrency-queue.yml"})
@LoadFlows(value = {"flows/valids/flow-concurrency-queue.yml"}, tenantId = "shouldforcerunexecutionaqueuedflow")
void shouldForceRunExecutionAQueuedFlow() throws QueueException, TimeoutException {
String tenantId = "shouldforcerunexecutionaqueuedflow";
when(tenantService.resolveTenant()).thenReturn(tenantId);
// 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");
runnerUtils.runOneUntilRunning(tenantId, TESTS_FLOW_NS, "flow-concurrency-queue");
Execution result = runUntilQueued(tenantId, TESTS_FLOW_NS, "flow-concurrency-queue");
var response = client.toBlocking().exchange(HttpRequest.POST("/api/v1/main/executions/" + result.getId() + "/force-run", null));
var response = client.toBlocking().exchange(HttpRequest.POST("/api/v1/" + tenantId + "/executions/" + result.getId() + "/force-run", null));
assertThat(response.getStatus().getCode()).isEqualTo(HttpStatus.OK.getCode());
Optional<Execution> forcedRun = executionRepositoryInterface.findById(TENANT_ID, result.getId());
Optional<Execution> forcedRun = executionRepositoryInterface.findById(tenantId, result.getId());
assertThat(forcedRun.isPresent()).isTrue();
assertThat(forcedRun.get().getState().getCurrent()).isNotEqualTo(State.Type.QUEUED);
@@ -1850,14 +1904,16 @@ class ExecutionControllerRunnerTest {
}
@Test
@LoadFlows({"flows/valids/minimal.yaml"})
void shouldForceRunExecutionACreatedFlow() throws QueueException, TimeoutException {
Execution result = this.createExecution(TESTS_FLOW_NS, "minimal");
@LoadFlows(value = {"flows/valids/minimal.yaml"}, tenantId = "shouldforcerunexecutionacreatedflow")
void shouldForceRunExecutionACreatedFlow() throws QueueException {
String tenantId = "shouldforcerunexecutionacreatedflow";
when(tenantService.resolveTenant()).thenReturn(tenantId);
Execution result = this.createExecution(tenantId, TESTS_FLOW_NS, "minimal");
this.executionQueue.emit(result);
var response = client.toBlocking().exchange(HttpRequest.POST("/api/v1/main/executions/" + result.getId() + "/force-run", null));
var response = client.toBlocking().exchange(HttpRequest.POST("/api/v1/" + tenantId + "/executions/" + result.getId() + "/force-run", null));
assertThat(response.getStatus().getCode()).isEqualTo(HttpStatus.OK.getCode());
Optional<Execution> forcedRun = executionRepositoryInterface.findById(TENANT_ID, result.getId());
Optional<Execution> forcedRun = executionRepositoryInterface.findById(tenantId, result.getId());
assertThat(forcedRun.isPresent()).isTrue();
assertThat(forcedRun.get().getState().getCurrent()).isNotEqualTo(State.Type.CREATED);
}
@@ -1978,7 +2034,7 @@ class ExecutionControllerRunnerTest {
return client.toBlocking().retrieve(
HttpRequest
.POST(
"/api/v1/main/executions/" + execution.getId() + "/eval/" + execution.getTaskRunList().get(index).getId(),
"/api/v1/" + execution.getTenantId() + "/executions/" + execution.getId() + "/eval/" + execution.getTaskRunList().get(index).getId(),
expression
)
.contentType(MediaType.TEXT_PLAIN_TYPE),
@@ -1987,23 +2043,23 @@ class ExecutionControllerRunnerTest {
}
private Execution triggerExecutionExecution(String namespace, String flowId, MultipartBody requestBody, Boolean wait) {
return triggerExecutionExecution(namespace, flowId, requestBody, wait, null);
private Execution triggerExecutionExecution(String tenantId, String namespace, String flowId, MultipartBody requestBody, Boolean wait) {
return triggerExecutionExecution(tenantId, namespace, flowId, requestBody, wait, null);
}
private Execution triggerExecutionExecution(String namespace, String flowId, MultipartBody requestBody, Boolean wait, String breakpoint) {
private Execution triggerExecutionExecution(String tenantId, String namespace, String flowId, MultipartBody requestBody, Boolean wait, String breakpoint) {
return client.toBlocking().retrieve(
HttpRequest
.POST("/api/v1/main/executions/" + namespace + "/" + flowId + "?labels=a:label-1&labels=b:label-2&labels=url:" + ENCODED_URL_LABEL_VALUE + (wait ? "&wait=true" : "") + (breakpoint != null ? "&breakpoints=" + breakpoint : ""), requestBody)
.POST("/api/v1/" + tenantId + "/executions/" + namespace + "/" + flowId + "?labels=a:label-1&labels=b:label-2&labels=url:" + ENCODED_URL_LABEL_VALUE + (wait ? "&wait=true" : "") + (breakpoint != null ? "&breakpoints=" + breakpoint : ""), requestBody)
.contentType(MediaType.MULTIPART_FORM_DATA_TYPE),
Execution.class
);
}
private Execution triggerExecutionInputsFlowExecution(Boolean wait) {
private Execution triggerExecutionInputsFlowExecution(String tenantId, Boolean wait) {
MultipartBody requestBody = createExecutionInputsFlowBody();
return triggerExecutionExecution(TESTS_FLOW_NS, "inputs", requestBody, wait);
return triggerExecutionExecution(tenantId, TESTS_FLOW_NS, "inputs", requestBody, wait);
}
private MultipartBody createExecutionInputsFlowBody() {
@@ -2031,17 +2087,17 @@ class ExecutionControllerRunnerTest {
.build();
}
private Execution runUntilQueued(String namespace, String flowId) throws TimeoutException, QueueException {
return runUntilState(namespace, flowId, State.Type.QUEUED);
private Execution runUntilQueued(String tenantId, String namespace, String flowId) throws TimeoutException, QueueException {
return runUntilState(tenantId, namespace, flowId, State.Type.QUEUED);
}
private Execution createExecution(String namespace, String flowId) {
Flow flow = flowRepositoryInterface.findById(TENANT_ID, namespace, flowId).orElseThrow();
private Execution createExecution(String tenantId, String namespace, String flowId) {
Flow flow = flowRepositoryInterface.findById(tenantId, 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);
private Execution runUntilState(String tenantId, String namespace, String flowId, State.Type state) throws TimeoutException, QueueException {
Execution execution = this.createExecution(tenantId, namespace, flowId);
return runnerUtils.awaitExecution(
it -> execution.getId().equals(it.getId()) && it.getState().getCurrent() == state,
throwRunnable(() -> this.executionQueue.emit(execution)),
@@ -2086,14 +2142,16 @@ class ExecutionControllerRunnerTest {
}
@Test
@LoadFlows({"flows/valids/minimal.yaml"})
@LoadFlows(value = {"flows/valids/minimal.yaml"}, tenantId = "shouldnotallowaddingsystemlabels")
void shouldNotAllowAddingSystemLabels() throws QueueException, TimeoutException {
Execution result = runnerUtils.runOne(TENANT_ID, TESTS_FLOW_NS, "minimal");
String tenantId = "shouldnotallowaddingsystemlabels";
when(tenantService.resolveTenant()).thenReturn(tenantId);
Execution result = runnerUtils.runOne(tenantId, TESTS_FLOW_NS, "minimal");
assertThat(result.getState().getCurrent()).isEqualTo(State.Type.SUCCESS);
List<Label> systemLabels = List.of(new Label("system.key", "system-value"));
HttpClientResponseException e = assertThrows(HttpClientResponseException.class, () -> client.toBlocking().retrieve(
HttpRequest.POST("/api/v1/main/executions/" + result.getId() + "/labels", systemLabels),
HttpRequest.POST("/api/v1/" + tenantId + "/executions/" + result.getId() + "/labels", systemLabels),
Execution.class
));
@@ -2102,9 +2160,11 @@ class ExecutionControllerRunnerTest {
}
@Test
@LoadFlows({"flows/valids/minimal.yaml"})
void shouldSuspendAtBreakpointThenResume() throws QueueException, TimeoutException, InterruptedException {
Execution execution = triggerExecutionExecution(TESTS_FLOW_NS, "minimal", null, false, "date");
@LoadFlows(value = {"flows/valids/minimal.yaml"}, tenantId = "shouldsuspendatbreakpointthenresume")
void shouldSuspendAtBreakpointThenResume() throws TimeoutException {
String tenantId = "shouldsuspendatbreakpointthenresume";
when(tenantService.resolveTenant()).thenReturn(tenantId);
Execution execution = triggerExecutionExecution(tenantId, TESTS_FLOW_NS, "minimal", null, false, "date");
assertThat(execution).isNotNull();
assertThat(execution.getState().getCurrent()).isEqualTo(State.Type.CREATED);
@@ -2115,7 +2175,7 @@ class ExecutionControllerRunnerTest {
// resume the suspended execution
HttpResponse<Void> resume = client.toBlocking().exchange(
HttpRequest.POST("/api/v1/main/executions/" + suspended.getId() + "/resume-from-breakpoint", null),
HttpRequest.POST("/api/v1/" + tenantId + "/executions/" + suspended.getId() + "/resume-from-breakpoint", null),
Void.class
);
assertThat(resume.getStatus().getCode()).isEqualTo(HttpStatus.OK.getCode());
@@ -2132,15 +2192,20 @@ class ExecutionControllerRunnerTest {
@FlakyTest
@Test
@LoadFlows({"flows/valids/subflow-parent.yaml", "flows/valids/subflow-child.yaml", "flows/valids/subflow-grand-child.yaml"})
@LoadFlows(value = {"flows/valids/subflow-parent.yaml",
"flows/valids/subflow-child.yaml",
"flows/valids/subflow-grand-child.yaml"},
tenantId = "triggerexecutionandfollowdependencies")
void triggerExecutionAndFollowDependencies() throws InterruptedException {
Execution result = triggerExecutionExecution(TESTS_FLOW_NS, "subflow-parent", null, true);
String tenantId = "triggerexecutionandfollowdependencies";
when(tenantService.resolveTenant()).thenReturn(tenantId);
Execution result = triggerExecutionExecution(tenantId, TESTS_FLOW_NS, "subflow-parent", null, true);
// without this slight delay, the event stream may miss some 'end' events
Thread.sleep(500);
List<Event<ExecutionStatusEvent>> results = sseClient
.eventStream("/api/v1/main/executions/" + result.getId() + "/follow-dependencies?expandAll=true", ExecutionStatusEvent.class)
.eventStream("/api/v1/" + tenantId + "/executions/" + result.getId() + "/follow-dependencies?expandAll=true", ExecutionStatusEvent.class)
.collectList()
.block();
@@ -2154,7 +2219,7 @@ class ExecutionControllerRunnerTest {
// check that a second call work: calling follow on an already terminated execution.
results = sseClient
.eventStream("/api/v1/main/executions/" + result.getId() + "/follow-dependencies?expandAll=true", ExecutionStatusEvent.class)
.eventStream("/api/v1/" + tenantId + "/executions/" + result.getId() + "/follow-dependencies?expandAll=true", ExecutionStatusEvent.class)
.collectList()
.block();
@@ -2168,7 +2233,7 @@ class ExecutionControllerRunnerTest {
// check that a without expandAll it would return only the immediate dependencies.
results = sseClient
.eventStream("/api/v1/main/executions/" + result.getId() + "/follow-dependencies", ExecutionStatusEvent.class)
.eventStream("/api/v1/" + tenantId + "/executions/" + result.getId() + "/follow-dependencies", ExecutionStatusEvent.class)
.collectList()
.block();

View File

@@ -2,6 +2,7 @@ package io.kestra.webserver.controllers.api;
import com.google.common.collect.ImmutableMap;
import io.kestra.core.junit.annotations.KestraTest;
import io.kestra.core.junit.annotations.LoadFlows;
import io.kestra.core.models.Label;
import io.kestra.core.models.executions.Execution;
import io.kestra.core.models.flows.Flow;
@@ -10,7 +11,6 @@ import io.kestra.core.models.property.Property;
import io.kestra.core.models.tasks.TaskForExecution;
import io.kestra.core.models.triggers.AbstractTriggerForExecution;
import io.kestra.core.repositories.LocalFlowRepositoryLoader;
import io.kestra.core.utils.TestsUtils;
import io.kestra.jdbc.JdbcTestUtils;
import io.kestra.plugin.core.debug.Return;
import io.kestra.webserver.responses.BulkResponse;
@@ -22,10 +22,8 @@ import io.micronaut.http.client.exceptions.HttpClientResponseException;
import io.micronaut.http.client.multipart.MultipartBody;
import io.micronaut.reactor.http.client.ReactorHttpClient;
import jakarta.inject.Inject;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
@@ -68,15 +66,6 @@ class ExecutionControllerTest {
public static final String TESTS_FLOW_NS = "io.kestra.tests";
public static final String TESTS_WEBHOOK_KEY = "a-secret-key";
@SneakyThrows
@BeforeEach
protected void setup() {
jdbcTestUtils.drop();
jdbcTestUtils.migrate();
TestsUtils.loads(MAIN_TENANT, repositoryLoader);
}
@Test
void getExecutionNotFound() {
HttpClientResponseException e = assertThrows(
@@ -177,6 +166,7 @@ class ExecutionControllerTest {
}
@Test
@LoadFlows(value = {"flows/valids/webhook-dynamic-key.yaml"})
void webhookDynamicKey() {
Execution execution = client.toBlocking().retrieve(
GET(
@@ -190,6 +180,7 @@ class ExecutionControllerTest {
}
@Test
@LoadFlows(value = {"flows/valids/webhook-secret-key.yaml"})
@EnabledIfEnvironmentVariable(named = "SECRET_WEBHOOK_KEY", matches = ".*")
void webhookDynamicKeyFromASecret() {
Execution execution = client.toBlocking().retrieve(
@@ -204,6 +195,7 @@ class ExecutionControllerTest {
}
@Test
@LoadFlows(value = {"flows/valids/webhook-with-condition.yaml"})
void webhookWithCondition() {
record Hello(String hello) {}
@@ -232,6 +224,7 @@ class ExecutionControllerTest {
}
@Test
@LoadFlows(value = {"flows/valids/webhook-inputs.yaml"})
void webhookWithInputs() {
record Hello(String hello) {}
@@ -289,6 +282,7 @@ class ExecutionControllerTest {
@SuppressWarnings("DataFlowIssue")
@Test
@LoadFlows(value = {"flows/valids/full.yaml"})
void getExecutionFlowForExecution() {
FlowForExecution result = client.toBlocking().retrieve(
GET("/api/v1/main/executions/flows/io.kestra.tests/full"),
@@ -301,6 +295,7 @@ class ExecutionControllerTest {
}
@Test
@LoadFlows(value = {"flows/valids/full.yaml"})
void getExecutionFlowForExecutionWithOldUrl() {
FlowForExecution result = client.toBlocking().retrieve(
GET("/api/v1/main/executions/flows/io.kestra.tests/full"),
@@ -314,6 +309,7 @@ class ExecutionControllerTest {
@SuppressWarnings("DataFlowIssue")
@Test
@LoadFlows(value = {"flows/valids/webhook.yaml"})
void getExecutionFlowForExecutionById() {
Execution execution = client.toBlocking().retrieve(
HttpRequest
@@ -336,24 +332,26 @@ class ExecutionControllerTest {
@SuppressWarnings("unchecked")
@Test
@LoadFlows(value = {"flows/valids/minimal.yaml"})
void getExecutionDistinctNamespaceExecutables() {
List<String> result = client.toBlocking().retrieve(
GET("/api/v1/main/executions/namespaces"),
Argument.of(List.class, String.class)
);
assertThat(result.size()).isGreaterThanOrEqualTo(5);
assertThat(result.size()).isGreaterThanOrEqualTo(1);
}
@SuppressWarnings("unchecked")
@Test
@LoadFlows(value = {"flows/valids/webhook.yaml", "flows/valids/minimal.yaml"})
void getExecutionFlowFromNamespace() {
List<FlowForExecution> result = client.toBlocking().retrieve(
GET("/api/v1/main/executions/namespaces/io.kestra.tests/flows"),
Argument.of(List.class, FlowForExecution.class)
);
assertThat(result.size()).isGreaterThan(100);
assertThat(result.size()).isGreaterThanOrEqualTo(2);
}
@Test
@@ -378,6 +376,7 @@ class ExecutionControllerTest {
}
@Test
@LoadFlows(value = {"flows/valids/inputs.yaml"})
void commaInSingleLabelsValue() {
String encodedCommaWithinLabel = URLEncoder.encode("project:foo,bar", StandardCharsets.UTF_8);
@@ -448,6 +447,7 @@ class ExecutionControllerTest {
}
@Test
@LoadFlows(value = {"flows/valids/minimal.yaml"})
void scheduleDate() {
// given
ZonedDateTime now = ZonedDateTime.now().truncatedTo(ChronoUnit.SECONDS).plusSeconds(1);
@@ -489,6 +489,7 @@ class ExecutionControllerTest {
}
@Test
@LoadFlows(value = {"flows/valids/minimal.yaml"})
void shouldHaveAnUrlWhenCreated() {
// ExecutionController.ExecutionResponse cannot be deserialized because it didn't have any default constructor.
// adding it would mean updating the Execution itself, which is too annoying, so for the test we just deserialize to a Map.
@@ -504,6 +505,7 @@ class ExecutionControllerTest {
}
@Test
@LoadFlows(value = {"flows/valids/minimal.yaml"})
void shouldRefuseSystemLabelsWhenCreatingAnExecution() {
var error = assertThrows(HttpClientResponseException.class, () -> client.toBlocking().retrieve(
HttpRequest

View File

@@ -167,7 +167,7 @@ class PluginControllerTest {
Argument.mapOf(String.class, Object.class)
);
assertThat((Map<String, Object>) doc.get("properties")).hasSize(23);
assertThat((Map<String, Object>) doc.get("properties")).hasSize(24);
assertThat((List<String>) doc.get("required")).hasSize(3);
}