Skip to content

Commit 59d5d4c

Browse files
feat(dashboard): mandatory id + add autogenerated id to source for legacy handling (#10912)
closes kestra-io/kestra-ee#4484
1 parent e8ee3b0 commit 59d5d4c

File tree

7 files changed

+321
-19
lines changed

7 files changed

+321
-19
lines changed

ui/scripts/product/flow.ts renamed to ui/scripts/id.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,4 @@ const ANIMALS: string[] = [
2525
const getRandomNumber = (minimum: number = MINIMUM, maximum: number = MAXIMUM): number => Math.floor(Math.random() * (maximum - minimum + 1)) + minimum;
2626
const getRandomAnimal = (): string => ANIMALS[Math.floor(Math.random() * ANIMALS.length)];
2727

28-
export const getRandomFlowID = (): string => `${getRandomAnimal()}_${getRandomNumber()}`.toLowerCase();
28+
export const getRandomID = (): string => `${getRandomAnimal()}_${getRandomNumber()}`.toLowerCase();

ui/src/components/dashboard/components/Create.vue

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@
3838
import type {Dashboard} from "../../../components/dashboard/composables/useDashboards";
3939
import {getDashboard, processFlowYaml} from "../../../components/dashboard/composables/useDashboards";
4040
41+
import {getRandomID} from "../../../../scripts/id";
42+
4143
const dashboard = ref<Dashboard>({id: "", charts: []});
4244
const save = async (source: string) => {
4345
const response = await dashboardStore.create(source)
@@ -69,6 +71,8 @@
6971
} else {
7072
dashboard.value.sourceCode = name === "namespaces/update" ? YAML_NAMESPACE : YAML_MAIN;
7173
}
74+
75+
dashboard.value.sourceCode = "id: " + getRandomID() + "\n" + dashboard.value.sourceCode;
7276
}
7377
});
7478

ui/src/components/flows/FlowCreate.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import {useBlueprintsStore} from "../../stores/blueprints";
1717
import {useCoreStore} from "../../stores/core";
1818
19-
import {getRandomFlowID} from "../../../scripts/product/flow";
19+
import {getRandomID} from "../../../scripts/id";
2020
2121
export default {
2222
mixins: [RouteContext],
@@ -52,7 +52,7 @@
5252
} else {
5353
const defaultNamespace = localStorage.getItem(storageKeys.DEFAULT_NAMESPACE);
5454
const selectedNamespace = this.$route.query.namespace || defaultNamespace || "company.team";
55-
flowYaml = `id: ${getRandomFlowID()}
55+
flowYaml = `id: ${getRandomID()}
5656
namespace: ${selectedNamespace}
5757
5858
tasks:

ui/src/override/components/dashboard/Edit.vue

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,20 @@
22
<TopNavBar :title="header.title" :breadcrumb="header.breadcrumb" />
33
<section class="full-container">
44
<Editor
5-
v-if="dashboard.sourceCode"
6-
:initial-source="dashboard.sourceCode"
5+
v-if="sourceCode"
6+
v-model="sourceCode"
77
@save="save"
88
/>
99
</section>
1010
</template>
1111

1212
<script setup lang="ts">
13-
import {onMounted, computed, ref} from "vue";
13+
import {onMounted, computed, ref, watch} from "vue";
1414
1515
import {useRoute} from "vue-router";
16+
17+
import * as YAML_UTILS from "@kestra-io/ui-libs/flow-yaml-utils";
18+
1619
const route = useRoute();
1720
1821
import {useCoreStore} from "../../../stores/core";
@@ -21,6 +24,8 @@
2124
import {useDashboardStore} from "../../../stores/dashboard";
2225
const dashboardStore = useDashboardStore();
2326
27+
const sourceCode = ref("");
28+
2429
import {useI18n} from "vue-i18n";
2530
const {t} = useI18n({useScope: "global"});
2631
@@ -45,9 +50,26 @@
4550
onMounted(() => {
4651
dashboardStore.load(route.params.dashboard as string).then((response) => {
4752
dashboard.value = response;
53+
sourceCode.value = response.sourceCode;
4854
});
4955
});
5056
57+
watch(sourceCode, (newValue) => {
58+
if (YAML_UTILS.parse(newValue).id !== dashboard.value.id) {
59+
const coreStore = useCoreStore();
60+
coreStore.message = {
61+
variant: "error",
62+
title: t("readonly property"),
63+
message: t("dashboards.edition.id readonly"),
64+
};
65+
sourceCode.value = YAML_UTILS.replaceBlockWithPath({
66+
source: sourceCode.value,
67+
path: "id",
68+
newContent: dashboard.value.id
69+
});
70+
}
71+
})
72+
5173
const header = computed(() => ({
5274
title: dashboard.value?.title || route.params.dashboard,
5375
breadcrumb: [{label: t("dashboards.edition.label"), link: {}}],

ui/src/translations/en.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1369,7 +1369,8 @@
13691369
"edition": {
13701370
"label": "Edit Dashboard",
13711371
"confirmation": "Changes in dashboard <code>{title}</code> are saved.",
1372-
"chart": "Edit this chart"
1372+
"chart": "Edit this chart",
1373+
"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."
13731374
},
13741375
"deletion": {
13751376
"confirmation": "Are you sure you want to delete <code>{title}</code> dashboard?"

webserver/src/main/java/io/kestra/webserver/controllers/api/DashboardController.java

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@
88
import io.kestra.core.models.dashboards.charts.DataChart;
99
import io.kestra.core.models.dashboards.charts.DataChartKPI;
1010
import io.kestra.core.models.flows.Flow;
11+
import io.kestra.core.models.validations.ManualConstraintViolation;
1112
import io.kestra.core.models.validations.ModelValidator;
1213
import io.kestra.core.models.validations.ValidateConstraintViolation;
1314
import io.kestra.core.repositories.ArrayListTotal;
1415
import io.kestra.core.repositories.DashboardRepositoryInterface;
1516
import io.kestra.core.repositories.FlowRepositoryInterface;
1617
import io.kestra.core.serializers.YamlParser;
1718
import io.kestra.core.tenant.TenantService;
18-
import io.kestra.core.utils.IdUtils;
1919
import io.kestra.plugin.core.dashboard.chart.Markdown;
2020
import io.kestra.plugin.core.dashboard.chart.Table;
2121
import io.kestra.plugin.core.dashboard.chart.mardown.sources.FlowDescription;
@@ -49,9 +49,8 @@
4949
import java.io.OutputStreamWriter;
5050
import java.time.Duration;
5151
import java.time.ZonedDateTime;
52-
import java.util.List;
53-
import java.util.Map;
54-
import java.util.Optional;
52+
import java.util.*;
53+
import java.util.regex.Pattern;
5554

5655
import static io.kestra.core.utils.DateUtils.validateTimeline;
5756

@@ -60,6 +59,7 @@
6059
@Slf4j
6160
public class DashboardController {
6261
protected static final YamlParser YAML_PARSER = new YamlParser();
62+
public static final Pattern DASHBOARD_ID_PATTERN = Pattern.compile("^id:.*$", Pattern.MULTILINE);
6363

6464
@Inject
6565
private DashboardRepositoryInterface dashboardRepository;
@@ -91,7 +91,12 @@ public PagedResults<Dashboard> searchDashboards(
9191
public Dashboard getDashboard(
9292
@Parameter(description = "The dashboard id") @PathVariable String id
9393
) throws ConstraintViolationException {
94-
return dashboardRepository.get(tenantService.resolveTenant(), id).orElse(null);
94+
return dashboardRepository.get(tenantService.resolveTenant(), id).map(d -> {
95+
if (!DASHBOARD_ID_PATTERN.matcher(d.getSourceCode()).matches()) {
96+
return d.toBuilder().sourceCode("id: " + d.getId() + "\n" + d.getSourceCode()).build();
97+
}
98+
return d;
99+
}).orElse(null);
95100
}
96101

97102
@ExecuteOn(TaskExecutors.IO)
@@ -101,13 +106,24 @@ public HttpResponse<Dashboard> createDashboard(
101106
@RequestBody(description = "The dashboard definition as YAML") @Body String dashboard
102107
) throws ConstraintViolationException {
103108
Dashboard dashboardParsed = parseDashboard(dashboard);
109+
110+
if (dashboardParsed.getId() == null) {
111+
throw new IllegalArgumentException("Dashboard id is mandatory");
112+
}
104113
modelValidator.validate(dashboardParsed);
105114

106-
if (dashboardParsed.getId() != null) {
107-
throw new IllegalArgumentException("Dashboard id is not editable");
115+
Optional<Dashboard> existingDashboard = dashboardRepository.get(tenantService.resolveTenant(), dashboardParsed.getId());
116+
if (existingDashboard.isPresent()) {
117+
throw new ConstraintViolationException(Collections.singleton(ManualConstraintViolation.of(
118+
"Dashboard id already exists",
119+
dashboardParsed,
120+
Dashboard.class,
121+
"dashboard.id",
122+
dashboardParsed.getId()
123+
)));
108124
}
109125

110-
return HttpResponse.ok(this.save(null, dashboardParsed.toBuilder().id(IdUtils.create()).build(), dashboard));
126+
return HttpResponse.ok(this.save(null, dashboardParsed, dashboard));
111127
}
112128

113129
@ExecuteOn(TaskExecutors.IO)
@@ -148,6 +164,15 @@ public HttpResponse<Dashboard> updateDashboard(
148164
return HttpResponse.status(HttpStatus.NOT_FOUND);
149165
}
150166
Dashboard dashboardToSave = parseDashboard(dashboard);
167+
if (!dashboardToSave.getId().equals(id)) {
168+
throw new ConstraintViolationException(Set.of(ManualConstraintViolation.of(
169+
"Illegal dashboard id update",
170+
dashboardToSave,
171+
Dashboard.class,
172+
"dashboard.id",
173+
dashboardToSave.getId()
174+
)));
175+
}
151176
modelValidator.validate(dashboardToSave);
152177

153178
return HttpResponse.ok(this.save(existingDashboard.get(), dashboardToSave, dashboard));

0 commit comments

Comments
 (0)