Skip to content

Commit c433ebb

Browse files
committed
feat(dashboard): shared UI primitives & display components
Core label/field/field_list, local_time + future-aware relative_time with JS-localized tooltips, JSON highlighter, DataTable label-idiom headers, and the shared LogLine / StepDetail components reused across tabs and the flow inspector.
1 parent 54f6a09 commit c433ebb

6 files changed

Lines changed: 811 additions & 24 deletions

File tree

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
* Local time rendering — show UTC timestamps in the viewer's own timezone.
3+
*
4+
* The server can only emit UTC (it has no idea where the viewer is), so a raw
5+
* `2026-06-23T11:39:40.328499Z` is ambiguous and hard to read. The server
6+
* renders `<time data-ts="<iso>" data-format="...">` with a UTC fallback as
7+
* its text (legible even without JS); these helpers rewrite the text to the
8+
* browser's locale + timezone and add a full-detail tooltip.
9+
*
10+
* Wired through LiveSocket's `dom` callbacks in `main.ts` rather than a
11+
* per-element hook, so timestamps inside lists don't each need a unique id:
12+
* one pass localizes the initial render, `onNodeAdded` catches streamed-in
13+
* nodes, and `onBeforeElUpdated` re-localizes elements LiveView re-renders.
14+
*
15+
* Formats (via `data-format`):
16+
* - "time" 11:39:40.328 local time of day, ms precision
17+
* - "datetime" Jun 23, 2026, 11:39:40 local date + time
18+
* - "date" Jun 23, 2026
19+
*/
20+
21+
type Fmt = "time" | "datetime" | "date";
22+
23+
/** Localize a single `<time data-ts>` element in place. */
24+
export function localizeTime(el: HTMLElement): void {
25+
const iso = el.dataset.ts;
26+
if (!iso) return;
27+
28+
const d = new Date(iso);
29+
if (Number.isNaN(d.getTime())) return;
30+
31+
// Relative elements ("2m ago" / "in 2h") keep their server-computed text;
32+
// only the hover tooltip is localized to the viewer's timezone so the exact
33+
// moment is legible without exposing a confusing UTC string.
34+
if (el.dataset.rel) {
35+
el.setAttribute("title", format(d, "datetime"));
36+
return;
37+
}
38+
39+
el.textContent = format(d, (el.dataset.format as Fmt) || "datetime");
40+
el.setAttribute("title", d.toString());
41+
}
42+
43+
/** Localize every `<time data-ts>` under (and including) `root`. */
44+
export function localizeTimes(root: ParentNode | Element): void {
45+
if (root instanceof HTMLElement && root.matches("time[data-ts]")) {
46+
localizeTime(root);
47+
}
48+
for (const el of root.querySelectorAll<HTMLElement>("time[data-ts]")) {
49+
localizeTime(el);
50+
}
51+
}
52+
53+
function format(d: Date, fmt: Fmt): string {
54+
switch (fmt) {
55+
case "time":
56+
return d.toLocaleTimeString([], {
57+
hour: "2-digit",
58+
minute: "2-digit",
59+
second: "2-digit",
60+
fractionalSecondDigits: 3,
61+
hour12: false,
62+
});
63+
case "date":
64+
return d.toLocaleDateString([], { year: "numeric", month: "short", day: "numeric" });
65+
default:
66+
return d.toLocaleString([], {
67+
year: "numeric",
68+
month: "short",
69+
day: "numeric",
70+
hour: "2-digit",
71+
minute: "2-digit",
72+
second: "2-digit",
73+
hour12: false,
74+
});
75+
}
76+
}

durable_dashboard/assets/src/main.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { LiveSocket } from "phoenix_live_view";
1212

1313
import { CommandPalette } from "./hooks/command_palette";
1414
import { FlowGraph } from "./hooks/flow_graph";
15+
import { localizeTime, localizeTimes } from "./hooks/local_time";
1516

1617
const csrfToken = document.querySelector("meta[name='csrf-token']")?.getAttribute("content");
1718

@@ -21,9 +22,25 @@ const liveSocketPath =
2122
const liveSocket = new LiveSocket(liveSocketPath, Socket, {
2223
hooks: { FlowGraph, CommandPalette },
2324
params: { _csrf_token: csrfToken },
25+
dom: {
26+
// Render UTC `<time data-ts>` elements in the viewer's timezone — once for
27+
// streamed-in nodes, and again whenever LiveView re-renders one (which
28+
// would otherwise reset the text back to the server's UTC fallback).
29+
onNodeAdded(node: Node) {
30+
if (node instanceof HTMLElement) localizeTimes(node);
31+
},
32+
onBeforeElUpdated(_from: Element, to: Element) {
33+
if (to instanceof HTMLElement && to.matches("time[data-ts]")) localizeTime(to);
34+
},
35+
},
2436
});
2537

2638
liveSocket.connect();
2739

40+
// The initial server-rendered DOM is already in place before any patch, so
41+
// localize it directly.
42+
localizeTimes(document);
43+
document.addEventListener("phx:page-loading-stop", () => localizeTimes(document));
44+
2845
// Expose for debugging.
2946
(window as unknown as Record<string, unknown>).liveSocket = liveSocket;

0 commit comments

Comments
 (0)