diff --git a/packages/views/common/timezone-select.tsx b/packages/views/common/timezone-select.tsx new file mode 100644 index 0000000000..b6eab16f37 --- /dev/null +++ b/packages/views/common/timezone-select.tsx @@ -0,0 +1,112 @@ +"use client"; + +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@multica/ui/components/ui/select"; + +// Common IANA zones surfaced as quick picks. Used as the fallback option set +// when Intl.supportedValuesOf is not available, and promoted to the top of +// the list when it is. +const COMMON_TIMEZONES = [ + "UTC", + "America/Los_Angeles", + "America/Denver", + "America/Chicago", + "America/New_York", + "America/Sao_Paulo", + "Europe/London", + "Europe/Berlin", + "Europe/Paris", + "Europe/Moscow", + "Africa/Cairo", + "Asia/Dubai", + "Asia/Kolkata", + "Asia/Bangkok", + "Asia/Shanghai", + "Asia/Singapore", + "Asia/Tokyo", + "Australia/Sydney", + "Pacific/Auckland", +]; + +export function browserTimezone(): string { + try { + const tz = Intl.DateTimeFormat().resolvedOptions().timeZone; + return tz || "UTC"; + } catch { + return "UTC"; + } +} + +type IntlWithSupportedValues = typeof Intl & { + supportedValuesOf?: (key: "timeZone") => string[]; +}; + +function supportedTimezones(): string[] { + try { + const supported = (Intl as IntlWithSupportedValues).supportedValuesOf?.( + "timeZone", + ); + return supported && supported.length > 0 ? supported : COMMON_TIMEZONES; + } catch { + return COMMON_TIMEZONES; + } +} + +export function timezoneOptions(current: string): string[] { + const browser = browserTimezone(); + return Array.from( + new Set([current, browser, ...COMMON_TIMEZONES, ...supportedTimezones()]), + ).filter(Boolean); +} + +// Shared single-select timezone picker. Surfaces the browser-resolved zone +// with a translated suffix (passed in by the caller — the picker itself stays +// i18n-namespace agnostic), followed by a curated set of common IANA zones +// and everything Intl.supportedValuesOf exposes. +export function TimezoneSelect({ + value, + onValueChange, + browserSuffix, + disabled, + triggerClassName, +}: { + value: string; + onValueChange: (next: string) => void; + browserSuffix: string; + disabled?: boolean; + triggerClassName?: string; +}) { + const browser = browserTimezone(); + const options = timezoneOptions(value); + const render = (tz: string) => + tz === browser ? `${tz}${browserSuffix}` : tz; + + return ( + + ); +} diff --git a/packages/views/dashboard/components/dashboard-page.tsx b/packages/views/dashboard/components/dashboard-page.tsx index 096b71bcb9..cda7fee430 100644 --- a/packages/views/dashboard/components/dashboard-page.tsx +++ b/packages/views/dashboard/components/dashboard-page.tsx @@ -25,6 +25,10 @@ import { KpiCard } from "../../runtimes/components/shared"; import { DailyCostChart } from "../../runtimes/components/charts"; import { ProjectIcon } from "../../projects/components/project-icon"; import { ActorAvatar } from "../../common/actor-avatar"; +import { + TimezoneSelect, + browserTimezone, +} from "../../common/timezone-select"; import { formatTokens } from "../../runtimes/utils"; import { useT } from "../../i18n"; import { @@ -106,9 +110,14 @@ function Segmented({ */ export function DashboardPage() { const { t } = useT("usage"); + const { t: tRuntimes } = useT("runtimes"); const wsId = useWorkspaceId(); const [days, setDays] = useState(30); const [projectValue, setProjectValue] = useState(ALL_PROJECTS); + // Default to the browser's resolved zone so day-boundary buckets match the + // user's local clock on first render. Pure client-state — the rollup queries + // are zone-agnostic today; this is the UI affordance the user can pin. + const [timezone, setTimezone] = useState(() => browserTimezone()); // The user can save model prices from the runtimes page; re-render when // they do so the dashboard reflects the new rates. @@ -176,12 +185,18 @@ export function DashboardPage() { return (
- -
- -

{t(($) => $.title)}

+ {/* h-auto + min-h-12 + flex-wrap: the toolbar (project filter, range + switch, timezone select) overflows the single h-12 row on narrow + and medium widths once the timezone picker is added — letting the + right cluster wrap underneath keeps every control reachable + without an off-screen bleed. Wider viewports still render the + original single row. */} + +
+ +

{t(($) => $.title)}

-
+
({ label: r.label, value: r.days }))} /> + $.detail.timezone_browser_suffix)} + triggerClassName="rounded-md font-mono text-xs" + />
diff --git a/packages/views/runtimes/components/runtime-detail.tsx b/packages/views/runtimes/components/runtime-detail.tsx index 2c356a1468..5734f13419 100644 --- a/packages/views/runtimes/components/runtime-detail.tsx +++ b/packages/views/runtimes/components/runtime-detail.tsx @@ -38,14 +38,8 @@ import { TooltipContent, TooltipTrigger, } from "@multica/ui/components/ui/tooltip"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@multica/ui/components/ui/select"; import { ActorAvatar } from "../../common/actor-avatar"; +import { TimezoneSelect } from "../../common/timezone-select"; import { AppLink } from "../../navigation"; import { availabilityConfig, workloadConfig } from "../../agents/presence"; import { formatLastSeen } from "../utils"; @@ -567,54 +561,6 @@ function DiagnosticsCard({ ); } -// Common IANA zones offered as quick picks when Intl.supportedValuesOf is not -// available, and promoted near the top otherwise. -const COMMON_TIMEZONES = [ - "UTC", - "America/Los_Angeles", - "America/Denver", - "America/Chicago", - "America/New_York", - "America/Sao_Paulo", - "Europe/London", - "Europe/Berlin", - "Europe/Paris", - "Europe/Moscow", - "Africa/Cairo", - "Asia/Dubai", - "Asia/Kolkata", - "Asia/Bangkok", - "Asia/Shanghai", - "Asia/Singapore", - "Asia/Tokyo", - "Australia/Sydney", - "Pacific/Auckland", -]; - -function browserTimezone(): string { - try { - const tz = Intl.DateTimeFormat().resolvedOptions().timeZone; - return tz || "UTC"; - } catch { - return "UTC"; - } -} - -type IntlWithSupportedValues = typeof Intl & { - supportedValuesOf?: (key: "timeZone") => string[]; -}; - -function supportedTimezones(): string[] { - try { - const supported = (Intl as IntlWithSupportedValues).supportedValuesOf?.( - "timeZone", - ); - return supported && supported.length > 0 ? supported : COMMON_TIMEZONES; - } catch { - return COMMON_TIMEZONES; - } -} - // VisibilityReadout renders a static "Private" / "Public" pill for users // who can't edit the runtime. The description used to sit under the chip; // it now lives in the hover tooltip so the Diagnostics column stays compact @@ -755,12 +701,7 @@ function TimezoneEditor({ runtime }: { runtime: AgentRuntime }) { const wsId = useWorkspaceId(); const updateRuntime = useUpdateRuntime(wsId); const current = runtime.timezone || "UTC"; - const browser = browserTimezone(); - const browserSuffix = t(($) => $.detail.timezone_browser_suffix); - const options = Array.from( - new Set([current, browser, ...COMMON_TIMEZONES, ...supportedTimezones()]), - ).filter(Boolean); const handleTimezoneChange = (next: string) => { if (next === current) return; updateRuntime.mutate( @@ -776,26 +717,12 @@ function TimezoneEditor({ runtime }: { runtime: AgentRuntime }) { return (
- + />

{t(($) => $.detail.timezone_hint)}