From 935acfc90e3ad4e17caa6d92d0ab9e4e177d420e Mon Sep 17 00:00:00 2001 From: Loakesh Indiran Date: Wed, 4 Nov 2026 11:08:59 +0530 Subject: [PATCH] fix(templates): put Channel first in the preview row and add All kinds to Applies to The preview row now shows Channel, Severity and then Kind, so the first dropdown is the channel list again. The kind dropdown is labelled Kind. The Applies to select lists All kinds first. It is ticked when the list is empty, ticking it clears the chosen kinds, and ticking a kind while it is ticked selects that kind alone. The stored value stays applies_to []. --- web/src/components/alerts/AddTemplate.spec.ts | 36 ++++++++++++++++++- web/src/components/alerts/AddTemplate.vue | 16 ++++++--- .../TemplatePreviewPanel.spec.ts | 32 +++++++++++++++++ .../template-content/TemplatePreviewPanel.vue | 16 ++++----- web/src/locales/languages/en-US.json | 2 +- 5 files changed, 88 insertions(+), 14 deletions(-) diff --git a/web/src/components/alerts/AddTemplate.spec.ts b/web/src/components/alerts/AddTemplate.spec.ts index 9101e1448c..342abc4216 100644 --- a/web/src/components/alerts/AddTemplate.spec.ts +++ b/web/src/components/alerts/AddTemplate.spec.ts @@ -990,6 +990,40 @@ describe("AddTemplate - applies to", () => { expect(sent.data.applies_to).toEqual(["anomaly", "on_call"]); }); + it("lists All kinds first and ticking it clears the chosen kinds in one click", async () => { + const w = await mountComp(); + await selectCustomMode(w); + getForm(w).setFieldValue("name", "ops-card"); + await w.findComponent({ name: "QueryEditor" }).vm.$emit("update:query", '{"text":"x"}'); + expect((appliesTo(w).props("options") as any[])[0]).toEqual({ + label: "All kinds", + value: "all_kinds", + }); + expect(appliesTo(w).props("modelValue")).toEqual(["all_kinds"]); + + await appliesTo(w).vm.$emit("update:modelValue", ["anomaly", "incident"]); + await appliesTo(w).vm.$emit("update:modelValue", ["anomaly", "incident", "all_kinds"]); + await flushPromises(); + expect(appliesTo(w).props("modelValue")).toEqual(["all_kinds"]); + expect(w.find('[data-test="add-template-applies-to-mode"]').text()).toBe( + String(i18n.global.t("alert_templates.appliesToManual")), + ); + expect(w.find('[data-test="add-template-applies-to-reset"]').exists()).toBe(true); + + await submit(w); + expect((templateService.create as any).mock.calls[0][0].data.applies_to).toEqual([]); + }); + + it("ticking a kind while All kinds is ticked selects that kind alone", async () => { + const w = await mountComp(); + await selectCustomMode(w); + expect(appliesTo(w).props("modelValue")).toEqual(["all_kinds"]); + + await appliesTo(w).vm.$emit("update:modelValue", ["all_kinds", "synthetics"]); + await flushPromises(); + expect(appliesTo(w).props("modelValue")).toEqual(["synthetics"]); + }); + it("an existing template starts in manual mode with its stored list", async () => { const w = await mountComp({ template: { @@ -1008,7 +1042,7 @@ describe("AddTemplate - applies to", () => { const w = await mountComp({ template: { name: "ops-card", body: '{"text":"x"}', type: "http", title: "", applies_to: [] }, }); - expect(appliesTo(w).props("modelValue")).toEqual([]); + expect(appliesTo(w).props("modelValue")).toEqual(["all_kinds"]); expect(w.find('[data-test="add-template-applies-to-mode"]').text()).toBe( String(i18n.global.t("alert_templates.appliesToInferred")), ); diff --git a/web/src/components/alerts/AddTemplate.vue b/web/src/components/alerts/AddTemplate.vue index 9d9d809712..17b210190e 100644 --- a/web/src/components/alerts/AddTemplate.vue +++ b/web/src/components/alerts/AddTemplate.vue @@ -80,7 +80,7 @@ along with this program. If not, see .
([]); const appliesToMode = ref<"inferred" | "manual">("inferred"); const appliesToError = ref(null); const isPrebuiltTemplate = computed(() => !!props.template?.isPrebuilt && !props.isClone); -const appliesToOptions = computed(() => - NOTIFICATION_KINDS.map((kind) => ({ label: t(kindLabelKey(kind)), value: kind })), +// An empty list is stored for every kind; the select shows it as the ticked "All kinds" item. +const ALL_KINDS = "all_kinds"; +const appliesToOptions = computed(() => [ + { label: t("alert_templates.appliesToAll"), value: ALL_KINDS }, + ...NOTIFICATION_KINDS.map((kind) => ({ label: t(kindLabelKey(kind)), value: kind })), +]); +const appliesToSelection = computed(() => + appliesTo.value.length ? appliesTo.value : [ALL_KINDS], ); const isKind = (k: unknown): k is NotificationKind => NOTIFICATION_KINDS.includes(k as NotificationKind); @@ -491,7 +497,9 @@ const inferenceSource = computed(() => ); const onAppliesToChange = (value: unknown) => { - appliesTo.value = NOTIFICATION_KINDS.filter((k) => Array.isArray(value) && value.includes(k)); + const picked = Array.isArray(value) ? value : []; + const allKindsJustTicked = picked.includes(ALL_KINDS) && appliesTo.value.length > 0; + appliesTo.value = allKindsJustTicked ? [] : NOTIFICATION_KINDS.filter((k) => picked.includes(k)); appliesToMode.value = "manual"; appliesToError.value = null; }; diff --git a/web/src/components/alerts/template-content/TemplatePreviewPanel.spec.ts b/web/src/components/alerts/template-content/TemplatePreviewPanel.spec.ts index ca9a3eda7a..755841bcde 100644 --- a/web/src/components/alerts/template-content/TemplatePreviewPanel.spec.ts +++ b/web/src/components/alerts/template-content/TemplatePreviewPanel.spec.ts @@ -270,6 +270,38 @@ describe("TemplatePreviewPanel", () => { expect(button.text()).toBe("View in OpenObserve"); }); + it("puts Channel first, Severity next and Kind last in the preview row", async () => { + vi.spyOn(templateService, "preview").mockResolvedValue(makePreviewResponse() as any); + const w = mount(TemplatePreviewPanel, { + props: { spec: emptyContentSpec(), kinds: ["alert", "synthetics"] }, + global: { plugins: [i18n, makeStore()] }, + }); + await vi.advanceTimersByTimeAsync(300); + await flushPromises(); + + const selects = w + .find('[data-test="template-preview-panel-channel-select"]') + .element.parentElement!.querySelectorAll(":scope > [data-test]"); + expect(Array.from(selects).map((el) => el.getAttribute("data-test"))).toEqual([ + "template-preview-panel-channel-select", + "template-preview-panel-severity-select", + "template-preview-panel-kind-select", + ]); + const labels = w + .findAllComponents({ name: "OSelect" }) + .slice(0, 3) + .map((c) => c.props("label")); + expect(labels).toEqual(["Channel", "Severity", "Kind"]); + expect( + ( + w + .find('[data-test="template-preview-panel-channel-select"]') + .findComponent({ name: "OSelect" }) + .props("options") as any[] + ).length, + ).toBe(10); + }); + it("previews as the first selected kind and sends kind on each request", async () => { const previewSpy = vi .spyOn(templateService, "preview") diff --git a/web/src/components/alerts/template-content/TemplatePreviewPanel.vue b/web/src/components/alerts/template-content/TemplatePreviewPanel.vue index a6b169659e..b44f73798c 100644 --- a/web/src/components/alerts/template-content/TemplatePreviewPanel.vue +++ b/web/src/components/alerts/template-content/TemplatePreviewPanel.vue @@ -46,14 +46,6 @@ along with this program. If not, see . {{ t("alert_templates.previewGroupLabel") }}
- . :searchable="false" data-test="template-preview-panel-severity-select" /> +
diff --git a/web/src/locales/languages/en-US.json b/web/src/locales/languages/en-US.json index 30191475d5..ee9a4eaec5 100644 --- a/web/src/locales/languages/en-US.json +++ b/web/src/locales/languages/en-US.json @@ -13805,7 +13805,7 @@ "appliesToInferred": "Inferred from the variables in the body", "appliesToManual": "Chosen by you", "appliesToReset": "Reset to inferred", - "previewAs": "Preview as", + "previewKind": "Kind", "previewForbidden": "You do not have permission to preview templates", "previewFailed": "Preview failed: {message}", "severityFailed": "Failed",