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",