fix: alert timezone fix on main (#13519)

This commit is contained in:
Sai Nikhil
2026-08-04 09:36:56 +00:00
committed by GitHub
parent 1ec3b5d0dc
commit 76f1df06fa
5 changed files with 133 additions and 11 deletions
@@ -175,6 +175,13 @@ vi.mock("@/utils/zincutils", () => ({
}),
isAboveMinRefreshInterval: vi.fn(() => true),
describeCron: vi.fn((cron: string) => `Every ${cron}`),
resolveBrowserTimezone: vi.fn((tz: string) => {
if (typeof tz === "string" && tz.toLowerCase().startsWith("browser time")) {
const inner = tz.match(/\(([^)]+)\)/)?.[1]?.trim();
return inner || Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
}
return tz;
}),
}));
describe("QueryConfig.vue", () => {
@@ -318,6 +325,33 @@ describe("QueryConfig.vue", () => {
}
});
describe("Timezone Options", () => {
it("lists 'Browser Time' and 'UTC' shortcuts at the top of the options", () => {
const options = wrapper.vm.filteredTimezones;
expect(options[0].startsWith("Browser Time (")).toBe(true);
expect(options[1]).toBe("UTC");
expect(options.length).toBeGreaterThan(2);
});
it("onCronTimezoneChange stores 'UTC' unchanged", async () => {
wrapper.vm.onCronTimezoneChange("UTC");
await nextTick();
expect(wrapper.vm.cronTimezone).toBe("UTC");
});
it("onCronTimezoneChange passes a raw IANA zone through unchanged", async () => {
wrapper.vm.onCronTimezoneChange("Asia/Kolkata");
await nextTick();
expect(wrapper.vm.cronTimezone).toBe("Asia/Kolkata");
});
it("onCronTimezoneChange resolves a 'Browser Time (<zone>)' pick to the inner IANA zone", async () => {
wrapper.vm.onCronTimezoneChange("Browser Time (America/Los_Angeles)");
await nextTick();
expect(wrapper.vm.cronTimezone).toBe("America/Los_Angeles");
});
});
describe("Initialization", () => {
it("should mount component successfully", () => {
expect(wrapper.exists()).toBe(true);
@@ -1620,6 +1620,7 @@ import {
isAboveMinRefreshInterval,
describeCron,
getImageURL,
resolveBrowserTimezone,
} from "@/utils/zincutils";
import hljs from "highlight.js/lib/core";
import sql from "highlight.js/lib/languages/sql";
@@ -2559,13 +2560,16 @@ export default defineComponent({
// already seeds `timezone: "UTC"`, so the control is never blank anyway.
const initTimezones = () => {
try {
const browserTz = Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
// @ts-ignore
if (typeof Intl !== "undefined" && typeof Intl.supportedValuesOf === "function") {
// @ts-ignore
filteredTimezones.value = Intl.supportedValuesOf("timeZone");
} else {
filteredTimezones.value = [cronTimezone.value || "UTC"];
}
const zones: string[] =
typeof Intl !== "undefined" && typeof Intl.supportedValuesOf === "function"
? // @ts-ignore
Intl.supportedValuesOf("timeZone")
: [cronTimezone.value || "UTC"];
// Convenience shortcuts first (matching the reports picker), then every
// IANA zone. This only populates OPTIONS — it must not seed cronTimezone.
filteredTimezones.value = [`Browser Time (${browserTz})`, "UTC", ...zones];
} catch {
filteredTimezones.value = ["UTC"];
}
@@ -2673,7 +2677,10 @@ export default defineComponent({
const onCronTimezoneChange = (value: any) => {
isUserTriggerChange.value = true;
cronTimezone.value = value;
// "Browser Time (<zone>)" is a display-only shortcut; persist the resolved
// IANA zone so trigger_condition.timezone stays a value the backend can
// parse (storing the raw label produces a NaN tz_offset).
cronTimezone.value = resolveBrowserTimezone(value);
validateCron();
emitTriggerUpdate();
};
+15 -4
View File
@@ -46,6 +46,7 @@ import {
smartDecodeVrlFunction,
isValidResourceName,
getTimezonesByOffset,
resolveBrowserTimezone,
} from "@/utils/zincutils";
import { convertDateToTimestamp } from "@/utils/date";
import { generateSqlQuery } from "@/utils/alerts/alertQueryBuilder";
@@ -2107,11 +2108,16 @@ export function useAlertForm(props: AlertFormProps, emit: AlertFormEmit) {
const minutes = String(now.getMinutes()).padStart(2, "0");
const time = `${hours}:${minutes}`;
const convertedDateTime = convertDateToTimestamp(
date,
time,
formData.value.trigger_condition.timezone,
// Resolve any lingering "Browser Time (<zone>)" label (e.g. an existing
// alert opened from an older release) to a plain IANA zone before deriving
// the offset — otherwise convertDateToTimestamp returns NaN and tz_offset
// serializes to null in the saved payload.
const resolvedTimezone = resolveBrowserTimezone(
formData.value?.trigger_condition?.timezone ?? "",
);
setF("trigger_condition.timezone", resolvedTimezone);
const convertedDateTime = convertDateToTimestamp(date, time, resolvedTimezone);
setF("tz_offset", convertedDateTime.offset);
}
@@ -2341,6 +2347,11 @@ export function useAlertForm(props: AlertFormProps, emit: AlertFormEmit) {
// Resolved async AFTER the form.reset below → setF in the .then.
pendingTimezoneOffset = data.tz_offset;
}
} else {
// Heal legacy alerts (e.g. created on older releases) that persisted a
// "Browser Time (<zone>)" label — resolve it to a plain IANA zone so the
// picker shows a valid value and the save path computes a real offset.
data.trigger_condition.timezone = resolveBrowserTimezone(data.trigger_condition.timezone);
}
if (data.query_condition.vrl_function) {
+55
View File
@@ -40,6 +40,7 @@ import {
localTimeSelectedTimezoneUTCTime,
getLocalTime,
convertDateToTimestamp,
resolveBrowserTimezone,
getTimezoneOffset,
convertTimeFromMicroToMilli,
convertTimeFromNsToMs,
@@ -186,6 +187,60 @@ describe("convertDateToTimestamp", () => {
});
});
// ---------------------------------------------------------------------------
// resolveBrowserTimezone
// ---------------------------------------------------------------------------
describe("resolveBrowserTimezone", () => {
it("returns a raw IANA zone unchanged", () => {
expect(resolveBrowserTimezone("America/Los_Angeles")).toBe("America/Los_Angeles");
});
it("returns 'UTC' unchanged", () => {
expect(resolveBrowserTimezone("UTC")).toBe("UTC");
});
it("resolves a 'Browser Time (<zone>)' label to the inner IANA zone", () => {
expect(resolveBrowserTimezone("Browser Time (America/Los_Angeles)")).toBe(
"America/Los_Angeles",
);
});
it("resolves a legacy label saved on another machine (rc9 update case)", () => {
expect(resolveBrowserTimezone("Browser Time (Asia/Kolkata)")).toBe("Asia/Kolkata");
});
it("matches the label prefix regardless of case", () => {
expect(resolveBrowserTimezone("browser time (Asia/Kolkata)")).toBe("Asia/Kolkata");
expect(resolveBrowserTimezone("BROWSER TIME (Asia/Kolkata)")).toBe("Asia/Kolkata");
});
it("trims whitespace inside the label parentheses", () => {
expect(resolveBrowserTimezone("Browser Time ( Asia/Kolkata )")).toBe("Asia/Kolkata");
});
it("leaves a value that only contains (not starts with) the label untouched", () => {
expect(resolveBrowserTimezone("Not Browser Time (Asia/Kolkata)")).toBe(
"Not Browser Time (Asia/Kolkata)",
);
});
it("never returns a 'Browser Time' label for a malformed value", () => {
const result = resolveBrowserTimezone("Browser Time");
expect(result.toLowerCase().startsWith("browser time")).toBe(false);
expect(result).toBeTruthy();
});
it("returns an empty string unchanged", () => {
expect(resolveBrowserTimezone("")).toBe("");
});
it("passes a null/undefined value through without throwing", () => {
expect(resolveBrowserTimezone(null as any)).toBeNull();
expect(resolveBrowserTimezone(undefined as any)).toBeUndefined();
});
});
// ---------------------------------------------------------------------------
// getTimezoneOffset
// ---------------------------------------------------------------------------
+15
View File
@@ -89,6 +89,21 @@ export const getLocalTime = (datetime: string) => {
}
};
/**
* "Browser Time (<zone>)" is a display-only label that some schedule pickers
* historically persisted (e.g. alerts created on older releases). It is NOT a
* valid IANA zone, so passing it to Luxon/the backend yields a NaN tz_offset.
* Resolve it to the plain IANA zone named inside the parentheses. Any other
* value (a real IANA zone, "UTC", empty) is returned unchanged.
*/
export const resolveBrowserTimezone = (timezone: string): string => {
if (typeof timezone === "string" && timezone.toLowerCase().startsWith("browser time")) {
const inner = timezone.match(/\(([^)]+)\)/)?.[1]?.trim();
return inner || Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
}
return timezone;
};
export const convertDateToTimestamp = (date: string, time: string, timezone: string) => {
const browserTime = "Browser Time (" + Intl.DateTimeFormat().resolvedOptions().timeZone + ")";