Anonymous visitors to a public status page have no persisted Redux state, so ThemedHeatmap and ThemedHistogram tooltips always rendered timestamps in the hardcoded "America/Toronto" default — regardless of the timezone the operator configured. Even on the same domain, ThemedHeatmap and ThemedHistogram had no code path to consume the operator-configured statusPage.timezone returned by the API. - Add SYSTEM_TIMEZONE module constant and a small resolveTimezone() helper to TimeUtils.ts. formatDateWithTz's timezone parameter is now optional and defaults to the system tz, so callers don't need to rebuild the prop -> browser -> UTC chain inline. - Add a `timezone` field to StatusPageThemeContextValue so any themed component on the public page can read the page-wide tz without prop drilling (parallels how `theme` and `mode` are already exposed). - Wire statusPage.timezone into StatusPageThemeProvider from Pages/StatusPage/Status/index.tsx (both the public route and the admin preview). - ThemedHeatmap and ThemedHistogram now read tz from the context instead of useSelector(state.ui.timezone) — that selector was unreliable for anonymous visitors and removing it cuts a Redux subscription on the public page. - Replace the inline `data?.timezone || Intl.DateTimeFormat()...` expression in useStatusPageForm.ts with resolveTimezone(). Closes #3628.
80 lines
2.0 KiB
TypeScript
80 lines
2.0 KiB
TypeScript
import dayjs from "dayjs";
|
|
import duration from "dayjs/plugin/duration";
|
|
import utc from "dayjs/plugin/utc";
|
|
import timezone from "dayjs/plugin/timezone";
|
|
import customParseFormat from "dayjs/plugin/customParseFormat";
|
|
|
|
dayjs.extend(utc);
|
|
dayjs.extend(timezone);
|
|
dayjs.extend(customParseFormat);
|
|
dayjs.extend(duration);
|
|
|
|
export const MS_PER_SECOND = 1000;
|
|
export const MS_PER_MINUTE = 60 * MS_PER_SECOND;
|
|
export const MS_PER_HOUR = 60 * MS_PER_MINUTE;
|
|
export const MS_PER_DAY = 24 * MS_PER_HOUR;
|
|
export const MS_PER_WEEK = MS_PER_DAY * 7;
|
|
|
|
const resolveSystemTimezone = (): string => {
|
|
try {
|
|
return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
} catch {
|
|
return "UTC";
|
|
}
|
|
};
|
|
|
|
export const SYSTEM_TIMEZONE = resolveSystemTimezone();
|
|
|
|
export const resolveTimezone = (preferred?: string | null): string =>
|
|
preferred?.trim() || SYSTEM_TIMEZONE;
|
|
|
|
export const formatDateWithTz = (
|
|
timestamp: string,
|
|
format: string,
|
|
timezone?: string
|
|
) => {
|
|
if (!timestamp) {
|
|
return "Unknown time";
|
|
}
|
|
const tz = resolveTimezone(timezone);
|
|
try {
|
|
return dayjs(timestamp).tz(tz).format(format);
|
|
} catch {
|
|
return dayjs(timestamp).utc().format(format);
|
|
}
|
|
};
|
|
|
|
export const tickDateFormatLookup = (range: string) => {
|
|
const tickFormatLookup: Record<string, string> = {
|
|
recent: "h:mm A",
|
|
day: "h:mm A",
|
|
week: "MM/D, h:mm A",
|
|
month: "ddd. M/D",
|
|
};
|
|
const format = tickFormatLookup[range];
|
|
if (format === undefined) {
|
|
return "";
|
|
}
|
|
return format;
|
|
};
|
|
|
|
export const tooltipDateFormatLookup = (range: string) => {
|
|
const dateFormatLookup: Record<string, string> = {
|
|
recent: "ddd. MMMM D, YYYY, hh:mm A",
|
|
day: "ddd. MMMM D, YYYY, hh:mm A",
|
|
week: "ddd. MMMM D, YYYY, hh:mm A",
|
|
month: "ddd. MMMM D, YYYY",
|
|
};
|
|
const format = dateFormatLookup[range];
|
|
if (format === undefined) {
|
|
return "";
|
|
}
|
|
return format;
|
|
};
|
|
|
|
export const formatTimestamp = (timestamp: string | number | null): string => {
|
|
if (!timestamp) return "-";
|
|
const date = new Date(timestamp);
|
|
return date.toLocaleString();
|
|
};
|