mirror of
https://github.com/kestra-io/kestra.git
synced 2025-12-25 11:12:12 -05:00
Compare commits
21 Commits
feat/execu
...
fix/dashbo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c8a21c2e3 | ||
|
|
17e61bf687 | ||
|
|
c4022d2e3c | ||
|
|
ee48865706 | ||
|
|
f7a23ae459 | ||
|
|
a13909337e | ||
|
|
502f0362e3 | ||
|
|
dbaa35370f | ||
|
|
59a93b2ab9 | ||
|
|
bff8026ebb | ||
|
|
4481318023 | ||
|
|
c8b33dd690 | ||
|
|
05b485e6cc | ||
|
|
78a489882f | ||
|
|
b872223995 | ||
|
|
e3d2b93c6b | ||
|
|
1699403c95 | ||
|
|
b3fa5ead6d | ||
|
|
d4e7b0cde4 | ||
|
|
5da4d88738 | ||
|
|
d60ec87375 |
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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' }}"
|
||||
@@ -1,4 +1,4 @@
|
||||
id: pause
|
||||
id: pause-test
|
||||
namespace: io.kestra.tests
|
||||
|
||||
tasks:
|
||||
@@ -1,4 +1,4 @@
|
||||
id: pause
|
||||
id: pause-test
|
||||
namespace: sanitychecks.core
|
||||
|
||||
tasks:
|
||||
@@ -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'
|
||||
|
||||
@@ -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();
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
) {
|
||||
|
||||
@@ -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>
|
||||
40
ui/src/components/kv/InheritedKVs.vue
Normal file
40
ui/src/components/kv/InheritedKVs.vue
Normal 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>
|
||||
@@ -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">
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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?"
|
||||
|
||||
@@ -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}'"
|
||||
|
||||
@@ -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}'"
|
||||
|
||||
@@ -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 अपडेट करें"
|
||||
|
||||
@@ -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}'"
|
||||
|
||||
@@ -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}'の値を更新"
|
||||
|
||||
@@ -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}'의 값을 업데이트하세요"
|
||||
|
||||
@@ -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}'"
|
||||
|
||||
@@ -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}'"
|
||||
|
||||
@@ -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}'"
|
||||
|
||||
@@ -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}' 的值"
|
||||
|
||||
@@ -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)]},
|
||||
};
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user