Compare commits

...

21 Commits

Author SHA1 Message Date
nKwiatkowski
4c8a21c2e3 fix(tests): #4852 fix kafka executor test pause timeout 2025-08-29 09:53:36 +02:00
nKwiatkowski
17e61bf687 fix(tests): #4852 fix unit tests 2025-08-28 18:17:49 +02:00
dependabot[bot]
c4022d2e3c build(deps): bump flyingSaucerVersion from 9.13.2 to 9.13.3
Bumps `flyingSaucerVersion` from 9.13.2 to 9.13.3.

Updates `org.xhtmlrenderer:flying-saucer-core` from 9.13.2 to 9.13.3
- [Release notes](https://github.com/flyingsaucerproject/flyingsaucer/releases)
- [Changelog](https://github.com/flyingsaucerproject/flyingsaucer/blob/main/CHANGELOG.md)
- [Commits](https://github.com/flyingsaucerproject/flyingsaucer/compare/v9.13.2...v9.13.3)

Updates `org.xhtmlrenderer:flying-saucer-pdf` from 9.13.2 to 9.13.3
- [Release notes](https://github.com/flyingsaucerproject/flyingsaucer/releases)
- [Changelog](https://github.com/flyingsaucerproject/flyingsaucer/blob/main/CHANGELOG.md)
- [Commits](https://github.com/flyingsaucerproject/flyingsaucer/compare/v9.13.2...v9.13.3)

---
updated-dependencies:
- dependency-name: org.xhtmlrenderer:flying-saucer-core
  dependency-version: 9.13.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.xhtmlrenderer:flying-saucer-pdf
  dependency-version: 9.13.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-28 16:10:35 +02:00
dependabot[bot]
ee48865706 build(deps): bump software.amazon.awssdk:bom from 2.32.26 to 2.32.31
Bumps software.amazon.awssdk:bom from 2.32.26 to 2.32.31.

---
updated-dependencies:
- dependency-name: software.amazon.awssdk:bom
  dependency-version: 2.32.31
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-28 14:34:54 +02:00
dependabot[bot]
f7a23ae459 build(deps): bump org.jsoup:jsoup from 1.21.1 to 1.21.2
Bumps [org.jsoup:jsoup](https://github.com/jhy/jsoup) from 1.21.1 to 1.21.2.
- [Release notes](https://github.com/jhy/jsoup/releases)
- [Changelog](https://github.com/jhy/jsoup/blob/master/CHANGES.md)
- [Commits](https://github.com/jhy/jsoup/compare/jsoup-1.21.1...jsoup-1.21.2)

---
updated-dependencies:
- dependency-name: org.jsoup:jsoup
  dependency-version: 1.21.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-28 14:33:28 +02:00
Miloš Paunović
a13909337e feat(namespaces): introduce inherited key/value pairs drawer (#10967)
Closes https://github.com/kestra-io/kestra-ee/issues/2830.
2025-08-28 13:06:10 +02:00
Piyush Bhaskar
502f0362e3 fix(flows): properly delete task from topology to reflect everywhere (#10924)
Co-authored-by: Miloš Paunović <paun992@hotmail.com>
2025-08-28 16:17:18 +05:30
brian-mulier-p
dbaa35370f fix(plugins): hide "apps", "appBlocks", "charts", "dataFilters", "dataFiltersKPI" types in Plugins page (#10965)
closes #10464
2025-08-28 11:36:50 +02:00
dependabot[bot]
59a93b2ab9 build(deps): bump com.github.ksuid:ksuid from 1.1.3 to 1.1.4
Bumps [com.github.ksuid:ksuid](https://github.com/ksuid/ksuid) from 1.1.3 to 1.1.4.
- [Release notes](https://github.com/ksuid/ksuid/releases)
- [Commits](https://github.com/ksuid/ksuid/compare/ksuid-1.1.3...ksuid-1.1.4)

---
updated-dependencies:
- dependency-name: com.github.ksuid:ksuid
  dependency-version: 1.1.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-28 11:03:27 +02:00
dependabot[bot]
bff8026ebb build(deps): bump actions/setup-java from 4 to 5
Bumps [actions/setup-java](https://github.com/actions/setup-java) from 4 to 5.
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](https://github.com/actions/setup-java/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-java
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-28 11:02:21 +02:00
dependabot[bot]
4481318023 build(deps): bump jakarta.mail:jakarta.mail-api from 2.1.3 to 2.1.4
Bumps [jakarta.mail:jakarta.mail-api](https://github.com/jakartaee/mail-api) from 2.1.3 to 2.1.4.
- [Release notes](https://github.com/jakartaee/mail-api/releases)
- [Commits](https://github.com/jakartaee/mail-api/compare/2.1.3...2.1.4)

---
updated-dependencies:
- dependency-name: jakarta.mail:jakarta.mail-api
  dependency-version: 2.1.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-28 10:59:56 +02:00
dependabot[bot]
c8b33dd690 build(deps): bump net.thisptr:jackson-jq from 1.4.0 to 1.5.0
Bumps [net.thisptr:jackson-jq](https://github.com/eiiches/jackson-jq) from 1.4.0 to 1.5.0.
- [Release notes](https://github.com/eiiches/jackson-jq/releases)
- [Commits](https://github.com/eiiches/jackson-jq/compare/1.4.0...1.5.0)

---
updated-dependencies:
- dependency-name: net.thisptr:jackson-jq
  dependency-version: 1.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-28 10:58:36 +02:00
Nicolas K.
05b485e6cc feat(API): add a new endpoint to replay and execution / task with new… (#10868)
* feat(API): add a new endpoint to replay and execution / task with new inputs

* clean(API): code review

---------

Co-authored-by: nKwiatkowski <nkwiatkowski@kestra.io>
2025-08-28 10:35:39 +02:00
brian-mulier-p
78a489882f feat(executions): add autoselectFirst property to select inputs (#10919)
closes #9691
2025-08-28 09:39:05 +02:00
github-actions[bot]
b872223995 chore(core): localize to languages other than english (#10933)
Extended localization support by adding translations for multiple languages using English as the base. This enhances accessibility and usability for non-English-speaking users while keeping English as the source reference.

Co-authored-by: GitHub Action <actions@github.com>
2025-08-28 08:26:32 +02:00
YannC
e3d2b93c6b feat: export auditlogs through a streamed file (#10569) 2025-08-27 23:44:34 +02:00
brian.mulier
1699403c95 fix(dashboard): working dashboard edit 2025-08-27 22:36:15 +02:00
brian.mulier
b3fa5ead6d fix(dashboard): don't duplicate id on source retrieval 2025-08-27 21:04:02 +02:00
YannC.
d4e7b0cde4 fix: throw an error when trying to create a flow with a reserved keyword id
close #5832
2025-08-27 19:17:04 +02:00
brian-mulier-p
5da4d88738 feat(dashboard): mandatory id + add autogenerated id to source for legacy handling (#10912)
closes kestra-io/kestra-ee#4484
2025-08-27 14:10:28 +02:00
Miloš Paunović
d60ec87375 chore(core): align flow options in tour to the top of the page (#10920)
Closes https://github.com/kestra-io/kestra/issues/10915.
2025-08-27 13:54:07 +02:00
50 changed files with 858 additions and 79 deletions

View File

@@ -50,7 +50,7 @@ jobs:
# Set up JDK
- name: Set up JDK
uses: actions/setup-java@v4
uses: actions/setup-java@v5
if: ${{ matrix.language == 'java' }}
with:
distribution: 'temurin'

View File

@@ -37,7 +37,7 @@ dependencies {
implementation 'nl.basjes.gitignore:gitignore-reader'
implementation group: 'dev.failsafe', name: 'failsafe'
implementation 'com.github.ben-manes.caffeine:caffeine'
implementation 'com.github.ksuid:ksuid:1.1.3'
implementation 'com.github.ksuid:ksuid:1.1.4'
api 'org.apache.httpcomponents.client5:httpclient5'
// plugins

View File

@@ -36,6 +36,7 @@ public class Plugin {
private List<PluginElementMetadata> appBlocks;
private List<PluginElementMetadata> charts;
private List<PluginElementMetadata> dataFilters;
private List<PluginElementMetadata> dataFiltersKPI;
private List<PluginElementMetadata> logExporters;
private List<PluginElementMetadata> additionalPlugins;
private List<PluginSubGroup.PluginCategory> categories;
@@ -96,6 +97,7 @@ public class Plugin {
plugin.appBlocks = filterAndGetTypeWithMetadata(registeredPlugin.getAppBlocks(), packagePredicate);
plugin.charts = filterAndGetTypeWithMetadata(registeredPlugin.getCharts(), packagePredicate);
plugin.dataFilters = filterAndGetTypeWithMetadata(registeredPlugin.getDataFilters(), packagePredicate);
plugin.dataFiltersKPI = filterAndGetTypeWithMetadata(registeredPlugin.getDataFiltersKPI(), packagePredicate);
plugin.logExporters = filterAndGetTypeWithMetadata(registeredPlugin.getLogExporters(), packagePredicate);
plugin.additionalPlugins = filterAndGetTypeWithMetadata(registeredPlugin.getAdditionalPlugins(), packagePredicate);

View File

@@ -3,6 +3,7 @@ package io.kestra.core.models.flows.input;
import io.kestra.core.models.flows.Input;
import io.kestra.core.models.flows.RenderableInput;
import io.kestra.core.models.flows.Type;
import io.kestra.core.models.property.Property;
import io.kestra.core.models.validations.ManualConstraintViolation;
import io.kestra.core.validations.Regex;
import io.swagger.v3.oas.annotations.media.Schema;
@@ -13,6 +14,7 @@ import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
@@ -56,6 +58,23 @@ public class MultiselectInput extends Input<List<String>> implements ItemTypeInt
@Builder.Default
Boolean allowCustomValue = false;
@Schema(
title = "Whether the first value of the multi-select should be selected by default."
)
@NotNull
@Builder.Default
Boolean autoSelectFirst = false;
@Override
public Property<List<String>> getDefaults() {
Property<List<String>> baseDefaults = super.getDefaults();
if (baseDefaults == null && autoSelectFirst && !Optional.ofNullable(values).map(Collection::isEmpty).orElse(true)) {
return Property.ofValue(List.of(values.getFirst()));
}
return baseDefaults;
}
@Override
public void validate(List<String> inputs) throws ConstraintViolationException {
if (values != null && options != null) {
@@ -100,6 +119,7 @@ public class MultiselectInput extends Input<List<String>> implements ItemTypeInt
.dependsOn(getDependsOn())
.itemType(getItemType())
.displayName(getDisplayName())
.autoSelectFirst(getAutoSelectFirst())
.build();
}
return this;

View File

@@ -2,6 +2,7 @@ package io.kestra.core.models.flows.input;
import io.kestra.core.models.flows.Input;
import io.kestra.core.models.flows.RenderableInput;
import io.kestra.core.models.property.Property;
import io.kestra.core.models.validations.ManualConstraintViolation;
import io.kestra.core.validations.Regex;
import io.swagger.v3.oas.annotations.media.Schema;
@@ -12,6 +13,7 @@ import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
@@ -46,6 +48,23 @@ public class SelectInput extends Input<String> implements RenderableInput {
@Builder.Default
Boolean isRadio = false;
@Schema(
title = "Whether the first value of the select should be selected by default."
)
@NotNull
@Builder.Default
Boolean autoSelectFirst = false;
@Override
public Property<String> getDefaults() {
Property<String> baseDefaults = super.getDefaults();
if (baseDefaults == null && autoSelectFirst && !Optional.ofNullable(values).map(Collection::isEmpty).orElse(true)) {
return Property.ofValue(values.getFirst());
}
return baseDefaults;
}
@Override
public void validate(String input) throws ConstraintViolationException {
if (!values.contains(input) && this.getRequired()) {
@@ -78,6 +97,7 @@ public class SelectInput extends Input<String> implements RenderableInput {
.dependsOn(getDependsOn())
.displayName(getDisplayName())
.isRadio(getIsRadio())
.autoSelectFirst(getAutoSelectFirst())
.build();
}
return this;

View File

@@ -36,6 +36,19 @@ import static io.kestra.core.models.Label.SYSTEM_PREFIX;
@Singleton
@Introspected
public class FlowValidator implements ConstraintValidator<FlowValidation, Flow> {
public static List<String> RESERVED_FLOW_IDS = List.of(
"pause",
"resume",
"force-run",
"change-status",
"kill",
"executions",
"search",
"source",
"disable",
"enable"
);
@Inject
private FlowService flowService;
@@ -50,6 +63,10 @@ public class FlowValidator implements ConstraintValidator<FlowValidation, Flow>
List<String> violations = new ArrayList<>();
if (RESERVED_FLOW_IDS.contains(value.getId())) {
violations.add("Flow id is a reserved keyword: " + value.getId() + ". List of reserved keywords: " + String.join(", ", RESERVED_FLOW_IDS));
}
if (flowService.requireExistingNamespace(value.getTenantId(), value.getNamespace())) {
violations.add("Namespace '" + value.getNamespace() + "' does not exist but is required to exist before a flow can be created in it.");
}

View File

@@ -60,4 +60,43 @@ class MultiselectInputTest {
// Then
Assertions.assertEquals(((MultiselectInput)renderInput).getValues(), List.of("1", "2"));
}
@Test
void staticAutoselectFirst() throws IllegalVariableEvaluationException {
RunContext runContext = runContextFactory.of();
MultiselectInput input = MultiselectInput
.builder()
.id("id")
.values(List.of("V1", "V2"))
.autoSelectFirst(true)
.build();
Assertions.assertEquals(List.of("V1"), runContext.render(input.getDefaults()).asList(String.class));
}
@Test
void dynamicAutoselectFirst() throws IllegalVariableEvaluationException {
// Given
RunContext runContext = runContextFactory.of(Map.of("values", List.of("V1", "V2")));
MultiselectInput input = MultiselectInput
.builder()
.id("id")
.expression("{{ values }}")
.autoSelectFirst(true)
.build();
Assertions.assertNull(input.getDefaults());
// When
Input<?> renderInput = RenderableInput.mayRenderInput(input, s -> {
try {
return runContext.renderTyped(s);
} catch (IllegalVariableEvaluationException e) {
throw new RuntimeException(e);
}
});
// Then
Assertions.assertEquals(List.of("V1"), runContext.render(((MultiselectInput)renderInput).getDefaults()).asList(String.class));
}
}

View File

@@ -60,4 +60,43 @@ class SelectInputTest {
// Then
Assertions.assertEquals(((SelectInput)renderInput).getValues(), List.of("1", "2"));
}
@Test
void staticAutoselectFirst() throws IllegalVariableEvaluationException {
RunContext runContext = runContextFactory.of();
SelectInput input = SelectInput
.builder()
.id("id")
.values(List.of("V1", "V2"))
.autoSelectFirst(true)
.build();
Assertions.assertEquals("V1", runContext.render(input.getDefaults()).as(String.class).orElseThrow());
}
@Test
void dynamicAutoselectFirst() throws IllegalVariableEvaluationException {
// Given
RunContext runContext = runContextFactory.of(Map.of("values", List.of("V1", "V2")));
SelectInput input = SelectInput
.builder()
.id("id")
.expression("{{ values }}")
.autoSelectFirst(true)
.build();
Assertions.assertNull(input.getDefaults());
// When
Input<?> renderInput = RenderableInput.mayRenderInput(input, s -> {
try {
return runContext.renderTyped(s);
} catch (IllegalVariableEvaluationException e) {
throw new RuntimeException(e);
}
});
// Then
Assertions.assertEquals("V1", runContext.render(((SelectInput)renderInput).getDefaults()).as(String.class).orElseThrow());
}
}

View File

@@ -312,7 +312,7 @@ public abstract class AbstractRunnerTest {
}
@RetryingTest(5) // flaky on MySQL
@LoadFlows({"flows/valids/pause.yaml"})
@LoadFlows({"flows/valids/pause-test.yaml"})
public void pauseRun() throws Exception {
pauseTest.run(runnerUtils);
}

View File

@@ -344,9 +344,9 @@ class ExecutionServiceTest {
}
@Test
@LoadFlows({"flows/valids/pause.yaml"})
@LoadFlows({"flows/valids/pause-test.yaml"})
void resumePausedToRunning() throws Exception {
Execution execution = runnerUtils.runOneUntilPaused(MAIN_TENANT, "io.kestra.tests", "pause");
Execution execution = runnerUtils.runOneUntilPaused(MAIN_TENANT, "io.kestra.tests", "pause-test");
Flow flow = flowRepository.findByExecution(execution);
assertThat(execution.getTaskRunList()).hasSize(1);
@@ -364,9 +364,9 @@ class ExecutionServiceTest {
}
@Test
@LoadFlows({"flows/valids/pause.yaml"})
@LoadFlows({"flows/valids/pause-test.yaml"})
void resumePausedToKilling() throws Exception {
Execution execution = runnerUtils.runOneUntilPaused(MAIN_TENANT, "io.kestra.tests", "pause");
Execution execution = runnerUtils.runOneUntilPaused(MAIN_TENANT, "io.kestra.tests", "pause-test");
Flow flow = flowRepository.findByExecution(execution);
assertThat(execution.getTaskRunList()).hasSize(1);

View File

@@ -412,4 +412,23 @@ class FlowServiceTest {
"The task 'for' cannot use the 'workerGroup' property as it's only relevant for runnable tasks."
);
}
@Test
void shouldReturnValidationErrorForReservedFlowId() {
// Given
String source = """
id: pause
namespace: io.kestra.unittest
tasks:
- id: task
type: io.kestra.plugin.core.log.Log
message: Reserved id test
""";
// When
List<ValidateConstraintViolation> results = flowService.validate("my-tenant", source);
// Then
assertThat(results).hasSize(1);
assertThat(results.getFirst().getConstraints()).contains("Flow id is a reserved keyword: pause");
}
}

View File

@@ -61,7 +61,7 @@ class SanityCheckTest {
}
@Test
@ExecuteFlow("sanity-checks/pause.yaml")
@ExecuteFlow("sanity-checks/pause-test.yaml")
void qaPause(Execution execution) {
assertThat(execution.getTaskRunList()).hasSize(1);
assertThat(execution.getState().getCurrent()).isEqualTo(State.Type.SUCCESS);

View File

@@ -26,10 +26,10 @@ class ResumeTest {
private ExecutionRepositoryInterface executionRepository;
@Test
@LoadFlows({"flows/valids/pause.yaml",
@LoadFlows({"flows/valids/pause-test.yaml",
"flows/valids/resume-execution.yaml"})
void resume() throws Exception {
Execution pause = runnerUtils.runOneUntilPaused(MAIN_TENANT, "io.kestra.tests", "pause");
Execution pause = runnerUtils.runOneUntilPaused(MAIN_TENANT, "io.kestra.tests", "pause-test");
String pauseId = pause.getId();
Execution resume = runnerUtils.runOne(MAIN_TENANT, "io.kestra.tests", "resume-execution", null, (flow, execution) -> Map.of("executionId", pauseId));

View File

@@ -53,7 +53,7 @@ public class PauseTest {
Suite suite;
@Test
@LoadFlows({"flows/valids/pause.yaml"})
@LoadFlows({"flows/valids/pause-test.yaml"})
void run() throws Exception {
suite.run(runnerUtils);
}
@@ -161,7 +161,7 @@ public class PauseTest {
protected QueueInterface<Execution> executionQueue;
public void run(RunnerUtils runnerUtils) throws Exception {
Execution execution = runnerUtils.runOneUntilPaused(MAIN_TENANT, "io.kestra.tests", "pause", null, null, Duration.ofSeconds(30));
Execution execution = runnerUtils.runOneUntilPaused(MAIN_TENANT, "io.kestra.tests", "pause-test", null, null, Duration.ofSeconds(30));
String executionId = execution.getId();
Flow flow = flowRepository.findByExecution(execution);

View File

@@ -0,0 +1,19 @@
id: condition_with_input
namespace: io.kestra.tests
inputs:
- id: condition
type: SELECT
values:
- success
- fail
defaults: success
tasks:
- id: hello
type: io.kestra.plugin.core.log.Log
message: Hello World!
- id: fail
type: io.kestra.plugin.core.execution.Fail
runIf: "{{ inputs.condition == 'fail' }}"

View File

@@ -1,4 +1,4 @@
id: pause
id: pause-test
namespace: io.kestra.tests
tasks:

View File

@@ -1,4 +1,4 @@
id: pause
id: pause-test
namespace: sanitychecks.core
tasks:

View File

@@ -20,7 +20,7 @@ dependencies {
def kafkaVersion = "4.0.0"
def opensearchVersion = "3.2.0"
def opensearchRestVersion = "3.2.0"
def flyingSaucerVersion = "9.13.2"
def flyingSaucerVersion = "9.13.3"
def jacksonVersion = "2.19.2"
def jugVersion = "5.1.0"
@@ -32,7 +32,7 @@ dependencies {
// we define cloud bom here for GCP, Azure and AWS so they are aligned for all plugins that use them (secret, storage, oss and ee plugins)
api platform('com.google.cloud:libraries-bom:26.66.0')
api platform("com.azure:azure-sdk-bom:1.2.37")
api platform('software.amazon.awssdk:bom:2.32.26')
api platform('software.amazon.awssdk:bom:2.32.31')
constraints {
@@ -80,7 +80,7 @@ dependencies {
api group: 'org.slf4j', name: 'jcl-over-slf4j', version: slf4jVersion
api group: 'org.fusesource.jansi', name: 'jansi', version: '2.4.2'
api group: 'com.devskiller.friendly-id', name: 'friendly-id', version: '1.1.0'
api group: 'net.thisptr', name: 'jackson-jq', version: '1.4.0'
api group: 'net.thisptr', name: 'jackson-jq', version: '1.5.0'
api group: 'com.google.guava', name: 'guava', version: '33.4.8-jre'
api group: 'commons-io', name: 'commons-io', version: '2.20.0'
api group: 'org.apache.commons', name: 'commons-lang3', version: '3.18.0'
@@ -111,10 +111,10 @@ dependencies {
api (group: 'org.opensearch.client', name: 'opensearch-java', version: "$opensearchVersion")
api (group: 'org.opensearch.client', name: 'opensearch-rest-client', version: "$opensearchRestVersion")
api (group: 'org.opensearch.client', name: 'opensearch-rest-high-level-client', version: "$opensearchRestVersion") // used by the elasticsearch plugin
api 'org.jsoup:jsoup:1.21.1'
api 'org.jsoup:jsoup:1.21.2'
api "org.xhtmlrenderer:flying-saucer-core:$flyingSaucerVersion"
api "org.xhtmlrenderer:flying-saucer-pdf:$flyingSaucerVersion"
api group: 'jakarta.mail', name: 'jakarta.mail-api', version: '2.1.3'
api group: 'jakarta.mail', name: 'jakarta.mail-api', version: '2.1.4'
api group: 'org.eclipse.angus', name: 'jakarta.mail', version: '2.0.4'
api group: 'com.github.ben-manes.caffeine', name: 'caffeine', version: '3.2.2'
api group: 'de.siegmar', name: 'fastcsv', version: '4.0.0'

View File

@@ -25,4 +25,4 @@ const ANIMALS: string[] = [
const getRandomNumber = (minimum: number = MINIMUM, maximum: number = MAXIMUM): number => Math.floor(Math.random() * (maximum - minimum + 1)) + minimum;
const getRandomAnimal = (): string => ANIMALS[Math.floor(Math.random() * ANIMALS.length)];
export const getRandomFlowID = (): string => `${getRandomAnimal()}_${getRandomNumber()}`.toLowerCase();
export const getRandomID = (): string => `${getRandomAnimal()}_${getRandomNumber()}`.toLowerCase();

View File

@@ -38,6 +38,8 @@
import type {Dashboard} from "../../../components/dashboard/composables/useDashboards";
import {getDashboard, processFlowYaml} from "../../../components/dashboard/composables/useDashboards";
import {getRandomID} from "../../../../scripts/id";
const dashboard = ref<Dashboard>({id: "", charts: []});
const save = async (source: string) => {
const response = await dashboardStore.create(source)
@@ -69,6 +71,8 @@
} else {
dashboard.value.sourceCode = name === "namespaces/update" ? YAML_NAMESPACE : YAML_MAIN;
}
dashboard.value.sourceCode = "id: " + getRandomID() + "\n" + dashboard.value.sourceCode;
}
});

View File

@@ -103,8 +103,6 @@
</div>
</template>
<script setup>
import {YamlUtils as YAML_UTILS} from "@kestra-io/ui-libs";
import PluginDocumentation from "../../plugins/PluginDocumentation.vue";
import Sections from "../sections/Sections.vue";
import ValidationErrors from "../../flows/ValidationError.vue"
@@ -125,6 +123,8 @@
import yaml from "yaml";
import ContentSave from "vue-material-design-icons/ContentSave.vue";
import intro from "../../../assets/docs/dashboard_home.md?raw";
import * as YAML_UTILS from "@kestra-io/ui-libs/flow-yaml-utils";
import {useCoreStore} from "../../../stores/core.js";
export default {
computed: {
@@ -144,6 +144,9 @@
displaySide() {
return this.currentView !== this.views.NONE && this.currentView !== this.views.DASHBOARD;
},
dashboardId() {
return this.initialSource === undefined ? undefined : YAML_UTILS.parse(this.initialSource).id
}
},
props: {
allowSaveUnchanged: {
@@ -153,6 +156,10 @@
initialSource: {
type: String,
default: undefined
},
modelValue: {
type: String,
default: undefined
}
},
mounted() {
@@ -164,7 +171,7 @@
methods: {
async updatePluginDocumentation(event) {
if (this.currentView === this.views.DOC) {
const type = YAML_UTILS.getTaskType(event.model.getValue(), event.position, this.plugins)
const type = YAML_UTILS.getTypeAtPosition(event.model.getValue(), event.position, this.plugins);
if (type) {
this.pluginsStore.load({cls: type})
@@ -280,6 +287,23 @@
this.errors = undefined;
}
});
if (YAML_UTILS.parse(this.source).id !== this.dashboardId) {
const coreStore = useCoreStore();
coreStore.message = {
variant: "error",
title: this.$t("readonly property"),
message: this.$t("dashboards.edition.id readonly"),
};
this.$nextTick(() => {
this.source = YAML_UTILS.replaceBlockWithPath({
source: this.source,
path: "id",
newContent: this.dashboardId
});
})
}
}
},
beforeUnmount() {

View File

@@ -16,7 +16,7 @@
import {useCoreStore} from "../../stores/core";
import {editorViewTypes} from "../../utils/constants";
import {getRandomFlowID} from "../../../scripts/product/flow";
import {getRandomID} from "../../../scripts/id";
import {useEditorStore} from "../../stores/editor";
import {useFlowStore} from "../../stores/flow";
@@ -52,7 +52,7 @@
} else {
const defaultNamespace = localStorage.getItem(storageKeys.DEFAULT_NAMESPACE);
const selectedNamespace = this.$route.query.namespace || defaultNamespace || "company.team";
flowYaml = `id: ${getRandomFlowID()}
flowYaml = `id: ${getRandomID()}
namespace: ${selectedNamespace}
tasks:

View File

@@ -229,9 +229,9 @@
toast.confirm(
t("delete task confirm", {taskId: event.id}),
() => {
const section = event.section ? event.section : SECTIONS.TASKS;
const section = event.section ? event.section.toLowerCase() : SECTIONS.TASKS.toLowerCase();
if (
section === SECTIONS.TASKS &&
section === SECTIONS.TASKS.toLowerCase() &&
flowParsed.tasks.length === 1 &&
flowParsed.tasks.map((e: any) => e.id).includes(event.id)
) {

View File

@@ -85,13 +85,23 @@
}))
};
const onEdit = (source:string, currentIsFlow = false) => {
const onEdit = async (source: string, currentIsFlow = false) => {
flowStore.flowYaml = source
return flowStore.onEdit({
const result = await flowStore.onEdit({
source,
currentIsFlow,
editorViewType: "YAML",
})
if (currentIsFlow && source) {
await flowStore.loadGraphFromSource({
flow: source,
}).catch((error) => {
console.error("Error loading graph:", error);
})
}
return result
}
</script>
@@ -106,4 +116,4 @@
margin: 1rem;
width: auto;
}
</style>
</style>

View File

@@ -0,0 +1,40 @@
<template>
<el-table :data="store.inheritedKVs" table-layout="auto">
<el-table-column prop="key" :label="$t('key')">
<template #default="scope">
<code>{{ scope.row.key }}</code>
</template>
</el-table-column>
<el-table-column prop="description" :label="$t('description')">
<template #default="scope">
<span>{{ scope.row.description }}</span>
</template>
</el-table-column>
<el-table-column prop="updateDate" :label="$t('last modified')">
<template #default="scope">
<span>{{ scope.row.updateDate }}</span>
</template>
</el-table-column>
<el-table-column prop="creationDate" :label="$t('created date')">
<template #default="scope">
<span>{{ scope.row.creationDate }}</span>
</template>
</el-table-column>
</el-table>
</template>
<script setup>
import {onMounted} from "vue";
import {useNamespacesStore} from "override/stores/namespaces";
const props = defineProps({namespace: {type: String, required: true}});
const store = useNamespacesStore();
const loadItem = () => store.loadInheritedKVs(props.namespace);
onMounted(() => loadItem());
</script>

View File

@@ -189,6 +189,14 @@
</el-button>
</template>
</Drawer>
<drawer
v-if="namespacesStore.inheritedKVModalVisible"
v-model="namespacesStore.inheritedKVModalVisible"
:title="$t('kv.inherited')"
>
<InheritedKVs :namespace="namespacesStore?.namespace?.id" />
</drawer>
</template>
<script setup lang="ts">
@@ -207,6 +215,8 @@
import KestraFilter from "../filter/KestraFilter.vue";
import Id from "../Id.vue";
import Drawer from "../Drawer.vue";
import InheritedKVs from "./InheritedKVs.vue";
</script>
<script lang="ts">

View File

@@ -15,6 +15,7 @@
fullscreen: currentStep(tour).fullscreen,
color: tour.currentStep === 1,
condensed: currentStep(tour).condensed,
second: tour.currentStep === 1
}"
>
<template #header>
@@ -479,8 +480,16 @@ $flow-image-size-container: 36px;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
justify-content: center;
&.second {
justify-content: flex-start;
& .flows {
margin-top: 160px !important;
}
}
}
#app .v-step {

View File

@@ -1,7 +1,7 @@
<template>
<top-nav-bar :title="routeInfo.title" :breadcrumb="routeInfo?.breadcrumb" />
<template v-if="!pluginIsSelected">
<plugin-home v-if="pluginsStore.plugins" :plugins="pluginsStore.plugins" />
<plugin-home v-if="filteredPlugins" :plugins="filteredPlugins" />
</template>
<docs-layout v-else>
<template #menu>
@@ -120,12 +120,13 @@
return {
isLoading: false,
version: undefined,
pluginType: undefined
pluginType: undefined,
filteredPlugins: undefined
};
},
created() {
this.loadToc();
this.loadPlugin()
this.loadPlugin();
},
watch: {
$route: {
@@ -139,6 +140,15 @@
}
},
immediate: true
},
async "pluginsStore.plugins"() {
this.filteredPlugins = await this.pluginsStore.filteredPlugins([
"apps",
"appBlocks",
"charts",
"dataFilters",
"dataFiltersKPI"
])
}
},
methods: {

View File

@@ -17,6 +17,8 @@ export function useBaseNamespacesStore() {
const secrets = ref<any[] | undefined>(undefined);
const inheritedSecrets = ref<any>(undefined);
const kvs = ref<any[] | undefined>(undefined);
const inheritedKVs = ref<any>(undefined);
const inheritedKVModalVisible = ref(false);
const addKvModalVisible = ref(false);
const autocomplete = ref<any>(undefined);
const total = ref(0);
@@ -74,6 +76,11 @@ export function useBaseNamespacesStore() {
return data;
}
async function loadInheritedKVs(this: any, id: string) {
const response = await this.$http.get(`${apiUrl(this.vuexStore)}/namespaces/${id}/kv/inheritance`, {...VALIDATE});
inheritedKVs.value = response.data;
}
async function createKv(this: any, payload: {namespace: string; key: string; value: any; contentType: string; description: string; ttl: string}) {
await this.$http.put(
`${apiUrl(this.vuexStore)}/namespaces/${payload.namespace}/kv/${payload.key}`,
@@ -238,9 +245,12 @@ export function useBaseNamespacesStore() {
secrets,
inheritedSecrets,
kvs,
inheritedKVModalVisible,
addKvModalVisible,
kvsList,
kv,
loadInheritedKVs,
inheritedKVs,
createKv,
deleteKv,
deleteKvs,

View File

@@ -2,7 +2,7 @@
<TopNavBar :title="header.title" :breadcrumb="header.breadcrumb" />
<section class="full-container">
<Editor
v-if="dashboard.sourceCode"
v-if="dashboard?.sourceCode"
:initial-source="dashboard.sourceCode"
@save="save"
/>
@@ -13,6 +13,7 @@
import {onMounted, computed, ref} from "vue";
import {useRoute} from "vue-router";
const route = useRoute();
import {useCoreStore} from "../../../stores/core";

View File

@@ -5,6 +5,12 @@
:to="{name: 'flows/create', query: {namespace}}"
/>
<Action
:label="t('kv.inherited')"
:icon="FamilyTree"
@click="namespacesStore.inheritedKVModalVisible = true"
/>
<Action
v-if="tab === 'kv'"
:label="t('kv.add')"
@@ -19,6 +25,8 @@
import {useNamespacesStore} from "override/stores/namespaces";
import Action from "../../../components/namespaces/components/buttons/Action.vue";
import FamilyTree from "vue-material-design-icons/FamilyTree.vue";
const route = useRoute();
const {t} = useI18n({useScope: "global"});
const namespacesStore = useNamespacesStore();

View File

@@ -28,8 +28,12 @@ export interface Plugin {
taskRunners: PluginComponent[];
charts: PluginComponent[];
dataFilters: PluginComponent[];
dataFiltersKPI: PluginComponent[];
aliases: PluginComponent[];
logExporters: PluginComponent[];
apps: PluginComponent[];
appBlocks: PluginComponent[];
additionalPlugins: PluginComponent[];
}
interface State {
@@ -139,6 +143,20 @@ export const usePluginsStore = defineStore("plugins", {
}
return obj;
},
async filteredPlugins(excludedElements: string[]) {
if (this.plugins === undefined) {
this.plugins = await this.listWithSubgroup({includeDeprecated: false});
}
return this.plugins.map(p => ({
...p,
...Object.fromEntries(excludedElements.map(e => [e, undefined]))
})).filter(p => Object.entries(p)
.filter(([key, value]) => isEntryAPluginElementPredicate(key, value))
.some(([, value]: [string, PluginComponent[]]) => value.length !== 0))
},
async list() {
const response = await this.$http.get<Plugin[]>(`${apiUrlWithoutTenants()}/plugins`);
this.plugins = response.data;

View File

@@ -217,6 +217,7 @@
"edition": {
"chart": "Bearbeiten Sie dieses Diagramm",
"confirmation": "Änderungen im Dashboard <code>{title}</code> werden gespeichert.",
"id readonly": "Die Eigenschaft `id` kann nicht geändert werden — sie ist jetzt auf ihre Anfangswerte festgelegt. Wenn Sie sie ändern möchten, können Sie ein neues Dashboard erstellen und das alte entfernen.",
"label": "Dashboard bearbeiten"
},
"empty": "Es gibt keine Ergebnisse anzuzeigen.",
@@ -725,6 +726,7 @@
"warning": "Du hast keine Berechtigungen für diese namespaces, daher werden sie ausgelassen: <code>{namespaces}</code>"
},
"duplicate": "Dieser Key existiert bereits",
"inherited": "Geerbte KV-Paare",
"name": "KV Store",
"type": "Typ",
"update": "Wert für Key '{key}' aktualisieren"

View File

@@ -986,7 +986,8 @@
"delete multiple": {
"confirm": "Are you sure you want to delete: <code>{name}</code> KV(s)?",
"warning": "You don't have permissions for those namespaces so they would be omitted: <code>{namespaces}</code>"
}
},
"inherited": "Inherited KV pairs"
},
"expiration": "Expiration",
"failed to render pdf": "Failed to render the PDF preview",
@@ -1382,7 +1383,8 @@
"edition": {
"label": "Edit Dashboard",
"confirmation": "Changes in dashboard <code>{title}</code> are saved.",
"chart": "Edit this chart"
"chart": "Edit this chart",
"id readonly": "The property `id` cannot be changed — it's now set to its initial values. If you want to change it, you can create a new dashboard and remove the old one."
},
"deletion": {
"confirmation": "Are you sure you want to delete <code>{title}</code> dashboard?"

View File

@@ -217,6 +217,7 @@
"edition": {
"chart": "Editar este gráfico",
"confirmation": "Los cambios en el dashboard <code>{title}</code> se han guardado.",
"id readonly": "La propiedad `id` no se puede cambiar — ahora está establecida en sus valores iniciales. Si deseas cambiarla, puedes crear un nuevo dashboard y eliminar el antiguo.",
"label": "Editar Dashboard"
},
"empty": "No hay resultados para mostrar.",
@@ -725,6 +726,7 @@
"warning": "No tienes permisos para esos namespaces, por lo que se omitirán: <code>{namespaces}</code>"
},
"duplicate": "Esta key ya existe",
"inherited": "Pares KV heredados",
"name": "KV Store",
"type": "Tipo",
"update": "Actualizar value para key '{key}'"

View File

@@ -217,6 +217,7 @@
"edition": {
"chart": "Modifier ce graphique",
"confirmation": "Les modifications dans le tableau de bord <code>{title}</code> sont enregistrées.",
"id readonly": "La propriété `id` ne peut pas être modifiée — elle est maintenant définie sur ses valeurs initiales. Si vous souhaitez la changer, vous pouvez créer un nouveau tableau de bord et supprimer l'ancien.",
"label": "Modifier le tableau de bord"
},
"empty": "Il n'y a aucun résultat à afficher.",
@@ -725,6 +726,7 @@
"warning": "Vous n'avez pas les autorisations pour ces namespaces, ils seront donc omis : <code>{namespaces}</code>"
},
"duplicate": "Cette clé existe déjà",
"inherited": "Paires KV héritées",
"name": "Stockage de clés/valeurs",
"type": "Type",
"update": "Mettre à jour la valeur pour la clé '{key}'"

View File

@@ -217,6 +217,7 @@
"edition": {
"chart": "इस चार्ट को संपादित करें",
"confirmation": "डैशबोर्ड <code>{title}</code> में परिवर्तन सहेजे गए हैं।",
"id readonly": "संपत्ति `id` को बदला नहीं जा सकता — यह अब अपनी प्रारंभिक मानों पर सेट है। यदि आप इसे बदलना चाहते हैं, तो आप एक नया डैशबोर्ड बना सकते हैं और पुराने को हटा सकते हैं।",
"label": "डैशबोर्ड संपादित करें"
},
"empty": "कोई परिणाम नहीं दिखाया जा सकता।",
@@ -725,6 +726,7 @@
"warning": "आपके पास उन namespaces के लिए अनुमतियाँ नहीं हैं, इसलिए उन्हें हटा दिया जाएगा: <code>{namespaces}</code>"
},
"duplicate": "यह key पहले से मौजूद है",
"inherited": "विरासत में मिले KV जोड़े",
"name": "KV Store",
"type": "प्रकार",
"update": "key '{key}' के लिए value अपडेट करें"

View File

@@ -217,6 +217,7 @@
"edition": {
"chart": "Modifica questo grafico",
"confirmation": "Le modifiche nel dashboard <code>{title}</code> sono state salvate.",
"id readonly": "La proprietà `id` non può essere modificata — è ora impostata sui suoi valori iniziali. Se desideri cambiarla, puoi creare una nuova dashboard e rimuovere quella vecchia.",
"label": "Modifica Dashboard"
},
"empty": "Non ci sono risultati da mostrare.",
@@ -725,6 +726,7 @@
"warning": "Non hai i permessi per quei namespace, quindi verranno omessi: <code>{namespaces}</code>"
},
"duplicate": "Questo key esiste già",
"inherited": "Coppie KV ereditate",
"name": "KV Store",
"type": "Tipo",
"update": "Aggiorna il value per il key '{key}'"

View File

@@ -217,6 +217,7 @@
"edition": {
"chart": "このチャートを編集",
"confirmation": "ダッシュボード<code>{title}</code>の変更が保存されました。",
"id readonly": "プロパティ `id` は変更できません — 現在、初期値に設定されています。変更したい場合は、新しいダッシュボードを作成し、古いものを削除してください。",
"label": "ダッシュボードを編集"
},
"empty": "表示する結果がありません。",
@@ -725,6 +726,7 @@
"warning": "あなたはこれらのnamespaceに対する権限がないため、省略されます: <code>{namespaces}</code>"
},
"duplicate": "このキーは既に存在します",
"inherited": "継承されたKVペア",
"name": "KV Store",
"type": "タイプ",
"update": "キー'{key}'の値を更新"

View File

@@ -217,6 +217,7 @@
"edition": {
"chart": "이 차트 편집",
"confirmation": "대시보드 <code>{title}</code>의 변경 사항이 저장되었습니다.",
"id readonly": "`id` 속성은 변경할 수 없습니다 — 현재 초기 값으로 설정되어 있습니다. 변경하려면 새 대시보드를 생성하고 기존 것을 제거할 수 있습니다.",
"label": "대시보드 편집"
},
"empty": "결과가 표시되지 않습니다.",
@@ -725,6 +726,7 @@
"warning": "해당 namespace에 대한 권한이 없으므로 생략됩니다: <code>{namespaces}</code>"
},
"duplicate": "이 키는 이미 존재합니다",
"inherited": "상속된 KV 쌍",
"name": "KV Store",
"type": "유형",
"update": "키 '{key}'의 값을 업데이트하세요"

View File

@@ -217,6 +217,7 @@
"edition": {
"chart": "Edytuj ten wykres",
"confirmation": "Zmiany w dashboardzie <code>{title}</code> zostały zapisane.",
"id readonly": "Właściwość `id` nie może być zmieniona — jest teraz ustawiona na swoje początkowe wartości. Jeśli chcesz ją zmienić, możesz utworzyć nowy dashboard i usunąć stary.",
"label": "Edytuj Dashboard"
},
"empty": "Brak wyników do wyświetlenia.",
@@ -725,6 +726,7 @@
"warning": "Nie masz uprawnień do tych namespace, więc zostaną pominięte: <code>{namespaces}</code>"
},
"duplicate": "Ten key już istnieje",
"inherited": "Odziedziczone pary KV",
"name": "KV Store",
"type": "Typ",
"update": "Zaktualizuj value dla key '{key}'"

View File

@@ -217,6 +217,7 @@
"edition": {
"chart": "Edite este gráfico",
"confirmation": "As alterações no dashboard <code>{title}</code> foram salvas.",
"id readonly": "A propriedade `id` não pode ser alterada — agora está definida para seus valores iniciais. Se você quiser alterá-la, pode criar um novo dashboard e remover o antigo.",
"label": "Editar Dashboard"
},
"empty": "Não há resultados para serem exibidos.",
@@ -725,6 +726,7 @@
"warning": "Você não tem permissões para esses namespaces, então eles serão omitidos: <code>{namespaces}</code>"
},
"duplicate": "Esta key já existe",
"inherited": "Pares KV herdados",
"name": "KV Store",
"type": "Tipo",
"update": "Atualizar valor para a key '{key}'"

View File

@@ -217,6 +217,7 @@
"edition": {
"chart": "Редактировать этот график",
"confirmation": "Изменения в панели <code>{title}</code> сохранены.",
"id readonly": "Свойство `id` не может быть изменено — оно установлено на начальные значения. Если вы хотите его изменить, вы можете создать новую панель и удалить старую.",
"label": "Редактировать Dashboard"
},
"empty": "Нет результатов для отображения.",
@@ -725,6 +726,7 @@
"warning": "У вас нет разрешений для этих namespaces, поэтому они будут пропущены: <code>{namespaces}</code>"
},
"duplicate": "Этот key уже существует",
"inherited": "Наследуемые KV пары",
"name": "KV Store",
"type": "Тип",
"update": "Обновить значение для key '{key}'"

View File

@@ -217,6 +217,7 @@
"edition": {
"chart": "编辑此图表",
"confirmation": "仪表板<code>{title}</code>中的更改已保存。",
"id readonly": "属性 `id` 不能更改——它现在设置为初始值。如果您想更改它,可以创建一个新的仪表板并删除旧的。",
"label": "编辑仪表板"
},
"empty": "没有结果显示。",
@@ -725,6 +726,7 @@
"warning": "您没有这些namespace的权限因此它们将被省略<code>{namespaces}</code>"
},
"duplicate": "此键已存在",
"inherited": "继承的KV对",
"name": "键值存储",
"type": "类型",
"update": "更新键 '{key}' 的值"

View File

@@ -10,7 +10,7 @@ type DependencyOptions = {
subtype?: typeof FLOW | typeof EXECUTION | typeof NAMESPACE;
};
import {getRandomFlowID} from "../../../scripts/product/flow";
import {getRandomID} from "../../../scripts/id";
const namespaces = ["company", "team", "github", "qa", "system", "dev", "test", "data", "infra", "cloud", "backend", "frontend", "api", "services", "database", "mobile", "security"];
@@ -54,7 +54,7 @@ function createNode(subtype: typeof FLOW | typeof EXECUTION | typeof NAMESPACE):
return {
id: uuid(),
type: NODE,
flow: getRandomFlowID(),
flow: getRandomID(),
namespace: getRandomNamespace(),
metadata: subtype === FLOW || subtype === NAMESPACE ? {subtype} : {subtype: EXECUTION, state: states[getRandomNumber(0, states.length - 1)]},
};

View File

@@ -8,6 +8,8 @@ import io.kestra.core.models.dashboards.charts.Chart;
import io.kestra.core.models.dashboards.charts.DataChart;
import io.kestra.core.models.dashboards.charts.DataChartKPI;
import io.kestra.core.models.flows.Flow;
import io.kestra.core.models.flows.GenericFlow;
import io.kestra.core.models.validations.ManualConstraintViolation;
import io.kestra.core.models.validations.ModelValidator;
import io.kestra.core.models.validations.ValidateConstraintViolation;
import io.kestra.core.repositories.ArrayListTotal;
@@ -15,7 +17,6 @@ import io.kestra.core.repositories.DashboardRepositoryInterface;
import io.kestra.core.repositories.FlowRepositoryInterface;
import io.kestra.core.serializers.YamlParser;
import io.kestra.core.tenant.TenantService;
import io.kestra.core.utils.IdUtils;
import io.kestra.plugin.core.dashboard.chart.Markdown;
import io.kestra.plugin.core.dashboard.chart.Table;
import io.kestra.plugin.core.dashboard.chart.mardown.sources.FlowDescription;
@@ -49,9 +50,8 @@ import java.io.IOException;
import java.io.OutputStreamWriter;
import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.*;
import java.util.regex.Pattern;
import static io.kestra.core.utils.DateUtils.validateTimeline;
@@ -60,6 +60,7 @@ import static io.kestra.core.utils.DateUtils.validateTimeline;
@Slf4j
public class DashboardController {
protected static final YamlParser YAML_PARSER = new YamlParser();
public static final Pattern DASHBOARD_ID_PATTERN = Pattern.compile("^id:.*$", Pattern.MULTILINE);
@Inject
private DashboardRepositoryInterface dashboardRepository;
@@ -91,7 +92,12 @@ public class DashboardController {
public Dashboard getDashboard(
@Parameter(description = "The dashboard id") @PathVariable String id
) throws ConstraintViolationException {
return dashboardRepository.get(tenantService.resolveTenant(), id).orElse(null);
return dashboardRepository.get(tenantService.resolveTenant(), id).map(d -> {
if (!DASHBOARD_ID_PATTERN.matcher(d.getSourceCode()).find()) {
return d.toBuilder().sourceCode("id: " + d.getId() + "\n" + d.getSourceCode()).build();
}
return d;
}).orElse(null);
}
@ExecuteOn(TaskExecutors.IO)
@@ -101,13 +107,24 @@ public class DashboardController {
@RequestBody(description = "The dashboard definition as YAML") @Body String dashboard
) throws ConstraintViolationException {
Dashboard dashboardParsed = parseDashboard(dashboard);
if (dashboardParsed.getId() == null) {
throw new IllegalArgumentException("Dashboard id is mandatory");
}
modelValidator.validate(dashboardParsed);
if (dashboardParsed.getId() != null) {
throw new IllegalArgumentException("Dashboard id is not editable");
Optional<Dashboard> existingDashboard = dashboardRepository.get(tenantService.resolveTenant(), dashboardParsed.getId());
if (existingDashboard.isPresent()) {
throw new ConstraintViolationException(Collections.singleton(ManualConstraintViolation.of(
"Dashboard id already exists",
dashboardParsed,
Dashboard.class,
"dashboard.id",
dashboardParsed.getId()
)));
}
return HttpResponse.ok(this.save(null, dashboardParsed.toBuilder().id(IdUtils.create()).build(), dashboard));
return HttpResponse.ok(this.save(null, dashboardParsed, dashboard));
}
@ExecuteOn(TaskExecutors.IO)
@@ -148,6 +165,15 @@ public class DashboardController {
return HttpResponse.status(HttpStatus.NOT_FOUND);
}
Dashboard dashboardToSave = parseDashboard(dashboard);
if (!dashboardToSave.getId().equals(id)) {
throw new ConstraintViolationException(Set.of(ManualConstraintViolation.of(
"Illegal dashboard id update",
dashboardToSave,
Dashboard.class,
"dashboard.id",
dashboardToSave.getId()
)));
}
modelValidator.validate(dashboardToSave);
return HttpResponse.ok(this.save(existingDashboard.get(), dashboardToSave, dashboard));

View File

@@ -1058,6 +1058,32 @@ public class ExecutionController {
return innerReplay(execution.get(), taskRunId, revision, breakpoints);
}
@ExecuteOn(TaskExecutors.IO)
@Post(uri = "/{executionId}/replay-with-inputs", consumes = MediaType.MULTIPART_FORM_DATA)
@Operation(tags = {"Executions"}, summary = "Create a new execution from an old one and start it from a specified task run id")
public Mono<Execution> replayExecutionWithinputs(
@Parameter(description = "the original execution id to clone") @PathVariable String executionId,
@Parameter(description = "The taskrun id") @Nullable @QueryValue String taskRunId,
@Parameter(description = "The flow revision to use for new execution") @Nullable @QueryValue Integer revision,
@Parameter(description = "Set a list of breakpoints at specific tasks 'id.value', separated by a coma.") @QueryValue Optional<String> breakpoints,
@RequestBody(description = "The inputs") @Body MultipartBody inputs
) {
Optional<Execution> execution = executionRepository.findById(tenantService.resolveTenant(), executionId);
if (execution.isEmpty()) {
return null;
}
Execution current = execution.get();
this.controlRevision(current, revision);
Flow flow = flowService.getFlowIfExecutableOrThrow(tenantService.resolveTenant(), current.getNamespace(), current.getFlowId(), Optional.ofNullable(revision));
return flowInputOutput.readExecutionInputs(flow, current, inputs)
.flatMap(newInputs -> Mono.fromCallable(() ->
innerReplay(current.withInputs(newInputs), taskRunId, revision, breakpoints)));
}
private Execution innerReplay(Execution execution, @Nullable String taskRunId, @Nullable Integer revision, Optional<String> breakpoints) throws Exception {
Execution replay = executionService.replay(execution, taskRunId, revision)
.withBreakpoints(breakpoints.map(s -> Arrays.stream(s.split(",")).map(Breakpoint::of).toList()).orElse(null));

View File

@@ -1,6 +1,7 @@
package io.kestra.webserver.utils;
import io.kestra.core.exceptions.KestraRuntimeException;
import reactor.core.publisher.Flux;
import java.io.IOException;
import java.io.Writer;
@@ -25,4 +26,33 @@ public class CSVUtils {
throw new KestraRuntimeException("could not convert to CSV", e);
}
}
public static Flux<String> toCSVFlux(Flux<Map<String, Object>> records) {
return records.switchOnFirst((signal, flux) -> {
if (!signal.hasValue()) {
return Flux.empty();
}
Map<String, Object> first = signal.get();
// Create the header from the keys of the first record
String header = String.join(",", first.keySet()) + "\n";
Flux<String> headerFlux = Flux.just(header);
// Content of the CSV
Flux<String> rowsFlux = flux.map(record ->
record.values().stream()
.map(v -> v != null ? v.toString() : "")
.map(CSVUtils::escapeCsv)
.reduce((a, b) -> a + "," + b)
.orElse("") + "\n"
);
return headerFlux.concatWith(rowsFlux);
});
}
private static String escapeCsv(String value) {
if (value.contains(",") || value.contains("\"") || value.contains("\n")) {
value = value.replace("\"", "\"\"");
return "\"" + value + "\"";
}
return value;
}
}

View File

@@ -1,10 +1,14 @@
package io.kestra.webserver.controllers.api;
import com.fasterxml.jackson.core.JsonProcessingException;
import io.kestra.core.junit.annotations.KestraTest;
import io.kestra.core.models.dashboards.Dashboard;
import io.kestra.core.models.executions.ExecutionKind;
import io.kestra.core.models.executions.LogEntry;
import io.kestra.core.repositories.DashboardRepositoryInterface;
import io.kestra.core.repositories.LogRepositoryInterface;
import io.kestra.core.serializers.JacksonMapper;
import io.kestra.core.tenant.TenantService;
import io.kestra.core.utils.IdUtils;
import io.kestra.webserver.models.ChartFiltersOverrides;
import io.kestra.webserver.responses.PagedResults;
@@ -12,8 +16,11 @@ import io.micronaut.core.type.Argument;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.MediaType;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.http.client.exceptions.HttpClientResponseException;
import io.micronaut.http.exceptions.HttpStatusException;
import io.micronaut.reactor.http.client.ReactorHttpClient;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.slf4j.event.Level;
@@ -38,15 +45,208 @@ class DashboardControllerTest {
@Inject
LogRepositoryInterface logRepository;
@Inject
DashboardRepositoryInterface dashboardRepository;
@Test
void full() {
void full() throws JsonProcessingException {
String dashboardYaml = """
id: full
title: Some Dashboard
description: Default overview dashboard
timeWindow:
default: P30D # P30DT30H
max: P365D
charts:
- id: logs_timeseries
type: io.kestra.plugin.core.dashboard.chart.TimeSeries
chartOptions:
displayName: Error Logs
description: Count of ERROR logs per date
legend:
enabled: true
column: date
colorByColumn: level
data:
type: io.kestra.plugin.core.dashboard.data.Logs
columns:
date:
field: DATE
displayName: Execution Date
level:
field: LEVEL
total:
displayName: Total Error Logs
agg: COUNT
graphStyle: BARS
where:
- field: LEVEL
type: IN
values:
- ERROR""";
// Create a dashboard
Dashboard dashboard = client.toBlocking().retrieve(
POST(DASHBOARD_PATH, dashboardYaml).contentType(MediaType.APPLICATION_YAML),
Dashboard.class
);
assertThat(dashboard).isNotNull();
assertThat(dashboard.getId()).isEqualTo("full");
assertThat(dashboard.getTitle()).isEqualTo("Some Dashboard");
assertThat(dashboard.getDescription()).isEqualTo("Default overview dashboard");
// Get a dashboard
Dashboard get = client.toBlocking().retrieve(
GET(DASHBOARD_PATH + "/" + dashboard.getId()),
Dashboard.class
);
assertThat(get).isNotNull();
assertThat(get.getId()).isEqualTo(dashboard.getId());
assertThat(get.getSourceCode()).startsWith("""
id: full
title: Some Dashboard""");
// List dashboards
List<Dashboard> dashboards = client.toBlocking().retrieve(
GET(DASHBOARD_PATH),
Argument.listOf(Dashboard.class)
);
assertThat(dashboards).hasSize(1);
// Compute a dashboard
List<Map> chartData = client.toBlocking().retrieve(
POST(DASHBOARD_PATH + "/" + dashboard.getId() + "/charts/logs_timeseries", ChartFiltersOverrides.builder().filters(Collections.emptyList()).build()),
Argument.listOf(Map.class)
);
assertThat(chartData).isNotNull();
assertThat(chartData).hasSize(1);
// Delete a dashboard
HttpResponse<Void> deleted = client.toBlocking().exchange(
DELETE(DASHBOARD_PATH + "/" + dashboard.getId())
);
assertThat(deleted).isNotNull();
assertThat(deleted.code()).isEqualTo(204);
}
// The goal is to cover the legacy implementation that was autogenerating id so it was present on the backend but the source code didn't contain it.
// We now mandate the id within the dashboard source code and if it's not yet there, the "get" API should add it to the existing source if it's not there so that it's added on the next save.
@Test
void sourceShouldHaveIdAddedIfNotPresent() throws JsonProcessingException {
String dashboardYaml = """
title: Some Dashboard
description: Default overview dashboard
timeWindow:
default: P30D # P30DT30H
max: P365D
charts:
- id: logs_timeseries
type: io.kestra.plugin.core.dashboard.chart.TimeSeries
chartOptions:
displayName: Error Logs
description: Count of ERROR logs per date
legend:
enabled: true
column: date
colorByColumn: level
data:
type: io.kestra.plugin.core.dashboard.data.Logs
columns:
date:
field: DATE
displayName: Execution Date
level:
field: LEVEL
total:
displayName: Total Error Logs
agg: COUNT
graphStyle: BARS
where:
- field: LEVEL
type: IN
values:
- ERROR""";
String dashboardId = "sourceShouldHaveIdAddedIfNotPresent";
dashboardRepository.save(JacksonMapper.ofYaml().readValue(dashboardYaml, Dashboard.class).toBuilder().tenantId(TenantService.MAIN_TENANT).id(dashboardId).build(), dashboardYaml);
Dashboard repositoryDashboard = dashboardRepository.get(TenantService.MAIN_TENANT, dashboardId).get();
assertThat(repositoryDashboard.getId()).isEqualTo(dashboardId);
assertThat(repositoryDashboard.getSourceCode()).doesNotContain("id: " + dashboardId);
// Get a dashboard
Dashboard get = client.toBlocking().retrieve(
GET(DASHBOARD_PATH + "/" + dashboardId),
Dashboard.class
);
assertThat(get).isNotNull();
assertThat(get.getId()).isEqualTo(dashboardId);
assertThat(get.getSourceCode()).contains("id: " + dashboardId);
}
@Test
void cantHaveMultipleDashboardsWithSameId() {
String dashboardYaml = """
id: cantHaveMultipleDashboardsWithSameId
title: Some Dashboard
description: Default overview dashboard
timeWindow:
default: P30D # P30DT30H
max: P365D
charts:
- id: logs_timeseries
type: io.kestra.plugin.core.dashboard.chart.TimeSeries
chartOptions:
displayName: Error Logs
description: Count of ERROR logs per date
legend:
enabled: true
column: date
colorByColumn: level
data:
type: io.kestra.plugin.core.dashboard.data.Logs
columns:
date:
field: DATE
displayName: Execution Date
level:
field: LEVEL
total:
displayName: Total Error Logs
agg: COUNT
graphStyle: BARS
where:
- field: LEVEL
type: IN
values:
- ERROR""";
client.toBlocking().retrieve(
POST(DASHBOARD_PATH, dashboardYaml).contentType(MediaType.APPLICATION_YAML),
Dashboard.class
);
HttpClientResponseException httpClientResponseException = Assertions.assertThrows(HttpClientResponseException.class, () -> client.toBlocking().retrieve(
POST(DASHBOARD_PATH, dashboardYaml).contentType(MediaType.APPLICATION_YAML),
Dashboard.class
));
assertThat(httpClientResponseException.getStatus().getCode()).isEqualTo(422);
assertThat(httpClientResponseException.getMessage()).isEqualTo("Invalid entity: dashboard.id: Dashboard id already exists");
}
@Test
void update() {
String dashboardYaml = """
id: update
title: Some Dashboard
description: Default overview dashboard
timeWindow:
default: P30D # P30DT30H
max: P365D
charts:
- id: logs_timeseries
type: io.kestra.plugin.core.dashboard.chart.TimeSeries
@@ -85,35 +285,87 @@ class DashboardControllerTest {
assertThat(dashboard.getTitle()).isEqualTo("Some Dashboard");
assertThat(dashboard.getDescription()).isEqualTo("Default overview dashboard");
// Get a dashboard
Dashboard get = client.toBlocking().retrieve(
GET(DASHBOARD_PATH + "/" + dashboard.getId()),
Dashboard.class
);
assertThat(get).isNotNull();
assertThat(get.getId()).isEqualTo(dashboard.getId());
assertThat(dashboard.getDescription()).isEqualTo("Default overview dashboard");
// List dashboards
List<Dashboard> dashboards = client.toBlocking().retrieve(
GET(DASHBOARD_PATH),
Argument.listOf(Dashboard.class)
// Update a dashboard
dashboard = client.toBlocking().retrieve(
PUT(DASHBOARD_PATH + "/" + dashboard.getId(), dashboardYaml.replace("Default overview dashboard", "Another description")).contentType(MediaType.APPLICATION_YAML),
Dashboard.class
);
assertThat(dashboards).hasSize(1);
assertThat(dashboard).isNotNull();
// Compute a dashboard
List<Map> chartData = client.toBlocking().retrieve(
POST(DASHBOARD_PATH + "/" + dashboard.getId() + "/charts/logs_timeseries", ChartFiltersOverrides.builder().filters(Collections.emptyList()).build()),
Argument.listOf(Map.class)
get = client.toBlocking().retrieve(
GET(DASHBOARD_PATH + "/" + dashboard.getId()),
Dashboard.class
);
assertThat(chartData).isNotNull();
assertThat(chartData).hasSize(1);
assertThat(get).isNotNull();
assertThat(dashboard.getDescription()).isEqualTo("Another description");
// Delete a dashboard
HttpResponse<Void> deleted = client.toBlocking().exchange(
DELETE(DASHBOARD_PATH + "/" + dashboard.getId())
Dashboard finalDashboard = dashboard;
HttpClientResponseException httpStatusException = Assertions.assertThrows(HttpClientResponseException.class, () -> client.toBlocking().retrieve(
PUT(DASHBOARD_PATH + "/" + finalDashboard.getId(), dashboardYaml.replace(finalDashboard.getId(), finalDashboard.getId() + "-updated")).contentType(MediaType.APPLICATION_YAML)
, Dashboard.class));
assertThat(httpStatusException.getStatus().getCode()).isEqualTo(422);
assertThat(httpStatusException.getMessage()).isEqualTo("Invalid entity: dashboard.id: Illegal dashboard id update");
get = client.toBlocking().retrieve(
GET(DASHBOARD_PATH + "/" + dashboard.getId()),
Dashboard.class
);
assertThat(deleted).isNotNull();
assertThat(deleted.code()).isEqualTo(204);
assertThat(get).isNotNull();
assertThat(dashboard.getSourceCode()).contains("id: " + dashboard.getId());
assertThat(dashboard.getDescription()).isEqualTo("Another description");
}
@Test
void mandatoryId() {
String dashboardYaml = """
title: Some Dashboard
description: Default overview dashboard
timeWindow:
default: P30D # P30DT30H
max: P365D
charts:
- id: logs_timeseries
type: io.kestra.plugin.core.dashboard.chart.TimeSeries
chartOptions:
displayName: Error Logs
description: Count of ERROR logs per date
legend:
enabled: true
column: date
colorByColumn: level
data:
type: io.kestra.plugin.core.dashboard.data.Logs
columns:
date:
field: DATE
displayName: Execution Date
level:
field: LEVEL
total:
displayName: Total Error Logs
agg: COUNT
graphStyle: BARS
where:
- field: LEVEL
type: IN
values:
- ERROR""";
// Create a dashboard
HttpClientResponseException httpClientResponseException = Assertions.assertThrows(HttpClientResponseException.class, () -> client.toBlocking().retrieve(
POST(DASHBOARD_PATH, dashboardYaml).contentType(MediaType.APPLICATION_YAML),
Dashboard.class
));
assertThat(httpClientResponseException.getStatus().getCode()).isEqualTo(422);
assertThat(httpClientResponseException.getMessage()).isEqualTo("Illegal argument: Dashboard id is mandatory");
}
@Test
@@ -136,6 +388,7 @@ class DashboardControllerTest {
.build());
String dashboardYaml = """
id: exportACustomDashboardChartToCsv
title: A dashboard with a simple table
timeWindow:
default: P30D # P30DT30H
@@ -238,4 +491,4 @@ class DashboardControllerTest {
var csv = new String(csvBytes, StandardCharsets.UTF_8);
assertThat(csv).isEqualTo("chart_namespace,chart_execution_id\r\n%s,%s\r\n".formatted(fakeNamespace, fakeExecutionId));
}
}
}

View File

@@ -13,6 +13,7 @@ import io.kestra.core.models.executions.ExecutionKilledExecution;
import io.kestra.core.models.executions.TaskRun;
import io.kestra.core.models.flows.Flow;
import io.kestra.core.models.flows.State;
import io.kestra.core.models.flows.State.Type;
import io.kestra.core.models.storage.FileMetas;
import io.kestra.core.queues.QueueException;
import io.kestra.core.queues.QueueFactoryInterface;
@@ -447,6 +448,106 @@ class ExecutionControllerRunnerTest {
.forEach(state -> assertThat(state.getCurrent()).isEqualTo(State.Type.SUCCESS));
}
@Test
@LoadFlows({"flows/valids/condition_with_input.yaml"})
void restartExecutionWithNewInputs() throws Exception {
final String flowId = "condition_with_input";
// Run execution until it ends
Execution parentExecution = runnerUtils.runOne(TENANT_ID, TESTS_FLOW_NS, flowId, null,
(flow, execution1) -> flowIO.readExecutionInputs(flow, execution1, Map.of("condition", "fail")));
assertThat(parentExecution.getState().getCurrent()).isEqualTo(Type.FAILED);
Optional<Flow> flow = flowRepositoryInterface.findById(TENANT_ID, TESTS_FLOW_NS, flowId);
assertThat(flow.isPresent()).isTrue();
// Run child execution starting from a specific task and wait until it finishes
Execution finishedChildExecution = runnerUtils.awaitChildExecution(
flow.get(),
parentExecution, throwRunnable(() -> {
Thread.sleep(100);
MultipartBody multipartBody = MultipartBody.builder()
.addPart("condition", "success")
.build();
Execution replay = client.toBlocking().retrieve(
HttpRequest
.POST("/api/v1/main/executions/" + parentExecution.getId() + "/replay-with-inputs", multipartBody)
.contentType(MediaType.MULTIPART_FORM_DATA_TYPE),
Execution.class
);
assertThat(replay).isNotNull();
assertThat(replay.getParentId()).isEqualTo(parentExecution.getId());
assertThat(replay.getState().getCurrent()).isEqualTo(Type.CREATED);
}),
Duration.ofSeconds(15));
assertThat(finishedChildExecution).isNotNull();
assertThat(finishedChildExecution.getParentId()).isEqualTo(parentExecution.getId());
assertThat(finishedChildExecution.getTaskRunList().size()).isEqualTo(2);
finishedChildExecution
.getTaskRunList()
.stream()
.map(TaskRun::getState)
.forEach(state -> assertThat(state.getCurrent()).isIn(State.Type.SUCCESS, State.Type.SKIPPED));
}
@Test
@LoadFlows({"flows/valids/condition_with_input.yaml"})
void restartExecutionFromTaskIdWithInputs() throws Exception {
final String flowId = "condition_with_input";
final String referenceTaskId = "fail";
// Run execution until it ends
Execution parentExecution = runnerUtils.runOne(TENANT_ID, TESTS_FLOW_NS, flowId, null,
(flow, execution1) -> flowIO.readExecutionInputs(flow, execution1, Map.of("condition", "fail")));
assertThat(parentExecution.getState().getCurrent()).isEqualTo(Type.FAILED);
Optional<Flow> flow = flowRepositoryInterface.findById(TENANT_ID, TESTS_FLOW_NS, flowId);
assertThat(flow.isPresent()).isTrue();
// Run child execution starting from a specific task and wait until it finishes
Execution finishedChildExecution = runnerUtils.awaitChildExecution(
flow.get(),
parentExecution, throwRunnable(() -> {
Thread.sleep(100);
MultipartBody multipartBody = MultipartBody.builder()
.addPart("condition", "success")
.build();
Execution replay = client.toBlocking().retrieve(
HttpRequest
.POST("/api/v1/main/executions/" + parentExecution.getId() + "/replay-with-inputs?taskRunId=" + parentExecution.findTaskRunByTaskIdAndValue(referenceTaskId, List.of()).getId(), multipartBody)
.contentType(MediaType.MULTIPART_FORM_DATA_TYPE),
Execution.class
);
assertThat(replay).isNotNull();
assertThat(replay.getParentId()).isEqualTo(parentExecution.getId());
assertThat(replay.getTaskRunList().size()).isEqualTo(2);
assertThat(replay.getState().getCurrent()).isEqualTo(State.Type.RESTARTED);
}),
Duration.ofSeconds(15));
assertThat(finishedChildExecution).isNotNull();
assertThat(finishedChildExecution.getParentId()).isEqualTo(parentExecution.getId());
assertThat(finishedChildExecution.getTaskRunList().size()).isEqualTo(2);
finishedChildExecution
.getTaskRunList()
.stream()
.map(TaskRun::getState)
.forEach(state -> assertThat(state.getCurrent()).isIn(State.Type.SUCCESS, State.Type.SKIPPED));
}
@Test
@LoadFlows({"flows/valids/restart-each.yaml"})
void restartExecutionFromTaskIdWithSequential() throws Exception {
@@ -866,11 +967,11 @@ class ExecutionControllerRunnerTest {
}
@Test
@LoadFlows({"flows/valids/pause.yaml"})
@LoadFlows({"flows/valids/pause-test.yaml"})
@SuppressWarnings("unchecked")
void resumeExecutionPaused() throws TimeoutException, InterruptedException, QueueException, InternalException {
// Run execution until it is paused
Execution pausedExecution = runnerUtils.runOneUntilPaused(TENANT_ID, TESTS_FLOW_NS, "pause");
Execution pausedExecution = runnerUtils.runOneUntilPaused(TENANT_ID, TESTS_FLOW_NS, "pause-test");
assertThat(pausedExecution.getState().isPaused()).isTrue();
// resume the execution
@@ -938,10 +1039,10 @@ class ExecutionControllerRunnerTest {
}
@Test
@LoadFlows({"flows/valids/pause.yaml"})
@LoadFlows({"flows/valids/pause-test.yaml"})
void resumeExecutionByIds() throws TimeoutException, InterruptedException, QueueException {
Execution pausedExecution1 = runnerUtils.runOneUntilPaused(TENANT_ID, TESTS_FLOW_NS, "pause");
Execution pausedExecution2 = runnerUtils.runOneUntilPaused(TENANT_ID, TESTS_FLOW_NS, "pause");
Execution pausedExecution1 = runnerUtils.runOneUntilPaused(TENANT_ID, TESTS_FLOW_NS, "pause-test");
Execution pausedExecution2 = runnerUtils.runOneUntilPaused(TENANT_ID, TESTS_FLOW_NS, "pause-test");
assertThat(pausedExecution1.getState().isPaused()).isTrue();
assertThat(pausedExecution2.getState().isPaused()).isTrue();
@@ -972,10 +1073,10 @@ class ExecutionControllerRunnerTest {
}
@Test
@LoadFlows({"flows/valids/pause.yaml"})
@LoadFlows({"flows/valids/pause-test.yaml"})
void resumeExecutionByQuery() throws TimeoutException, InterruptedException, QueueException {
Execution pausedExecution1 = runnerUtils.runOneUntilPaused(TENANT_ID, TESTS_FLOW_NS, "pause");
Execution pausedExecution2 = runnerUtils.runOneUntilPaused(TENANT_ID, TESTS_FLOW_NS, "pause");
Execution pausedExecution1 = runnerUtils.runOneUntilPaused(TENANT_ID, TESTS_FLOW_NS, "pause-test");
Execution pausedExecution2 = runnerUtils.runOneUntilPaused(TENANT_ID, TESTS_FLOW_NS, "pause-test");
assertThat(pausedExecution1.getState().isPaused()).isTrue();
assertThat(pausedExecution2.getState().isPaused()).isTrue();
@@ -1164,10 +1265,10 @@ class ExecutionControllerRunnerTest {
}
@RetryingTest(5)
@LoadFlows({"flows/valids/pause.yaml"})
@LoadFlows({"flows/valids/pause-test.yaml"})
void killExecutionPaused() throws TimeoutException, InterruptedException, QueueException {
// Run execution until it is paused
Execution pausedExecution = runnerUtils.runOneUntilPaused(TENANT_ID, TESTS_FLOW_NS, "pause");
Execution pausedExecution = runnerUtils.runOneUntilPaused(TENANT_ID, TESTS_FLOW_NS, "pause-test");
assertThat(pausedExecution.getState().isPaused()).isTrue();
// resume the execution
@@ -1726,10 +1827,10 @@ class ExecutionControllerRunnerTest {
}
@Test
@LoadFlows({"flows/valids/pause.yaml"})
@LoadFlows({"flows/valids/pause-test.yaml"})
void shouldForceRunExecutionAPausedFlow() throws QueueException, TimeoutException {
// Run execution until it is paused
Execution result = runnerUtils.runOneUntilPaused(TENANT_ID, TESTS_FLOW_NS, "pause");
Execution result = runnerUtils.runOneUntilPaused(TENANT_ID, TESTS_FLOW_NS, "pause-test");
var response = client.toBlocking().exchange(HttpRequest.POST("/api/v1/main/executions/" + result.getId() + "/force-run", null));
assertThat(response.getStatus().getCode()).isEqualTo(HttpStatus.OK.getCode());