mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-05 16:10:43 +00:00
fix(time-picker): locale-aware range labels, inclusive whole-day chip, shared offset cache
This commit is contained in:
@@ -195,7 +195,6 @@ describe("DateTime rail", () => {
|
||||
expect(el("datetime-timezone-select-trigger")?.textContent).toContain(
|
||||
"Asia/Calcutta (UTC+05:30)",
|
||||
);
|
||||
expect(el("date-time-cancel-btn")).toBeNull();
|
||||
expect(el("date-time-apply-btn")).not.toBeNull();
|
||||
});
|
||||
|
||||
|
||||
@@ -485,6 +485,9 @@ interface ConsumableDateTime {
|
||||
userChangedValue?: boolean;
|
||||
}
|
||||
|
||||
// One offset lookup per zone for the whole app, not per picker instance.
|
||||
const zoneOffsets = new Map<string, string>();
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
OSeparator,
|
||||
@@ -680,8 +683,6 @@ export default defineComponent({
|
||||
const filteredTimezone: any = ref(timezoneOptions);
|
||||
const isTimezoneSelectOpen = ref(false);
|
||||
|
||||
const zoneOffsets = new Map<string, string>();
|
||||
|
||||
// longOffset throws on engines that lack it, so the label falls back to no offset.
|
||||
const zoneOffset = (zone: string): string => {
|
||||
const cached = zoneOffsets.get(zone);
|
||||
|
||||
@@ -258,9 +258,9 @@ const tabStop = computed(() =>
|
||||
: focusOrder.value[0],
|
||||
);
|
||||
|
||||
// The accessible name must contain the visible compact label.
|
||||
const hasCalendar = computed(() => props.calendar !== null && props.calendar.length > 0);
|
||||
|
||||
// The accessible name must contain the visible compact label.
|
||||
const rowName = (item: TimeRangeRailItem) =>
|
||||
item.compact ? `${item.compact.label} (${item.label})` : item.label;
|
||||
|
||||
|
||||
@@ -120,12 +120,31 @@ describe("timeRangeRail", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("locale", () => {
|
||||
it("keeps each locale's word order for recent labels and hints", () => {
|
||||
const start = us("2026-10-02T08:00:00Z");
|
||||
const end = us("2026-10-03T09:00:00Z");
|
||||
expect(
|
||||
formatRecentRangeLabel(start, end, "UTC", Date.parse("2026-11-04T12:00:00Z"), "de-DE"),
|
||||
).toBe("2.–3. Okt.");
|
||||
expect(
|
||||
formatCalendarRailHint(
|
||||
"calendar:month:0",
|
||||
"UTC",
|
||||
Date.parse("2026-11-04T12:00:00Z"),
|
||||
"fr-FR",
|
||||
),
|
||||
).toBe("nov.");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatRecentDuration", () => {
|
||||
const dur = (seconds: number) => formatRecentDuration(0, seconds * 1_000_000);
|
||||
|
||||
it("reads a range ending at :59 as inclusive, so a whole day is 1d", () => {
|
||||
expect(dur(86_399)).toBe("1d");
|
||||
expect(dur(2 * 86_400 - 1)).toBe("2d");
|
||||
expect(formatRecentDuration(30_000_000, 89_000_000)).toBe("59s");
|
||||
});
|
||||
|
||||
it("keeps the two largest non-zero units", () => {
|
||||
@@ -158,8 +177,8 @@ describe("timeRangeRail", () => {
|
||||
});
|
||||
|
||||
it("shows the whole Monday-to-Sunday week, not the elapsed part", () => {
|
||||
expect(hint("calendar:week:0")).toBe("Nov 2–8");
|
||||
expect(hint("calendar:week:-1")).toBe("Oct 26–Nov 1");
|
||||
expect(hint("calendar:week:0")).toBe("Nov 2 – 8");
|
||||
expect(hint("calendar:week:-1")).toBe("Oct 26 – Nov 1");
|
||||
});
|
||||
|
||||
it("shows the month name and the quarter number", () => {
|
||||
@@ -173,7 +192,7 @@ describe("timeRangeRail", () => {
|
||||
const jan = Date.parse("2026-01-07T12:00:00Z");
|
||||
expect(hint("calendar:day:-1", "UTC", jan)).toBe("Jan 6");
|
||||
expect(hint("calendar:week:-1", "UTC", Date.parse("2026-01-05T12:00:00Z"))).toBe(
|
||||
"Dec 29, 2025–Jan 4, 2026",
|
||||
"Dec 29, 2025 – Jan 4, 2026",
|
||||
);
|
||||
expect(hint("calendar:month:-1", "UTC", jan)).toBe("Dec 2025");
|
||||
expect(hint("calendar:quarter:-1", "UTC", jan)).toBe("Q4 2025");
|
||||
|
||||
@@ -52,7 +52,7 @@ export interface TimeRangeRailItem {
|
||||
tooltip: I18nText | undefined;
|
||||
selected: boolean;
|
||||
disabled: boolean;
|
||||
/** Recent rows only: the compact bold label and the duration chip shown instead of the full range. */
|
||||
/** Recent rows only: the compact label and the duration chip shown instead of the full range. */
|
||||
compact?: { label: string; duration: string };
|
||||
/** Calendar rows only: the muted date hint shown beside the label. */
|
||||
hint?: string;
|
||||
@@ -96,17 +96,17 @@ export const formatRecentRange = (
|
||||
return raw(`${start.date} ${start.time} - ${end.date} ${end.time}`);
|
||||
};
|
||||
|
||||
const monthNames = new Map<string, string[]>();
|
||||
|
||||
// Month names follow the picker's locale, like the sibling older-period labels.
|
||||
const monthName = (locale: string, month: number): string => {
|
||||
let names = monthNames.get(locale);
|
||||
if (!names) {
|
||||
const format = new Intl.DateTimeFormat(locale, { month: "short", timeZone: "UTC" });
|
||||
names = Array.from({ length: 12 }, (_, m) => format.format(Date.UTC(2026, m, 1)));
|
||||
monthNames.set(locale, names);
|
||||
}
|
||||
return names[month];
|
||||
// Intl keeps each locale's word order, as the sibling older-period labels do; thin spaces become plain ones.
|
||||
const formatCivilDays = (locale: string, first: Date, last: Date, withYear: boolean): string => {
|
||||
const format = new Intl.DateTimeFormat(locale, {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
...(withYear ? { year: "numeric" } : {}),
|
||||
timeZone: "UTC",
|
||||
});
|
||||
const text =
|
||||
first.getTime() === last.getTime() ? format.format(first) : format.formatRange(first, last);
|
||||
return text.replace(/[\u2009\u202f]/g, " ");
|
||||
};
|
||||
|
||||
const civilParts = (date: string): { year: number; month: number; day: number } => {
|
||||
@@ -135,22 +135,22 @@ export const formatRecentRangeLabel = (
|
||||
const { start, end } = splitRecentRange(startUs, endUs, zone);
|
||||
const from = civilParts(start.date);
|
||||
const to = civilParts(end.date);
|
||||
const withYear =
|
||||
from.year !== currentYearIn(nowMs, zone) || to.year !== currentYearIn(nowMs, zone);
|
||||
const day = (p: typeof from) =>
|
||||
`${monthName(locale, p.month)} ${p.day}${withYear ? `, ${p.year}` : ""}`;
|
||||
if (from.year === to.year && from.month === to.month) {
|
||||
if (from.day === to.day) return `${day(from)}${sameDayTimes(start.time, end.time)}`;
|
||||
return `${monthName(locale, from.month)} ${from.day} – ${to.day}${withYear ? `, ${from.year}` : ""}`;
|
||||
}
|
||||
return `${day(from)} – ${day(to)}`;
|
||||
const year = currentYearIn(nowMs, zone);
|
||||
const days = formatCivilDays(
|
||||
locale,
|
||||
new Date(Date.UTC(from.year, from.month, from.day)),
|
||||
new Date(Date.UTC(to.year, to.month, to.day)),
|
||||
from.year !== year || to.year !== year,
|
||||
);
|
||||
const sameDay = from.year === to.year && from.month === to.month && from.day === to.day;
|
||||
return sameDay ? `${days}${sameDayTimes(start.time, end.time)}` : days;
|
||||
};
|
||||
|
||||
/** The two largest non-zero units of d/h/m/s in the span, e.g. "1d 15m", "45m", "30s". */
|
||||
export const formatRecentDuration = (startUs: number, endUs: number): string => {
|
||||
const spanSeconds = Math.max(0, Math.floor((endUs - startUs) / MICROS_PER_SECOND));
|
||||
// A range ending at :59 is inclusive of that minute, so a whole day reads 1d, not 23h 59m.
|
||||
const total = spanSeconds % 60 === 59 ? spanSeconds + 1 : spanSeconds;
|
||||
const total = Math.floor(endUs / MICROS_PER_SECOND) % 60 === 59 ? spanSeconds + 1 : spanSeconds;
|
||||
const units: [string, number][] = [
|
||||
["d", Math.floor(total / 86_400)],
|
||||
["h", Math.floor((total % 86_400) / 3600)],
|
||||
@@ -174,21 +174,16 @@ export const formatCalendarRailHint = (
|
||||
const { unit, first, last, today } = span;
|
||||
const currentYear = today.getUTCFullYear();
|
||||
const withYear = first.getUTCFullYear() !== currentYear || last.getUTCFullYear() !== currentYear;
|
||||
const day = (d: Date) =>
|
||||
`${monthName(locale, d.getUTCMonth())} ${d.getUTCDate()}${withYear ? `, ${d.getUTCFullYear()}` : ""}`;
|
||||
const yearSuffix = withYear ? ` ${first.getUTCFullYear()}` : "";
|
||||
if (unit === "day") return day(first);
|
||||
if (unit === "month") return `${monthName(locale, first.getUTCMonth())}${yearSuffix}`;
|
||||
if (unit === "quarter")
|
||||
return `${quarterText(Math.floor(first.getUTCMonth() / 3) + 1)}${yearSuffix}`;
|
||||
if (unit === "year") return String(first.getUTCFullYear());
|
||||
if (
|
||||
first.getUTCFullYear() === last.getUTCFullYear() &&
|
||||
first.getUTCMonth() === last.getUTCMonth()
|
||||
) {
|
||||
return `${monthName(locale, first.getUTCMonth())} ${first.getUTCDate()}–${last.getUTCDate()}${withYear ? `, ${first.getUTCFullYear()}` : ""}`;
|
||||
if (unit === "day" || unit === "week") return formatCivilDays(locale, first, last, withYear);
|
||||
if (unit === "month") {
|
||||
const month = new Intl.DateTimeFormat(locale, { month: "short", timeZone: "UTC" });
|
||||
return `${month.format(first)}${yearSuffix}`;
|
||||
}
|
||||
return `${day(first)}–${day(last)}`;
|
||||
if (unit === "quarter") {
|
||||
return `${quarterText(Math.floor(first.getUTCMonth() / 3) + 1)}${yearSuffix}`;
|
||||
}
|
||||
return String(first.getUTCFullYear());
|
||||
};
|
||||
|
||||
/** Whether a range is longer than the page's query-range limit; 0 means no limit. */
|
||||
|
||||
Reference in New Issue
Block a user