mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-05 08:09:22 +00:00
fix: alert timezone fix on main (#13519)
This commit is contained in:
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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 + ")";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user