@datum-cloud/datum-ui 2.4.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/dist/app-navigation/index.mjs +2 -2
  2. package/dist/{app-navigation--tEBCrfl.mjs → app-navigation-DhPaPFnN.mjs} +3 -7
  3. package/dist/assistant/index.mjs +15 -6
  4. package/dist/autocomplete/index.mjs +1 -1
  5. package/dist/{autocomplete-DZ_HSBak.mjs → autocomplete-BxtzndxZ.mjs} +1 -1
  6. package/dist/autosearch/index.mjs +3 -3
  7. package/dist/avatar-stack/index.mjs +1 -1
  8. package/dist/{avatar-stack-BZK6zUJr.mjs → avatar-stack-BUcsjhA6.mjs} +1 -1
  9. package/dist/button/index.mjs +2 -2
  10. package/dist/{button-H_8dI-Ib.mjs → button-CDPui_Ov.mjs} +1 -1
  11. package/dist/{button-A1qTjZ8U.mjs → button-DoMqfQLu.mjs} +1 -1
  12. package/dist/{calendar-date-picker-ynSGADfR.mjs → calendar-date-picker-bpTUcGu_.mjs} +1 -1
  13. package/dist/components/base/hover-card/hover-card.d.ts +13 -0
  14. package/dist/components/base/hover-card/index.d.ts +1 -1
  15. package/dist/components/base/popover/index.d.ts +1 -1
  16. package/dist/components/base/popover/popover.d.ts +15 -0
  17. package/dist/components/features/logs/components/columns.d.ts +15 -0
  18. package/dist/components/features/logs/components/root.d.ts +1 -1
  19. package/dist/components/features/logs/components/status-badge.d.ts +5 -0
  20. package/dist/components/features/logs/components/timeline.d.ts +3 -0
  21. package/dist/components/features/logs/fixtures/index.d.ts +2 -0
  22. package/dist/components/features/logs/index.d.ts +3 -2
  23. package/dist/components/features/logs/logs.d.ts +2 -0
  24. package/dist/components/features/logs/types.d.ts +51 -3
  25. package/dist/components/features/logs/utils/detail-width.d.ts +3 -0
  26. package/dist/components/features/logs/utils/facets.d.ts +7 -0
  27. package/dist/components/features/logs/utils/format-timestamp.d.ts +4 -0
  28. package/dist/components/features/logs/utils/histogram.d.ts +16 -0
  29. package/dist/components/features/logs/utils/host.d.ts +7 -0
  30. package/dist/components/features/logs/utils/index.d.ts +6 -4
  31. package/dist/components/features/logs/utils/parse-log-line.d.ts +17 -3
  32. package/dist/components/features/logs/utils/time-range.d.ts +19 -0
  33. package/dist/data-table/index.mjs +1 -1
  34. package/dist/{data-table-ClfRpyHq.mjs → data-table-BaWOHdZX.mjs} +6 -6
  35. package/dist/date-picker/index.mjs +3 -3
  36. package/dist/date-time/index.mjs +1 -1
  37. package/dist/{date-time-CA-09lnw.mjs → date-time-lZdefM4H.mjs} +2 -2
  38. package/dist/date-time-picker/index.mjs +1 -1
  39. package/dist/{date-time-picker-BzcHrf-A.mjs → date-time-picker-Bd7qSZVr.mjs} +1 -1
  40. package/dist/{date-time-range-picker-Dog33Ry0.mjs → date-time-range-picker-DlFTh3DF.mjs} +14 -5
  41. package/dist/dialog/index.mjs +1 -1
  42. package/dist/{dialog-N2pkQIc4.mjs → dialog-Gu70fban.mjs} +1 -1
  43. package/dist/dropdown/index.mjs +1 -1
  44. package/dist/{dropdown-B0znsOlu.mjs → dropdown-Xvduih96.mjs} +6 -6
  45. package/dist/dropzone/index.mjs +1 -1
  46. package/dist/{dropzone-DrWVd7OI.mjs → dropzone-ihksfjN2.mjs} +1 -1
  47. package/dist/empty-content/index.mjs +1 -1
  48. package/dist/{empty-content-DvaelFpJ.mjs → empty-content-CmuVcU0P.mjs} +2 -2
  49. package/dist/form/index.mjs +1 -1
  50. package/dist/form/stepper/index.mjs +1 -1
  51. package/dist/{form-BhBsoMFA.mjs → form-BKxvdIKB.mjs} +10 -10
  52. package/dist/grouped-table/index.mjs +1 -1
  53. package/dist/{grouped-table-Bb9KoI1e.mjs → grouped-table-DWtEx18V.mjs} +4 -8
  54. package/dist/hover-card/index.mjs +1 -1
  55. package/dist/{hover-card-08jU9cD8.mjs → hover-card-BNOE5vT5.mjs} +20 -3
  56. package/dist/index.mjs +35 -34
  57. package/dist/logs/fixtures/index.mjs +770 -3
  58. package/dist/logs/index.mjs +1044 -249
  59. package/dist/mobile-sheet/index.mjs +1 -1
  60. package/dist/{mobile-sheet-BYp-QQmn.mjs → mobile-sheet-9TootJGg.mjs} +1 -1
  61. package/dist/more-actions/index.mjs +1 -1
  62. package/dist/{more-actions-B1FUf2rm.mjs → more-actions-C3DYWO0a.mjs} +3 -3
  63. package/dist/motion-DvGRWdsm.mjs +46 -0
  64. package/dist/multi-select/index.mjs +1 -1
  65. package/dist/{multi-select-PtdMChPF.mjs → multi-select-CH8KERkt.mjs} +1 -1
  66. package/dist/picker/index.mjs +2 -2
  67. package/dist/{picker-raVuDxsh.mjs → picker-DbKukBkk.mjs} +1 -1
  68. package/dist/popover/index.mjs +1 -33
  69. package/dist/popover-X82NwhX9.mjs +53 -0
  70. package/dist/responsive-dropdown/index.mjs +1 -1
  71. package/dist/{responsive-dropdown-n0Txkax5.mjs → responsive-dropdown-C_KKgLbH.mjs} +2 -2
  72. package/dist/responsive-popover/index.mjs +1 -1
  73. package/dist/{responsive-popover-C_1nzIvm.mjs → responsive-popover-8VcjBM2d.mjs} +2 -2
  74. package/dist/rich-text-editor/index.mjs +1 -1
  75. package/dist/{rich-text-editor-Dohp-pbf.mjs → rich-text-editor-DVga5upM.mjs} +3 -3
  76. package/dist/select/index.mjs +1 -1
  77. package/dist/{select-DCU44UO6.mjs → select-CQsG8Kju.mjs} +1 -1
  78. package/dist/sheet/index.mjs +1 -1
  79. package/dist/{sheet-C2i2k5i2.mjs → sheet-DHn6C_rs.mjs} +1 -1
  80. package/dist/sidebar/index.mjs +1 -1
  81. package/dist/{sidebar-D6BoAr6C.mjs → sidebar-BeWEEu7P.mjs} +1 -1
  82. package/dist/styles/motion.css +61 -0
  83. package/dist/styles/root.css +6 -0
  84. package/dist/switch/index.mjs +1 -1
  85. package/dist/{switch-DPHr3TcN.mjs → switch-Z2pJMckf.mjs} +1 -1
  86. package/dist/tabs/index.mjs +1 -1
  87. package/dist/task-queue/index.mjs +1 -1
  88. package/dist/{task-queue-dropdown-ClLLTMxU.mjs → task-queue-dropdown-BJS3snfZ.mjs} +4 -4
  89. package/dist/time-picker/index.mjs +1 -1
  90. package/dist/{time-picker-CFhaHTtI.mjs → time-picker-rVTkmbIG.mjs} +1 -1
  91. package/dist/{to-api-format-WC7b9Mcv.mjs → to-api-format-Bm1VcKkS.mjs} +4 -4
  92. package/dist/tooltip/index.mjs +1 -1
  93. package/dist/{tooltip-C_SL1imw.mjs → tooltip-DhbN1BDK.mjs} +1 -1
  94. package/dist/transfer/index.mjs +1 -1
  95. package/dist/{transfer-CEq9AhL7.mjs → transfer-DsshYgIk.mjs} +1 -1
  96. package/dist/utils/index.d.ts +2 -0
  97. package/dist/utils/index.mjs +2 -1
  98. package/dist/utils/motion.d.ts +30 -0
  99. package/package.json +1 -1
@@ -1,86 +1,43 @@
1
1
  import { t as cn } from "../utils-Bu32wN-x.mjs";
2
2
  import { t as Badge } from "../badge-BTNW_FEU.mjs";
3
- import { t as Button } from "../button-A1qTjZ8U.mjs";
3
+ import { t as Button } from "../button-DoMqfQLu.mjs";
4
+ import { t as Icon } from "../icon-wrapper-DKfJlJd0.mjs";
4
5
  import { t as Checkbox } from "../checkbox-C6P12fio.mjs";
5
6
  import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../collapsible/index.mjs";
6
7
  import { t as Input } from "../input-j4tbmYYz.mjs";
7
- import { t as Sheet } from "../sheet-C2i2k5i2.mjs";
8
+ import { t as Sheet } from "../sheet-DHn6C_rs.mjs";
8
9
  import { t as useBreakpoint } from "../use-breakpoint-CwiUJSmk.mjs";
9
10
  import { Skeleton } from "../skeleton/index.mjs";
10
11
  import { TableBody, TableCell, TableHead, TableHeader, TableRow } from "../table/index.mjs";
11
- import { t as DateTimeRangePicker } from "../date-time-range-picker-Dog33Ry0.mjs";
12
- import { t as DateTime } from "../date-time-CA-09lnw.mjs";
12
+ import { t as DateTimeRangePicker } from "../date-time-range-picker-DlFTh3DF.mjs";
13
+ import { c as getBrowserTimezone, t as DateTime, u as getTimezoneAbbreviation } from "../date-time-lZdefM4H.mjs";
13
14
  import { t as useCopyToClipboard } from "../use-copy-to-clipboard-DZDq_85T.mjs";
14
- import { ChevronDown, ChevronUp, Copy, Download, RefreshCw, Search, SlidersHorizontal, X } from "lucide-react";
15
- import { createContext, use, useCallback, useEffect, useMemo, useRef, useState } from "react";
15
+ import { ChevronDown, ChevronUp, Clock, Copy, Download, Inbox, RefreshCw, Search, SlidersHorizontal, TriangleAlert, X } from "lucide-react";
16
+ import { createContext, memo, use, useCallback, useEffect, useMemo, useRef, useState } from "react";
16
17
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
17
18
  import { AnimatePresence, motion, useReducedMotion } from "motion/react";
18
- import { format } from "date-fns";
19
- //#region src/components/features/logs/hooks/use-logs.ts
20
- const LogsContext = createContext(null);
21
- function useLogs() {
22
- const context = use(LogsContext);
23
- if (!context) throw new Error("Logs components must be used within <Logs.Root>");
24
- return context;
25
- }
26
- //#endregion
27
- //#region src/components/features/logs/utils/parse-log-line.ts
28
- const ACCESS_LOG_RE = /^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s+(\S+)\s+(\d{3})\s+(\d+)ms\b/i;
29
- const PATH_TOKEN_RE = /(?:^|\s)(\/\S+)/;
30
- function parseLogLine(line) {
31
- const match = ACCESS_LOG_RE.exec(line);
32
- if (!match) return {
33
- kind: "text",
34
- line
35
- };
36
- const method = match[1];
37
- const path = match[2];
38
- const status = match[3];
39
- const duration = match[4];
40
- if (!method || !path || !status || !duration) return {
41
- kind: "text",
42
- line
43
- };
44
- return {
45
- kind: "http",
46
- method: method.toUpperCase(),
47
- path,
48
- status: Number(status),
49
- durationMs: Number(duration)
50
- };
51
- }
52
- function logLineDisplay(line) {
53
- const parsed = parseLogLine(line);
54
- if (parsed.kind === "http") {
55
- const match = ACCESS_LOG_RE.exec(line);
56
- const rest = match ? line.slice(match[0].length).trim() : "";
57
- return {
58
- parsed,
59
- path: parsed.path,
60
- message: extraMessage(rest)
61
- };
62
- }
63
- const path = PATH_TOKEN_RE.exec(line)?.[1] ?? null;
64
- if (!path) return {
65
- parsed,
66
- path: null,
67
- message: line
68
- };
19
+ import { format, isSameDay } from "date-fns";
20
+ //#region src/components/features/logs/utils/format-timestamp.ts
21
+ function formatLogTimestampParts(date) {
69
22
  return {
70
- parsed,
71
- path,
72
- message: extraMessage(stripOnce(line, path))
23
+ date: format(date, "MMM dd").toUpperCase(),
24
+ time: format(date, "HH:mm:ss.SS")
73
25
  };
74
26
  }
75
- /** Drop leftover that is only fields already shown (method, status, path, duration, label=value). */
76
- function extraMessage(rest) {
77
- if (!rest) return "";
78
- return rest.split(/\s+/).filter(Boolean).some((token) => !/^[A-Z_]\w*=\S+$/i.test(token)) ? rest : "";
27
+ function formatLogTimestamp(date) {
28
+ const { date: day, time } = formatLogTimestampParts(date);
29
+ return `${day} ${time}`;
79
30
  }
80
- function stripOnce(line, token) {
81
- const index = line.indexOf(token);
82
- if (index === -1) return line;
83
- return `${line.slice(0, index)}${line.slice(index + token.length)}`.replace(/\s+/g, " ").trim();
31
+ //#endregion
32
+ //#region src/components/features/logs/utils/host.ts
33
+ /**
34
+ * Host shown for an HTTP access log. An explicit `host` label (set by the
35
+ * host app) wins; otherwise prefer the name the client asked for
36
+ * (`requested_server_name`, `x_forwarded_host`) over the upstream
37
+ * `authority`, then fall back to `resource_name`.
38
+ */
39
+ function logRequestHost(labels) {
40
+ return labels.host?.trim() || labels.requested_server_name?.trim() || labels.x_forwarded_host?.trim() || labels.authority?.trim() || labels.resource_name?.trim() || void 0;
84
41
  }
85
42
  //#endregion
86
43
  //#region src/components/features/logs/utils/severity.ts
@@ -117,30 +74,370 @@ function httpStatusTextClass(status) {
117
74
  //#endregion
118
75
  //#region src/components/features/logs/components/status-badge.tsx
119
76
  function LogsSeverityBadge({ severity }) {
77
+ const unlabeled = !severity;
120
78
  return /* @__PURE__ */ jsx(Badge, {
121
79
  type: severityBadgeType(severity),
122
- theme: "light",
123
- className: "h-4 px-1.5 py-0 font-mono text-[10px] leading-4 font-medium",
80
+ theme: unlabeled ? "solid" : "light",
81
+ className: cn("h-4 px-1.5 py-0 font-mono text-[10px] leading-4 font-medium", unlabeled && "border-border bg-muted text-foreground"),
124
82
  children: severity ?? "LOG"
125
83
  });
126
84
  }
85
+ /** Compact HTTP status chip, used inline next to the detail title. */
86
+ function LogsHttpStatusChip({ status, className }) {
87
+ return /* @__PURE__ */ jsx(Badge, {
88
+ type: httpStatusBadgeType(status),
89
+ theme: "light",
90
+ "data-slot": "logs-http-status",
91
+ className: cn("h-5 shrink-0 px-1.5 py-0 font-mono text-[11px] leading-5 font-medium tabular-nums", className),
92
+ children: status
93
+ });
94
+ }
127
95
  function LogsStatusBadge({ parsed }) {
128
96
  if (parsed.kind !== "http") return /* @__PURE__ */ jsx("span", {
129
97
  className: "text-muted-foreground",
130
98
  children: "—"
131
99
  });
132
100
  return /* @__PURE__ */ jsxs("span", {
133
- className: "inline-grid grid-cols-[7ch_auto] gap-x-2 font-mono text-[11px] font-medium",
101
+ className: "inline-flex h-4 items-center gap-1.5 font-mono text-[11px] leading-4 font-medium",
134
102
  children: [/* @__PURE__ */ jsx("span", {
135
- className: "text-muted-foreground",
136
- children: parsed.method
137
- }), /* @__PURE__ */ jsx("span", {
138
103
  className: cn("tabular-nums", httpStatusTextClass(parsed.status)),
139
104
  children: parsed.status
105
+ }), /* @__PURE__ */ jsx("span", {
106
+ className: "text-muted-foreground",
107
+ children: parsed.method
140
108
  })]
141
109
  });
142
110
  }
143
111
  //#endregion
112
+ //#region src/components/features/logs/components/columns.tsx
113
+ const DEFAULT_FIXED_WIDTH = 160;
114
+ /** `MMM dd HH:mm:ss.SS` — e.g. `SEP 10 12:03:37.79`. */
115
+ const TIME_CHS = 18;
116
+ const METHOD_CHS = [
117
+ 3,
118
+ 4,
119
+ 3,
120
+ 4
121
+ ];
122
+ const HOST_CHS = [
123
+ 16,
124
+ 22,
125
+ 14,
126
+ 20,
127
+ 18,
128
+ 24
129
+ ];
130
+ const PATH_CHS = [
131
+ 12,
132
+ 28,
133
+ 20,
134
+ 40,
135
+ 16,
136
+ 34
137
+ ];
138
+ function pick(values, row) {
139
+ return values[row % values.length];
140
+ }
141
+ function lineSkeleton(widthCh, className) {
142
+ return /* @__PURE__ */ jsx(Skeleton, {
143
+ "data-slot": "logs-skeleton",
144
+ className: cn("inline-block h-4 max-w-full align-middle rounded-sm font-mono text-xs leading-4", className),
145
+ style: { width: `${widthCh}ch` }
146
+ });
147
+ }
148
+ const BUILTIN_COLUMNS = {
149
+ time: {
150
+ id: "time",
151
+ header: "Time",
152
+ size: "hug",
153
+ cell: ({ entry }) => {
154
+ const { date, time } = formatLogTimestampParts(entry.timestamp);
155
+ return /* @__PURE__ */ jsxs("time", {
156
+ dateTime: entry.timestamp.toISOString(),
157
+ className: "font-mono text-xs whitespace-nowrap",
158
+ children: [
159
+ /* @__PURE__ */ jsx("span", {
160
+ className: "text-muted-foreground",
161
+ children: date
162
+ }),
163
+ " ",
164
+ /* @__PURE__ */ jsx("span", {
165
+ className: "text-foreground",
166
+ children: time
167
+ })
168
+ ]
169
+ });
170
+ },
171
+ skeleton: () => lineSkeleton(TIME_CHS)
172
+ },
173
+ severity: {
174
+ id: "severity",
175
+ header: "Severity",
176
+ size: "hug",
177
+ cell: ({ entry }) => /* @__PURE__ */ jsx(LogsSeverityBadge, { severity: entry.labels.severity }),
178
+ skeleton: () => lineSkeleton(5, "h-4 rounded-md")
179
+ },
180
+ status: {
181
+ id: "status",
182
+ header: "Status",
183
+ size: "hug",
184
+ cell: ({ parsed }) => /* @__PURE__ */ jsx(LogsStatusBadge, { parsed }),
185
+ skeleton: ({ row }) => /* @__PURE__ */ jsxs("span", {
186
+ className: "inline-flex h-4 items-center gap-1.5 font-mono text-[11px] leading-4",
187
+ "data-slot": "logs-skeleton-status",
188
+ children: [lineSkeleton(3, "h-[11px]"), lineSkeleton(pick(METHOD_CHS, row), "h-[11px]")]
189
+ })
190
+ },
191
+ service: {
192
+ id: "service",
193
+ header: "Service",
194
+ size: "fixed",
195
+ width: DEFAULT_FIXED_WIDTH,
196
+ className: "text-muted-foreground truncate font-mono text-xs",
197
+ cell: ({ entry }) => entry.labels.service_name ?? "—",
198
+ skeleton: ({ row }) => lineSkeleton(pick(HOST_CHS, row))
199
+ },
200
+ resource: {
201
+ id: "resource",
202
+ header: "Resource",
203
+ size: "fixed",
204
+ width: DEFAULT_FIXED_WIDTH,
205
+ className: "text-muted-foreground truncate font-mono text-xs",
206
+ cell: ({ entry }) => {
207
+ const value = entry.labels.resource_name;
208
+ return value ? /* @__PURE__ */ jsx("span", {
209
+ title: value,
210
+ children: value
211
+ }) : "—";
212
+ },
213
+ skeleton: ({ row }) => lineSkeleton(pick(HOST_CHS, row))
214
+ },
215
+ host: {
216
+ id: "host",
217
+ header: "Host",
218
+ size: "fixed",
219
+ width: DEFAULT_FIXED_WIDTH,
220
+ className: "text-muted-foreground truncate font-mono text-xs",
221
+ cell: ({ entry }) => {
222
+ const value = logRequestHost(entry.labels);
223
+ return value ? /* @__PURE__ */ jsx("span", {
224
+ title: value,
225
+ children: value
226
+ }) : "—";
227
+ },
228
+ skeleton: ({ row }) => lineSkeleton(pick(HOST_CHS, row))
229
+ },
230
+ path: {
231
+ id: "path",
232
+ header: "Path",
233
+ size: "fill",
234
+ className: "truncate font-mono text-xs",
235
+ cell: ({ path }) => /* @__PURE__ */ jsx("span", {
236
+ className: path ? void 0 : "text-muted-foreground",
237
+ title: path ?? void 0,
238
+ children: path ?? "—"
239
+ }),
240
+ skeleton: ({ row }) => lineSkeleton(pick(PATH_CHS, row))
241
+ },
242
+ message: {
243
+ id: "message",
244
+ header: "Message",
245
+ size: "fill",
246
+ className: "truncate font-mono text-xs",
247
+ cell: ({ message }) => message,
248
+ skeleton: ({ row }) => lineSkeleton(pick(PATH_CHS, row))
249
+ }
250
+ };
251
+ const warnedColumnIds = /* @__PURE__ */ new Set();
252
+ /**
253
+ * Expand column specs into renderable columns. Unknown built-in ids are
254
+ * skipped (with a one-time dev warning) so a typo or a stale URL-driven
255
+ * column list degrades to a narrower table instead of a crash.
256
+ */
257
+ function resolveLogColumns(specs) {
258
+ const columns = [];
259
+ for (const spec of specs) {
260
+ if (typeof spec !== "string") {
261
+ columns.push({
262
+ size: "fixed",
263
+ width: DEFAULT_FIXED_WIDTH,
264
+ ...spec
265
+ });
266
+ continue;
267
+ }
268
+ const builtin = Object.hasOwn(BUILTIN_COLUMNS, spec) ? BUILTIN_COLUMNS[spec] : void 0;
269
+ if (builtin) {
270
+ columns.push(builtin);
271
+ continue;
272
+ }
273
+ if (process.env.NODE_ENV !== "production" && !warnedColumnIds.has(spec)) {
274
+ warnedColumnIds.add(spec);
275
+ console.warn(`[datum-ui/logs] Unknown column id "${spec}" was skipped. Built-in ids: ${Object.keys(BUILTIN_COLUMNS).join(", ")}.`);
276
+ }
277
+ }
278
+ return columns;
279
+ }
280
+ function columnSize(column) {
281
+ return column.size ?? "fixed";
282
+ }
283
+ function columnWidth(column) {
284
+ return column.width ?? DEFAULT_FIXED_WIDTH;
285
+ }
286
+ function columnHeadClass(column) {
287
+ switch (columnSize(column)) {
288
+ case "hug": return "w-px whitespace-nowrap";
289
+ case "fixed": return "whitespace-nowrap";
290
+ case "fill": return "min-w-0";
291
+ }
292
+ }
293
+ function columnCellClass(column) {
294
+ switch (columnSize(column)) {
295
+ case "hug": return "w-px whitespace-nowrap";
296
+ case "fixed": return "truncate";
297
+ case "fill": return "min-w-0 truncate";
298
+ }
299
+ }
300
+ function columnWidthStyle(column) {
301
+ if (columnSize(column) !== "fixed") return void 0;
302
+ const width = columnWidth(column);
303
+ return {
304
+ width,
305
+ maxWidth: width
306
+ };
307
+ }
308
+ function columnSkeleton(column, row) {
309
+ if (column.skeleton) return column.skeleton({ row });
310
+ switch (columnSize(column)) {
311
+ case "hug": return lineSkeleton(column.id === "time" ? TIME_CHS : 8);
312
+ case "fixed": return lineSkeleton(pick(HOST_CHS, row));
313
+ case "fill": return lineSkeleton(pick(PATH_CHS, row));
314
+ }
315
+ }
316
+ function columnHeader(column) {
317
+ return column.header;
318
+ }
319
+ //#endregion
320
+ //#region src/components/features/logs/hooks/use-logs.ts
321
+ const LogsContext = createContext(null);
322
+ function useLogs() {
323
+ const context = use(LogsContext);
324
+ if (!context) throw new Error("Logs components must be used within <Logs.Root>");
325
+ return context;
326
+ }
327
+ function clampLogsDetailWidth(width, maxWidth) {
328
+ return Math.min(Math.max(Math.round(width), 400), maxWidth > 400 ? maxWidth : 400);
329
+ }
330
+ function logsDetailMaxWidth(node) {
331
+ const parentWidth = node?.offsetParent instanceof HTMLElement ? node.offsetParent.clientWidth : 0;
332
+ if (parentWidth > 400) return parentWidth;
333
+ return typeof window === "undefined" ? 400 : window.innerWidth;
334
+ }
335
+ //#endregion
336
+ //#region src/components/features/logs/utils/parse-log-line.ts
337
+ const ACCESS_LOG_RE = /^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s+(\S+)\s+(\d{3})\s+(\d+)ms\b/i;
338
+ const PATH_TOKEN_RE = /(?:^|\s)(\/\S+)/;
339
+ /**
340
+ * Parse an access-log line, falling back to Envoy/OTEL stream labels when the
341
+ * body is empty (queryapi stores method/path/status as attributes).
342
+ */
343
+ function parseLogLine(line, labels = {}) {
344
+ return parseAccessLogText(line) ?? parseAccessLogFields(labels) ?? {
345
+ kind: "text",
346
+ line
347
+ };
348
+ }
349
+ function formatHttpLogLine(parsed) {
350
+ return `${parsed.method} ${parsed.path} ${parsed.status} ${parsed.durationMs}ms`;
351
+ }
352
+ function parseAccessLogText(line) {
353
+ const match = ACCESS_LOG_RE.exec(line);
354
+ if (!match) return null;
355
+ const method = match[1];
356
+ const path = match[2];
357
+ const status = match[3];
358
+ const duration = match[4];
359
+ if (!method || !path || !status || !duration) return null;
360
+ return {
361
+ kind: "http",
362
+ method: method.toUpperCase(),
363
+ path,
364
+ status: Number(status),
365
+ durationMs: Number(duration)
366
+ };
367
+ }
368
+ function parseAccessLogFields(fields) {
369
+ const method = fields.method?.trim();
370
+ const path = fields.path?.trim();
371
+ const status = fields.response_code?.trim() || fields.status?.trim();
372
+ if (!method || !path || !status || !/^\d{3}$/.test(status)) return null;
373
+ return {
374
+ kind: "http",
375
+ method: method.toUpperCase(),
376
+ path,
377
+ status: Number(status),
378
+ durationMs: parseDurationMs(fields.duration)
379
+ };
380
+ }
381
+ /** Envoy `%DURATION%` is milliseconds with no unit. */
382
+ function parseDurationMs(raw) {
383
+ if (!raw) return 0;
384
+ const n = Number.parseFloat(raw.replace(/ms$/i, "").trim());
385
+ return Number.isFinite(n) ? Math.max(0, Math.round(n)) : 0;
386
+ }
387
+ /** Separate `/a/b?x=1&y=2` into its pathname and decoded search params. */
388
+ function splitPathQuery(path) {
389
+ const hashIndex = path.indexOf("#");
390
+ const withoutHash = hashIndex === -1 ? path : path.slice(0, hashIndex);
391
+ const queryIndex = withoutHash.indexOf("?");
392
+ if (queryIndex === -1) return {
393
+ pathname: withoutHash,
394
+ search: "",
395
+ params: []
396
+ };
397
+ const pathname = withoutHash.slice(0, queryIndex);
398
+ const search = withoutHash.slice(queryIndex + 1);
399
+ const params = [];
400
+ for (const [key, value] of new URLSearchParams(search)) if (key) params.push([key, value]);
401
+ return {
402
+ pathname,
403
+ search,
404
+ params
405
+ };
406
+ }
407
+ function logLineDisplay(line, labels = {}) {
408
+ const parsed = parseLogLine(line, labels);
409
+ if (parsed.kind === "http") {
410
+ const match = ACCESS_LOG_RE.exec(line);
411
+ const rest = match ? line.slice(match[0].length).trim() : "";
412
+ return {
413
+ parsed,
414
+ path: parsed.path,
415
+ message: extraMessage(rest)
416
+ };
417
+ }
418
+ const path = labels.path?.trim() || PATH_TOKEN_RE.exec(line)?.[1] || null;
419
+ if (!path) return {
420
+ parsed,
421
+ path: null,
422
+ message: line
423
+ };
424
+ return {
425
+ parsed,
426
+ path,
427
+ message: extraMessage(line ? stripOnce(line, path) : "")
428
+ };
429
+ }
430
+ /** Drop leftover that is only fields already shown (method, status, path, duration, label=value). */
431
+ function extraMessage(rest) {
432
+ if (!rest) return "";
433
+ return rest.split(/\s+/).filter(Boolean).some((token) => !/^[A-Z_]\w*=\S+$/i.test(token)) ? rest : "";
434
+ }
435
+ function stripOnce(line, token) {
436
+ const index = line.indexOf(token);
437
+ if (index === -1) return line;
438
+ return `${line.slice(0, index)}${line.slice(index + token.length)}`.replace(/\s+/g, " ").trim();
439
+ }
440
+ //#endregion
144
441
  //#region src/components/features/logs/components/detail.tsx
145
442
  function MetaCell({ label, children, className }) {
146
443
  return /* @__PURE__ */ jsxs("div", {
@@ -154,77 +451,109 @@ function MetaCell({ label, children, className }) {
154
451
  })]
155
452
  });
156
453
  }
157
- function DetailBody({ showClose = true }) {
454
+ /** Stable React keys for search params, which may repeat `key=value` verbatim. */
455
+ function keyedParams(params) {
456
+ const seen = /* @__PURE__ */ new Map();
457
+ return params.map(([key, value]) => {
458
+ const pair = `${key}=${value}`;
459
+ const count = seen.get(pair) ?? 0;
460
+ seen.set(pair, count + 1);
461
+ return {
462
+ id: count === 0 ? pair : `${pair}#${count}`,
463
+ key,
464
+ value
465
+ };
466
+ });
467
+ }
468
+ const DetailBody = memo(({ showClose = true }) => {
158
469
  const { selectedEntry, selectPrevious, selectNext, setSelectedId } = useLogs();
159
470
  const [, copy] = useCopyToClipboard();
160
471
  if (!selectedEntry) return null;
161
- const parsed = parseLogLine(selectedEntry.line);
162
- const title = parsed.kind === "http" ? `${parsed.method} ${parsed.path}` : selectedEntry.labels.service_name ?? "Log";
163
- const service = selectedEntry.labels.service_name;
472
+ const parsed = parseLogLine(selectedEntry.line, selectedEntry.labels);
473
+ const displayLine = parsed.kind === "http" && !selectedEntry.line.trim() ? formatHttpLogLine(parsed) : selectedEntry.line;
474
+ const request = splitPathQuery(parsed.kind === "http" ? parsed.path : "");
475
+ const title = parsed.kind === "http" ? `${parsed.method} ${request.pathname}` : selectedEntry.labels.service_name ?? "Log";
476
+ const service = selectedEntry.labels.service_name ?? logRequestHost(selectedEntry.labels);
164
477
  return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("header", {
165
- className: "flex items-start gap-2 border-b px-4 py-3",
478
+ className: "border-b px-4 py-3",
166
479
  children: [/* @__PURE__ */ jsxs("div", {
167
- className: "min-w-0 flex-1",
480
+ className: "flex items-center gap-2",
168
481
  children: [
169
- /* @__PURE__ */ jsx("div", {
170
- className: "mb-1.5",
171
- children: /* @__PURE__ */ jsx(LogsSeverityBadge, { severity: selectedEntry.labels.severity })
172
- }),
173
482
  /* @__PURE__ */ jsx("h3", {
174
- className: "truncate font-mono text-sm font-medium",
483
+ className: "min-w-0 flex-1 truncate font-mono text-sm font-medium",
484
+ title,
175
485
  children: title
176
486
  }),
487
+ parsed.kind === "http" ? /* @__PURE__ */ jsx(LogsHttpStatusChip, { status: parsed.status }) : /* @__PURE__ */ jsx(LogsSeverityBadge, { severity: selectedEntry.labels.severity }),
177
488
  /* @__PURE__ */ jsxs("div", {
178
- className: "text-muted-foreground mt-1 flex min-w-0 items-center gap-1.5 text-xs",
179
- children: [service && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
180
- className: "truncate font-mono",
181
- children: service
182
- }), /* @__PURE__ */ jsx("span", {
183
- "aria-hidden": "true",
184
- children: "·"
185
- })] }), /* @__PURE__ */ jsx(DateTime, {
186
- date: selectedEntry.timestamp,
187
- timestamp: selectedEntry.timestampNs,
188
- variant: "relative",
189
- tooltip: "detailed",
190
- className: "text-muted-foreground text-xs"
191
- })]
489
+ className: "-my-1 flex shrink-0 items-center gap-1",
490
+ children: [
491
+ /* @__PURE__ */ jsx(Button, {
492
+ type: "secondary",
493
+ theme: "borderless",
494
+ size: "icon",
495
+ "aria-label": "Previous log",
496
+ onClick: selectPrevious,
497
+ children: /* @__PURE__ */ jsx(Icon, { icon: ChevronUp })
498
+ }),
499
+ /* @__PURE__ */ jsx(Button, {
500
+ type: "secondary",
501
+ theme: "borderless",
502
+ size: "icon",
503
+ "aria-label": "Next log",
504
+ onClick: selectNext,
505
+ children: /* @__PURE__ */ jsx(Icon, { icon: ChevronDown })
506
+ }),
507
+ /* @__PURE__ */ jsx(Button, {
508
+ type: "secondary",
509
+ theme: "borderless",
510
+ size: "icon",
511
+ "aria-label": "Copy log line",
512
+ onClick: () => copy(displayLine, { withToast: true }),
513
+ children: /* @__PURE__ */ jsx(Icon, { icon: Copy })
514
+ }),
515
+ showClose && /* @__PURE__ */ jsx(Button, {
516
+ type: "secondary",
517
+ theme: "borderless",
518
+ size: "icon",
519
+ "aria-label": "Close details",
520
+ onClick: () => setSelectedId(null),
521
+ children: /* @__PURE__ */ jsx(Icon, { icon: X })
522
+ })
523
+ ]
192
524
  })
193
525
  ]
194
526
  }), /* @__PURE__ */ jsxs("div", {
195
- className: "flex shrink-0 items-center gap-1",
527
+ className: "text-muted-foreground mt-1 flex min-w-0 flex-nowrap items-center gap-1.5 text-xs",
196
528
  children: [
197
- /* @__PURE__ */ jsx(Button, {
198
- type: "secondary",
199
- theme: "borderless",
200
- size: "icon",
201
- "aria-label": "Previous log",
202
- onClick: selectPrevious,
203
- children: /* @__PURE__ */ jsx(ChevronUp, { className: "size-4" })
204
- }),
205
- /* @__PURE__ */ jsx(Button, {
206
- type: "secondary",
207
- theme: "borderless",
208
- size: "icon",
209
- "aria-label": "Next log",
210
- onClick: selectNext,
211
- children: /* @__PURE__ */ jsx(ChevronDown, { className: "size-4" })
212
- }),
213
- /* @__PURE__ */ jsx(Button, {
214
- type: "secondary",
215
- theme: "borderless",
216
- size: "icon",
217
- "aria-label": "Copy log line",
218
- onClick: () => copy(selectedEntry.line, { withToast: true }),
219
- children: /* @__PURE__ */ jsx(Copy, { className: "size-4" })
220
- }),
221
- showClose && /* @__PURE__ */ jsx(Button, {
222
- type: "secondary",
223
- theme: "borderless",
224
- size: "icon",
225
- "aria-label": "Close details",
226
- onClick: () => setSelectedId(null),
227
- children: /* @__PURE__ */ jsx(X, { className: "size-4" })
529
+ parsed.kind === "http" && selectedEntry.labels.severity && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(LogsSeverityBadge, { severity: selectedEntry.labels.severity }), /* @__PURE__ */ jsx("span", {
530
+ className: "shrink-0",
531
+ "aria-hidden": "true",
532
+ children: "·"
533
+ })] }),
534
+ service && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
535
+ className: "min-w-0 truncate font-mono",
536
+ children: service
537
+ }), /* @__PURE__ */ jsx("span", {
538
+ className: "shrink-0",
539
+ "aria-hidden": "true",
540
+ children: "·"
541
+ })] }),
542
+ /* @__PURE__ */ jsx(DateTime, {
543
+ date: selectedEntry.timestamp,
544
+ timestamp: selectedEntry.timestampNs,
545
+ variant: "absolute",
546
+ tooltip: "detailed",
547
+ children: /* @__PURE__ */ jsxs("time", {
548
+ dateTime: selectedEntry.timestamp.toISOString(),
549
+ "data-slot": "logs-detail-time",
550
+ className: "shrink-0 cursor-default font-mono whitespace-nowrap tabular-nums",
551
+ children: [
552
+ formatLogTimestamp(selectedEntry.timestamp),
553
+ " ",
554
+ getTimezoneAbbreviation(selectedEntry.timestamp, getBrowserTimezone())
555
+ ]
556
+ })
228
557
  })
229
558
  ]
230
559
  })]
@@ -236,7 +565,7 @@ function DetailBody({ showClose = true }) {
236
565
  children: "Message"
237
566
  }), /* @__PURE__ */ jsx("pre", {
238
567
  className: "bg-muted text-foreground overflow-x-auto rounded-md border p-3 font-mono text-xs leading-5 whitespace-pre-wrap break-all",
239
- children: selectedEntry.line
568
+ children: displayLine
240
569
  })] }),
241
570
  parsed.kind === "http" && /* @__PURE__ */ jsxs("section", { children: [/* @__PURE__ */ jsx("h4", {
242
571
  className: "text-muted-foreground mb-2 text-xs font-medium",
@@ -262,7 +591,42 @@ function DetailBody({ showClose = true }) {
262
591
  /* @__PURE__ */ jsx(MetaCell, {
263
592
  label: "Path",
264
593
  className: "col-span-3",
265
- children: parsed.path
594
+ children: request.pathname
595
+ }),
596
+ request.params.length > 0 && /* @__PURE__ */ jsxs("div", {
597
+ className: "bg-background col-span-3 min-w-0 px-3 py-2.5",
598
+ "data-slot": "logs-search-params",
599
+ children: [/* @__PURE__ */ jsxs("div", {
600
+ className: "mb-1.5 flex items-center justify-between",
601
+ children: [/* @__PURE__ */ jsx("span", {
602
+ className: "text-muted-foreground text-[11px] font-medium",
603
+ children: "Search params"
604
+ }), /* @__PURE__ */ jsx(Button, {
605
+ type: "secondary",
606
+ theme: "borderless",
607
+ size: "icon",
608
+ className: "-my-1 size-6",
609
+ "aria-label": "Copy search params",
610
+ onClick: () => copy(`?${request.search}`, { withToast: true }),
611
+ children: /* @__PURE__ */ jsx(Icon, {
612
+ icon: Copy,
613
+ size: 14
614
+ })
615
+ })]
616
+ }), /* @__PURE__ */ jsx("dl", {
617
+ className: "flex flex-wrap gap-1.5",
618
+ children: keyedParams(request.params).map(({ id, key, value }) => /* @__PURE__ */ jsxs("div", {
619
+ className: "bg-muted/60 inline-flex max-w-full items-stretch overflow-hidden rounded-md border font-mono text-xs",
620
+ children: [/* @__PURE__ */ jsx("dt", {
621
+ className: "text-muted-foreground border-r px-2 py-1",
622
+ children: key
623
+ }), /* @__PURE__ */ jsx("dd", {
624
+ className: "min-w-0 truncate px-2 py-1",
625
+ title: value,
626
+ children: value || "—"
627
+ })]
628
+ }, id))
629
+ })]
266
630
  })
267
631
  ]
268
632
  })] }),
@@ -284,6 +648,59 @@ function DetailBody({ showClose = true }) {
284
648
  })] })
285
649
  ]
286
650
  })] });
651
+ });
652
+ function LogsDetailResizeHandle({ width, onWidthChange, panelRef }) {
653
+ const dragRef = useRef(null);
654
+ const [maxWidth, setMaxWidth] = useState(() => logsDetailMaxWidth(null));
655
+ const onPointerDown = useCallback((event) => {
656
+ if (event.button !== 0) return;
657
+ event.preventDefault();
658
+ const max = logsDetailMaxWidth(panelRef.current);
659
+ setMaxWidth(max);
660
+ dragRef.current = {
661
+ startX: event.clientX,
662
+ startWidth: width,
663
+ maxWidth: max
664
+ };
665
+ event.currentTarget.setPointerCapture(event.pointerId);
666
+ }, [panelRef, width]);
667
+ const onPointerMove = useCallback((event) => {
668
+ const drag = dragRef.current;
669
+ if (!drag) return;
670
+ onWidthChange(clampLogsDetailWidth(drag.startWidth + (drag.startX - event.clientX), drag.maxWidth));
671
+ }, [onWidthChange]);
672
+ const onPointerUp = useCallback((event) => {
673
+ dragRef.current = null;
674
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId);
675
+ }, []);
676
+ return /* @__PURE__ */ jsx("div", {
677
+ role: "separator",
678
+ "aria-orientation": "vertical",
679
+ "aria-label": "Resize details",
680
+ "aria-valuemin": 400,
681
+ "aria-valuemax": maxWidth,
682
+ "aria-valuenow": width,
683
+ "data-slot": "logs-detail-resize",
684
+ tabIndex: 0,
685
+ className: cn("absolute inset-y-0 left-0 z-10 w-3 -translate-x-1/2 cursor-ew-resize", "after:bg-transparent hover:after:bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-0.5 after:-translate-x-1/2"),
686
+ onPointerDown,
687
+ onPointerMove,
688
+ onPointerUp,
689
+ onPointerCancel: onPointerUp,
690
+ onKeyDown: (event) => {
691
+ const step = event.shiftKey ? 40 : 16;
692
+ if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
693
+ event.preventDefault();
694
+ const max = logsDetailMaxWidth(panelRef.current);
695
+ setMaxWidth(max);
696
+ onWidthChange(clampLogsDetailWidth(width + (event.key === "ArrowLeft" ? step : -step), max));
697
+ }
698
+ if (event.key === "Home") {
699
+ event.preventDefault();
700
+ onWidthChange(400);
701
+ }
702
+ }
703
+ });
287
704
  }
288
705
  function LogsDetail({ className }) {
289
706
  const { selectedEntry, setSelectedId } = useLogs();
@@ -291,6 +708,8 @@ function LogsDetail({ className }) {
291
708
  const reduceMotion = useReducedMotion();
292
709
  const isDesktop = breakpoint === "desktop";
293
710
  const open = selectedEntry !== null;
711
+ const panelRef = useRef(null);
712
+ const [width, setWidth] = useState(400);
294
713
  if (!isDesktop) return /* @__PURE__ */ jsx(Sheet, {
295
714
  open,
296
715
  onOpenChange: (next) => {
@@ -317,23 +736,101 @@ function LogsDetail({ className }) {
317
736
  ]
318
737
  })
319
738
  });
320
- return /* @__PURE__ */ jsx(AnimatePresence, { children: open && /* @__PURE__ */ jsx(motion.aside, {
739
+ return /* @__PURE__ */ jsx(AnimatePresence, { children: open && /* @__PURE__ */ jsxs(motion.aside, {
740
+ ref: panelRef,
321
741
  "data-slot": "logs-detail",
322
- initial: reduceMotion ? false : { width: 0 },
323
- animate: { width: 400 },
324
- exit: { width: 0 },
742
+ initial: reduceMotion ? false : { x: "100%" },
743
+ animate: { x: 0 },
744
+ exit: { x: "100%" },
325
745
  transition: reduceMotion ? { duration: 0 } : {
326
746
  duration: .2,
327
747
  ease: "easeOut"
328
748
  },
329
- className: cn("bg-background flex h-full shrink-0 flex-col overflow-hidden border-l", className),
330
- children: /* @__PURE__ */ jsx("div", {
331
- className: "flex h-full w-[400px] flex-col",
332
- children: /* @__PURE__ */ jsx(DetailBody, {})
333
- })
749
+ style: { width },
750
+ className: cn("bg-background absolute inset-y-0 right-0 z-20 flex h-full flex-col overflow-hidden border-l shadow-lg", className),
751
+ children: [/* @__PURE__ */ jsx(LogsDetailResizeHandle, {
752
+ width,
753
+ onWidthChange: setWidth,
754
+ panelRef
755
+ }), /* @__PURE__ */ jsx(DetailBody, {})]
334
756
  }) });
335
757
  }
336
758
  //#endregion
759
+ //#region src/components/features/logs/utils/time-range.ts
760
+ const MINUTE_MS$1 = 6e4;
761
+ const HOUR_MS = 60 * MINUTE_MS$1;
762
+ const DAY_MS$1 = 24 * HOUR_MS;
763
+ function relativePreset(key, label, durationMs, shortcut) {
764
+ return {
765
+ key,
766
+ label,
767
+ shortcut,
768
+ getRange: () => {
769
+ const now = /* @__PURE__ */ new Date();
770
+ return {
771
+ from: new Date(now.getTime() - durationMs),
772
+ to: now
773
+ };
774
+ }
775
+ };
776
+ }
777
+ /**
778
+ * Relative "Last N" windows offered by `Logs.Filters`. Keys are persisted on
779
+ * `LogTimeRange.preset` so hosts can slide the window forward with
780
+ * `resolveLogTimeRange` on refresh or while live tailing.
781
+ *
782
+ * Shortcuts follow the `picker` datetime presets (`1` / `2` / `H` / `W`) that
783
+ * the logs picker is built on. Keys that mean something else in the pickers
784
+ * (`3` is "Last 30 minutes" in `time-range-picker`, `D` is "Today") are not
785
+ * reused, so muscle memory carries over between the two.
786
+ */
787
+ const LOG_TIME_PRESETS = [
788
+ relativePreset("last-15m", "Last 15 minutes", 15 * MINUTE_MS$1, "1"),
789
+ relativePreset("last-30m", "Last 30 minutes", 30 * MINUTE_MS$1, "2"),
790
+ relativePreset("last-1h", "Last hour", HOUR_MS, "H"),
791
+ relativePreset("last-3h", "Last 3 hours", 3 * HOUR_MS),
792
+ relativePreset("last-6h", "Last 6 hours", 6 * HOUR_MS, "6"),
793
+ relativePreset("last-12h", "Last 12 hours", 12 * HOUR_MS),
794
+ relativePreset("last-24h", "Last 24 hours", DAY_MS$1),
795
+ relativePreset("last-7d", "Last 7 days", 7 * DAY_MS$1, "W")
796
+ ];
797
+ function lastThirtyMinutes(now = /* @__PURE__ */ new Date()) {
798
+ return {
799
+ from: (/* @__PURE__ */ new Date(now.getTime() - 30 * MINUTE_MS$1)).toISOString(),
800
+ to: now.toISOString(),
801
+ preset: "last-30m"
802
+ };
803
+ }
804
+ /**
805
+ * Re-anchor a preset range to `now`. Absolute ranges (no `preset`, or an
806
+ * unknown key) are returned unchanged.
807
+ */
808
+ function resolveLogTimeRange(range, now = /* @__PURE__ */ new Date()) {
809
+ if (!range.preset) return range;
810
+ const preset = LOG_TIME_PRESETS.find((item) => item.key === range.preset);
811
+ if (!preset) return range;
812
+ const { from, to } = preset.getRange("UTC");
813
+ const shift = now.getTime() - to.getTime();
814
+ return {
815
+ from: new Date(from.getTime() + shift).toISOString(),
816
+ to: now.toISOString(),
817
+ preset: range.preset
818
+ };
819
+ }
820
+ /** Human label for the time-range trigger: preset name, or a compact absolute range. */
821
+ function logTimeRangeLabel(range) {
822
+ const preset = range.preset ? LOG_TIME_PRESETS.find((item) => item.key === range.preset) : void 0;
823
+ if (preset) return preset.label;
824
+ const from = new Date(range.from);
825
+ const to = new Date(range.to);
826
+ if (Number.isNaN(from.getTime()) || Number.isNaN(to.getTime())) return "Select range";
827
+ if (isSameDay(from, to)) return `${format(from, "MMM d, HH:mm")} – ${format(to, "HH:mm")}`;
828
+ return `${format(from, "MMM d, HH:mm")} – ${format(to, "MMM d, HH:mm")}`;
829
+ }
830
+ function filtersAreActive(filters) {
831
+ return Object.values(filters).some((values) => values.length > 0);
832
+ }
833
+ //#endregion
337
834
  //#region src/components/features/logs/components/filters.tsx
338
835
  function LogsFilterGroup({ facet, defaultOpen }) {
339
836
  const { filters, toggleFilterValue } = useLogs();
@@ -343,7 +840,10 @@ function LogsFilterGroup({ facet, defaultOpen }) {
343
840
  className: "border-b border-border",
344
841
  children: [/* @__PURE__ */ jsxs(CollapsibleTrigger, {
345
842
  className: "hover:bg-muted/40 flex w-full items-center justify-between px-3 py-2 text-left text-sm font-medium",
346
- children: [facet.label, /* @__PURE__ */ jsx(ChevronDown, { className: "text-muted-foreground size-4 transition-transform [[data-state=open]_&]:rotate-180" })]
843
+ children: [facet.label, /* @__PURE__ */ jsx(Icon, {
844
+ icon: ChevronDown,
845
+ className: "text-muted-foreground transition-transform [[data-state=open]_&]:rotate-180"
846
+ })]
347
847
  }), /* @__PURE__ */ jsx(CollapsibleContent, { children: /* @__PURE__ */ jsx("ul", {
348
848
  className: "flex flex-col gap-1 px-3 pb-3",
349
849
  children: facet.options.map((option) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("label", {
@@ -367,6 +867,9 @@ function LogsFilterGroup({ facet, defaultOpen }) {
367
867
  }) })]
368
868
  });
369
869
  }
870
+ function isLogTimeRange(value) {
871
+ return typeof value === "object" && value !== null && typeof value.from === "string" && typeof value.to === "string";
872
+ }
370
873
  function LogsTimeRangeFilter({ className }) {
371
874
  const { timeRange, setTimeRange } = useLogs();
372
875
  return /* @__PURE__ */ jsxs("div", {
@@ -377,23 +880,83 @@ function LogsTimeRangeFilter({ className }) {
377
880
  }), /* @__PURE__ */ jsx(DateTimeRangePicker, {
378
881
  value: timeRange,
379
882
  onChange: (next) => {
380
- if (next) setTimeRange({
883
+ if (!next) return;
884
+ const preset = next.preset;
885
+ setTimeRange(preset ? {
886
+ from: next.from,
887
+ to: next.to,
888
+ preset
889
+ } : {
381
890
  from: next.from,
382
891
  to: next.to
383
892
  });
384
893
  },
894
+ presets: LOG_TIME_PRESETS,
895
+ triggerLabel: (value) => isLogTimeRange(value) ? logTimeRangeLabel(value) : "Select range",
896
+ icon: /* @__PURE__ */ jsx(Icon, {
897
+ icon: Clock,
898
+ className: "text-muted-foreground shrink-0"
899
+ }),
385
900
  placeholder: "Select range",
901
+ clearable: false,
386
902
  disableFuture: true,
387
903
  hideTimezone: true,
388
904
  numberOfMonths: 1,
389
905
  className: "w-full",
390
906
  triggerClassName: "w-full justify-start text-xs",
907
+ popoverClassName: "w-auto",
391
908
  sheetTitle: "Select time range"
392
909
  })]
393
910
  });
394
911
  }
912
+ const FILTER_LABEL_CHS = [
913
+ 6,
914
+ 11,
915
+ 4
916
+ ];
917
+ const FILTER_VALUE_CHS = [
918
+ 3,
919
+ 8,
920
+ 14,
921
+ 6,
922
+ 20,
923
+ 11,
924
+ 4,
925
+ 16
926
+ ];
927
+ function LogsFiltersSkeleton() {
928
+ return /* @__PURE__ */ jsx("div", {
929
+ "data-slot": "logs-filters-skeleton",
930
+ className: "flex flex-col",
931
+ "aria-hidden": true,
932
+ children: Array.from({ length: 3 }, (_, group) => /* @__PURE__ */ jsxs("div", {
933
+ className: "border-b border-border",
934
+ children: [/* @__PURE__ */ jsxs("div", {
935
+ className: "flex w-full items-center justify-between px-3 py-2",
936
+ children: [/* @__PURE__ */ jsx(Skeleton, {
937
+ className: "h-[21px] rounded-sm font-medium",
938
+ style: { width: `${FILTER_LABEL_CHS[group] ?? 8}ch` }
939
+ }), /* @__PURE__ */ jsx(Skeleton, { className: "size-4 shrink-0 rounded-sm" })]
940
+ }), /* @__PURE__ */ jsx("ul", {
941
+ className: "flex flex-col gap-1 px-3 pb-3",
942
+ children: Array.from({ length: 4 }, (_, row) => /* @__PURE__ */ jsxs("li", {
943
+ className: "flex items-center gap-2 px-1 py-1",
944
+ children: [
945
+ /* @__PURE__ */ jsx(Skeleton, { className: "size-4 shrink-0 rounded-[4px]" }),
946
+ /* @__PURE__ */ jsx(Skeleton, {
947
+ className: "h-4 max-w-full rounded-sm font-mono text-xs leading-4",
948
+ style: { width: `${FILTER_VALUE_CHS[(group * 4 + row) % FILTER_VALUE_CHS.length]}ch` }
949
+ }),
950
+ /* @__PURE__ */ jsx(Skeleton, { className: "ml-auto h-4 w-[2ch] rounded-sm" })
951
+ ]
952
+ }, row))
953
+ })]
954
+ }, group))
955
+ });
956
+ }
395
957
  function LogsFilters({ className }) {
396
- const { facets, hasActiveFilters, resetFilters } = useLogs();
958
+ const { facets, hasActiveFilters, resetFilters, isLoading } = useLogs();
959
+ const showFacetSkeleton = isLoading && facets.length === 0;
397
960
  return /* @__PURE__ */ jsxs("aside", {
398
961
  "data-slot": "logs-filters",
399
962
  className: cn("bg-background flex h-full min-h-0 w-64 shrink-0 flex-col border-r", className),
@@ -411,7 +974,7 @@ function LogsFilters({ className }) {
411
974
  })]
412
975
  }), /* @__PURE__ */ jsxs("div", {
413
976
  className: "min-h-0 flex-1 overflow-y-auto",
414
- children: [/* @__PURE__ */ jsx(LogsTimeRangeFilter, {}), facets.map((facet, index) => /* @__PURE__ */ jsx(LogsFilterGroup, {
977
+ children: [/* @__PURE__ */ jsx(LogsTimeRangeFilter, {}), showFacetSkeleton ? /* @__PURE__ */ jsx(LogsFiltersSkeleton, {}) : facets.map((facet, index) => /* @__PURE__ */ jsx(LogsFilterGroup, {
415
978
  facet,
416
979
  defaultOpen: index === 0
417
980
  }, facet.name))]
@@ -419,61 +982,78 @@ function LogsFilters({ className }) {
419
982
  });
420
983
  }
421
984
  //#endregion
422
- //#region src/components/features/logs/utils/format-timestamp.ts
423
- function formatLogTimestamp(date) {
424
- return format(date, "MMM dd HH:mm:ss.SS").toUpperCase();
425
- }
426
- //#endregion
427
985
  //#region src/components/features/logs/components/table.tsx
428
- const COLUMN_LABEL = {
429
- time: "Time",
430
- severity: "Severity",
431
- status: "Status",
432
- service: "Service",
433
- resource: "Resource",
434
- path: "Path",
435
- message: "Message"
436
- };
437
- const COLUMN_HEAD_CLASS = {
438
- time: "w-[168px]",
439
- severity: "w-[88px]",
440
- status: "w-[110px]",
441
- service: "w-[160px]",
442
- resource: "w-[160px]",
443
- path: "w-[200px]",
444
- message: ""
445
- };
446
- function LogCell({ column, entry, parsed, path, message }) {
447
- switch (column) {
448
- case "time": return /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx("time", {
449
- dateTime: entry.timestamp.toISOString(),
450
- className: "text-muted-foreground font-mono text-xs whitespace-nowrap",
451
- children: formatLogTimestamp(entry.timestamp)
452
- }) });
453
- case "severity": return /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx(LogsSeverityBadge, { severity: entry.labels.severity }) });
454
- case "status": return /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx(LogsStatusBadge, { parsed }) });
455
- case "service": return /* @__PURE__ */ jsx(TableCell, {
456
- className: "text-muted-foreground truncate font-mono text-xs",
457
- children: entry.labels.service_name ?? "—"
458
- });
459
- case "resource": return /* @__PURE__ */ jsx(TableCell, {
460
- className: "text-muted-foreground truncate font-mono text-xs",
461
- children: entry.labels.resource_name ?? "—"
462
- });
463
- case "path": return /* @__PURE__ */ jsx(TableCell, {
464
- className: cn("truncate font-mono text-xs", !path && "text-muted-foreground"),
465
- title: path ?? void 0,
466
- children: path ?? "—"
467
- });
468
- case "message": return /* @__PURE__ */ jsx(TableCell, {
469
- className: "truncate font-mono text-xs",
470
- children: message
471
- });
472
- }
986
+ const SKELETON_ROWS = 12;
987
+ /** Centred message row shared by the empty and error states. */
988
+ function LogsTableState({ icon, title, description, action, colSpan, slot, role, tone = "muted" }) {
989
+ return /* @__PURE__ */ jsx(TableRow, {
990
+ className: "hover:bg-transparent",
991
+ children: /* @__PURE__ */ jsx(TableCell, {
992
+ colSpan,
993
+ className: "pt-20 pb-12 text-center",
994
+ children: /* @__PURE__ */ jsxs("div", {
995
+ role,
996
+ "data-slot": slot,
997
+ className: "mx-auto flex max-w-md flex-col items-center gap-2 px-4",
998
+ children: [
999
+ /* @__PURE__ */ jsx("span", {
1000
+ className: cn("flex size-9 items-center justify-center rounded-full border", tone === "destructive" ? "border-destructive/20 bg-destructive/10 text-destructive" : "border-border bg-muted text-muted-foreground"),
1001
+ children: icon
1002
+ }),
1003
+ /* @__PURE__ */ jsx("p", {
1004
+ className: "text-foreground text-sm font-medium",
1005
+ children: title
1006
+ }),
1007
+ description && /* @__PURE__ */ jsx("p", {
1008
+ className: "text-muted-foreground text-xs",
1009
+ children: description
1010
+ }),
1011
+ action && /* @__PURE__ */ jsx("div", {
1012
+ className: "mt-1.5",
1013
+ children: action
1014
+ })
1015
+ ]
1016
+ })
1017
+ })
1018
+ });
1019
+ }
1020
+ function LogCell({ column, children }) {
1021
+ return /* @__PURE__ */ jsx(TableCell, {
1022
+ className: cn(columnCellClass(column), column.className),
1023
+ style: columnWidthStyle(column),
1024
+ children
1025
+ });
473
1026
  }
474
1027
  function LogsTable({ className }) {
475
- const { entries, columns, selectedId, setSelectedId, isLoading, error, selectPrevious, selectNext } = useLogs();
1028
+ const { entries, columns, selectedId, setSelectedId, isLoading, error, selectPrevious, selectNext, onRefresh, hasActiveFilters, resetFilters, search, setSearch } = useLogs();
476
1029
  const scrollerRef = useRef(null);
1030
+ const rows = useMemo(() => entries.map((entry) => {
1031
+ const { parsed, path, message } = logLineDisplay(entry.line, entry.labels);
1032
+ return {
1033
+ entry,
1034
+ ctx: {
1035
+ entry,
1036
+ parsed,
1037
+ path,
1038
+ message
1039
+ }
1040
+ };
1041
+ }), [entries]);
1042
+ const hasRows = entries.length > 0;
1043
+ const showError = Boolean(error) && !hasRows;
1044
+ const showErrorBanner = Boolean(error) && hasRows;
1045
+ const showSkeleton = isLoading && !hasRows && !error;
1046
+ const showEmpty = !isLoading && !hasRows && !error;
1047
+ const retry = onRefresh && /* @__PURE__ */ jsxs(Button, {
1048
+ type: "secondary",
1049
+ theme: "outline",
1050
+ size: "small",
1051
+ onClick: onRefresh,
1052
+ children: [/* @__PURE__ */ jsx(Icon, {
1053
+ icon: RefreshCw,
1054
+ size: 14
1055
+ }), "Retry"]
1056
+ });
477
1057
  function onKeyDown(event) {
478
1058
  if (event.key === "ArrowDown" || event.key === "j") {
479
1059
  event.preventDefault();
@@ -492,52 +1072,233 @@ function LogsTable({ className }) {
492
1072
  tabIndex: 0,
493
1073
  "aria-busy": isLoading || void 0,
494
1074
  onKeyDown,
495
- children: [error && /* @__PURE__ */ jsx("div", {
1075
+ children: [showErrorBanner && /* @__PURE__ */ jsxs("div", {
496
1076
  role: "alert",
497
- className: "text-destructive px-4 py-3 text-sm",
498
- children: error
1077
+ "data-slot": "logs-error-banner",
1078
+ className: "border-destructive/30 bg-destructive/5 text-foreground sticky top-0 z-20 flex items-center gap-2 border-b px-3 py-1.5 text-xs",
1079
+ children: [
1080
+ /* @__PURE__ */ jsx(Icon, {
1081
+ icon: TriangleAlert,
1082
+ size: 14,
1083
+ className: "text-destructive shrink-0"
1084
+ }),
1085
+ /* @__PURE__ */ jsxs("span", {
1086
+ className: "min-w-0 truncate",
1087
+ children: [
1088
+ /* @__PURE__ */ jsx("span", {
1089
+ className: "font-medium",
1090
+ children: "Refresh failed."
1091
+ }),
1092
+ " ",
1093
+ /* @__PURE__ */ jsx("span", {
1094
+ className: "text-muted-foreground",
1095
+ children: "Showing the last successful result."
1096
+ }),
1097
+ " ",
1098
+ /* @__PURE__ */ jsx("code", {
1099
+ className: "font-mono",
1100
+ children: error
1101
+ })
1102
+ ]
1103
+ }),
1104
+ /* @__PURE__ */ jsx("span", {
1105
+ className: "ml-auto shrink-0",
1106
+ children: retry
1107
+ })
1108
+ ]
499
1109
  }), /* @__PURE__ */ jsxs("table", {
500
- className: "w-full caption-bottom table-fixed text-sm",
1110
+ className: "w-full caption-bottom table-auto text-sm [&_th]:px-3 [&_th]:py-2 [&_td]:px-3 [&_td]:py-1.5",
501
1111
  children: [/* @__PURE__ */ jsx(TableHeader, {
502
1112
  className: "bg-background sticky top-0 z-10",
503
1113
  children: /* @__PURE__ */ jsx(TableRow, { children: columns.map((column) => /* @__PURE__ */ jsx(TableHead, {
504
- className: COLUMN_HEAD_CLASS[column],
505
- children: COLUMN_LABEL[column]
506
- }, column)) })
1114
+ className: cn(columnHeadClass(column), column.headerClassName),
1115
+ style: columnWidthStyle(column),
1116
+ children: columnHeader(column)
1117
+ }, column.id)) })
507
1118
  }), /* @__PURE__ */ jsxs(TableBody, { children: [
508
- isLoading && entries.length === 0 && Array.from({ length: 8 }, (_, index) => /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(TableCell, {
1119
+ showSkeleton && Array.from({ length: SKELETON_ROWS }, (_, index) => /* @__PURE__ */ jsx(TableRow, {
1120
+ "data-slot": "logs-skeleton-row",
1121
+ children: columns.map((column) => /* @__PURE__ */ jsx(LogCell, {
1122
+ column,
1123
+ children: columnSkeleton(column, index)
1124
+ }, column.id))
1125
+ }, `skeleton-${index}`)),
1126
+ showError && /* @__PURE__ */ jsx(LogsTableState, {
1127
+ role: "alert",
1128
+ slot: "logs-error",
1129
+ tone: "destructive",
509
1130
  colSpan: columns.length,
510
- children: /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-full" })
511
- }) }, `skeleton-${index}`)),
512
- !isLoading && entries.length === 0 && !error && /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(TableCell, {
1131
+ icon: /* @__PURE__ */ jsx(Icon, { icon: TriangleAlert }),
1132
+ title: "Couldn't load logs",
1133
+ description: /* @__PURE__ */ jsx("code", {
1134
+ className: "bg-muted text-foreground inline-block max-w-full rounded-md border px-2 py-1 text-left font-mono text-[11px] leading-4 break-all",
1135
+ children: error
1136
+ }),
1137
+ action: retry
1138
+ }),
1139
+ showEmpty && /* @__PURE__ */ jsx(LogsTableState, {
1140
+ slot: "logs-empty",
513
1141
  colSpan: columns.length,
514
- className: "text-muted-foreground py-10 text-center text-sm",
515
- children: "No logs in this time range"
516
- }) }),
517
- entries.map((entry) => {
518
- const { parsed, path, message } = logLineDisplay(entry.line);
1142
+ icon: /* @__PURE__ */ jsx(Icon, { icon: Inbox }),
1143
+ title: "No logs in this time range",
1144
+ description: hasActiveFilters || search ? "Try widening the range or clearing the filters." : "Try widening the range or check back in a moment.",
1145
+ action: (hasActiveFilters || search) && /* @__PURE__ */ jsx(Button, {
1146
+ type: "secondary",
1147
+ theme: "outline",
1148
+ size: "small",
1149
+ onClick: () => {
1150
+ resetFilters();
1151
+ setSearch("");
1152
+ },
1153
+ children: "Clear filters"
1154
+ })
1155
+ }),
1156
+ rows.map(({ entry, ctx }) => {
519
1157
  const selected = entry.id === selectedId;
520
1158
  return /* @__PURE__ */ jsx(TableRow, {
521
1159
  "data-state": selected ? "selected" : void 0,
522
1160
  "aria-selected": selected,
523
- className: cn("cursor-pointer hover:bg-foreground/[0.08]", "data-[state=selected]:bg-foreground/[0.14]", "data-[state=selected]:hover:bg-foreground/[0.14]"),
1161
+ className: cn("cursor-pointer hover:bg-foreground/5", "data-[state=selected]:bg-foreground/10", "data-[state=selected]:hover:bg-foreground/10"),
524
1162
  onClick: () => {
525
1163
  setSelectedId(selected ? null : entry.id);
526
1164
  scrollerRef.current?.focus();
527
1165
  },
528
1166
  children: columns.map((column) => /* @__PURE__ */ jsx(LogCell, {
529
1167
  column,
530
- entry,
531
- parsed,
532
- path,
533
- message
534
- }, column))
1168
+ children: column.cell(ctx)
1169
+ }, column.id))
535
1170
  }, entry.id);
536
1171
  })
537
1172
  ] })]
538
1173
  })]
539
1174
  });
540
1175
  }
1176
+ const MINUTE_MS = 6e4;
1177
+ const DAY_MS = 24 * (60 * MINUTE_MS);
1178
+ /**
1179
+ * Bucket `entries` into `bucketCount` equal-width bars across `range`.
1180
+ * Entries outside the range are ignored. Returns `[]` when the range is
1181
+ * invalid or empty so the timeline can fall back to an axis-only state.
1182
+ */
1183
+ function histogramFromEntries(entries, range, bucketCount = 60) {
1184
+ const from = new Date(range.from).getTime();
1185
+ const to = new Date(range.to).getTime();
1186
+ if (Number.isNaN(from) || Number.isNaN(to) || to <= from || bucketCount < 1) return [];
1187
+ const width = (to - from) / bucketCount;
1188
+ const counts = Array.from({ length: bucketCount }).fill(0);
1189
+ for (const entry of entries) {
1190
+ const at = entry.timestamp.getTime();
1191
+ if (at < from || at > to) continue;
1192
+ const index = Math.min(Math.floor((at - from) / width), bucketCount - 1);
1193
+ counts[index] += 1;
1194
+ }
1195
+ return counts.map((count, index) => ({
1196
+ start: new Date(from + index * width).toISOString(),
1197
+ end: new Date(index === bucketCount - 1 ? to : from + (index + 1) * width).toISOString(),
1198
+ count
1199
+ }));
1200
+ }
1201
+ /** Index of the bucket containing `date`, or `-1`. */
1202
+ function histogramBucketIndex(histogram, date) {
1203
+ const at = date.getTime();
1204
+ const last = histogram.length - 1;
1205
+ return histogram.findIndex((bucket, index) => {
1206
+ const start = new Date(bucket.start).getTime();
1207
+ const end = new Date(bucket.end).getTime();
1208
+ return at >= start && (at < end || index === last && at === end);
1209
+ });
1210
+ }
1211
+ /** Total span covered by a histogram, in ms. `0` when empty. */
1212
+ function histogramSpanMs(histogram) {
1213
+ const first = histogram[0];
1214
+ const last = histogram[histogram.length - 1];
1215
+ if (!first || !last) return 0;
1216
+ return new Date(last.end).getTime() - new Date(first.start).getTime();
1217
+ }
1218
+ /** Axis tick label whose precision follows the span being displayed. */
1219
+ function formatTimelineTick(date, spanMs) {
1220
+ if (spanMs <= 2 * MINUTE_MS) return format(date, "HH:mm:ss");
1221
+ if (spanMs < DAY_MS) return format(date, "HH:mm");
1222
+ return format(date, "MMM d HH:mm");
1223
+ }
1224
+ /** Tooltip text for a bucket: count plus its window. */
1225
+ function formatBucketSummary(bucket, spanMs) {
1226
+ const start = new Date(bucket.start);
1227
+ const end = new Date(bucket.end);
1228
+ const tickSpan = end.getTime() - start.getTime() < MINUTE_MS ? MINUTE_MS : spanMs;
1229
+ const noun = bucket.count === 1 ? "log" : "logs";
1230
+ return `${bucket.count} ${noun} · ${formatTimelineTick(start, tickSpan)} – ${formatTimelineTick(end, tickSpan)}`;
1231
+ }
1232
+ //#endregion
1233
+ //#region src/components/features/logs/components/timeline.tsx
1234
+ const SKELETON_HEIGHTS = [
1235
+ 18,
1236
+ 34,
1237
+ 22,
1238
+ 48,
1239
+ 30,
1240
+ 60,
1241
+ 26,
1242
+ 40,
1243
+ 20,
1244
+ 52,
1245
+ 36,
1246
+ 28
1247
+ ];
1248
+ function TimelineSkeleton() {
1249
+ return /* @__PURE__ */ jsx("div", {
1250
+ "data-slot": "logs-timeline-skeleton",
1251
+ className: "flex h-10 items-end gap-px",
1252
+ "aria-hidden": true,
1253
+ children: Array.from({ length: 60 }, (_, index) => /* @__PURE__ */ jsx(Skeleton, {
1254
+ className: "min-w-0 flex-1 rounded-[1px]",
1255
+ style: { height: `${SKELETON_HEIGHTS[index % SKELETON_HEIGHTS.length]}%` }
1256
+ }, index))
1257
+ });
1258
+ }
1259
+ function LogsTimeline({ className }) {
1260
+ const { histogram, isLoading, selectedEntry, timeRange } = useLogs();
1261
+ const spanMs = histogramSpanMs(histogram);
1262
+ const max = useMemo(() => histogram.reduce((peak, bucket) => Math.max(peak, bucket.count), 0), [histogram]);
1263
+ const total = useMemo(() => histogram.reduce((sum, bucket) => sum + bucket.count, 0), [histogram]);
1264
+ const selectedIndex = selectedEntry ? histogramBucketIndex(histogram, selectedEntry.timestamp) : -1;
1265
+ const titles = useMemo(() => histogram.map((bucket) => formatBucketSummary(bucket, spanMs)), [histogram, spanMs]);
1266
+ const from = histogram[0] ? new Date(histogram[0].start) : new Date(timeRange.from);
1267
+ const to = histogram.length > 0 ? new Date(histogram[histogram.length - 1].end) : new Date(timeRange.to);
1268
+ const axisSpan = spanMs > 0 ? spanMs : to.getTime() - from.getTime();
1269
+ const mid = /* @__PURE__ */ new Date((from.getTime() + to.getTime()) / 2);
1270
+ const showSkeleton = isLoading && histogram.length === 0;
1271
+ return /* @__PURE__ */ jsxs("div", {
1272
+ "data-slot": "logs-timeline",
1273
+ className: cn("border-b px-3 pt-2 pb-1", className),
1274
+ children: [showSkeleton ? /* @__PURE__ */ jsx(TimelineSkeleton, {}) : /* @__PURE__ */ jsx("div", {
1275
+ role: "img",
1276
+ "aria-label": `${total} logs between ${formatTimelineTick(from, axisSpan)} and ${formatTimelineTick(to, axisSpan)}`,
1277
+ className: "flex h-10 items-end gap-px",
1278
+ children: histogram.map((bucket, index) => {
1279
+ const height = max > 0 ? bucket.count / max * 100 : 0;
1280
+ const selected = index === selectedIndex;
1281
+ return /* @__PURE__ */ jsx("div", {
1282
+ title: titles[index],
1283
+ "data-slot": "logs-timeline-bucket",
1284
+ "data-selected": selected || void 0,
1285
+ className: "relative flex h-full min-w-0 flex-1 items-end",
1286
+ children: /* @__PURE__ */ jsx("div", {
1287
+ className: cn("w-full rounded-[1px] transition-colors", bucket.count > 0 ? "bg-foreground/30 hover:bg-foreground/50" : "bg-foreground/[0.06]", selected && "bg-primary hover:bg-primary"),
1288
+ style: { height: bucket.count > 0 ? `max(${height}%, 2px)` : "1px" }
1289
+ })
1290
+ }, bucket.start);
1291
+ })
1292
+ }), /* @__PURE__ */ jsxs("div", {
1293
+ className: "text-muted-foreground mt-1 flex justify-between font-mono text-[10px] tabular-nums",
1294
+ children: [
1295
+ /* @__PURE__ */ jsx("span", { children: formatTimelineTick(from, axisSpan) }),
1296
+ /* @__PURE__ */ jsx("span", { children: formatTimelineTick(mid, axisSpan) }),
1297
+ /* @__PURE__ */ jsx("span", { children: formatTimelineTick(to, axisSpan) })
1298
+ ]
1299
+ })]
1300
+ });
1301
+ }
541
1302
  //#endregion
542
1303
  //#region src/components/features/logs/components/toolbar.tsx
543
1304
  function LogsSearch({ placeholder = "Search logs...", className }) {
@@ -563,7 +1324,10 @@ function LogsSearch({ placeholder = "Search logs...", className }) {
563
1324
  }, [value, setSearch]);
564
1325
  return /* @__PURE__ */ jsxs("div", {
565
1326
  className: cn("relative min-w-0 flex-1", className),
566
- children: [/* @__PURE__ */ jsx(Search, { className: "text-muted-foreground pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2" }), /* @__PURE__ */ jsx(Input, {
1327
+ children: [/* @__PURE__ */ jsx(Icon, {
1328
+ icon: Search,
1329
+ className: "text-muted-foreground pointer-events-none absolute top-1/2 left-3 -translate-y-1/2"
1330
+ }), /* @__PURE__ */ jsx(Input, {
567
1331
  value,
568
1332
  onChange: (event) => setValue(event.target.value),
569
1333
  placeholder,
@@ -601,7 +1365,7 @@ function LogsToolbar({ children, className }) {
601
1365
  size: "icon",
602
1366
  "aria-label": "Refresh logs",
603
1367
  onClick: onRefresh,
604
- children: /* @__PURE__ */ jsx(RefreshCw, { className: "size-4" })
1368
+ children: /* @__PURE__ */ jsx(Icon, { icon: RefreshCw })
605
1369
  }),
606
1370
  onExport && /* @__PURE__ */ jsx(Button, {
607
1371
  type: "secondary",
@@ -609,7 +1373,7 @@ function LogsToolbar({ children, className }) {
609
1373
  size: "icon",
610
1374
  "aria-label": "Export logs",
611
1375
  onClick: () => onExport(entries),
612
- children: /* @__PURE__ */ jsx(Download, { className: "size-4" })
1376
+ children: /* @__PURE__ */ jsx(Icon, { icon: Download })
613
1377
  })
614
1378
  ]
615
1379
  });
@@ -644,16 +1408,20 @@ function LogsExplorer({ className }) {
644
1408
  }),
645
1409
  /* @__PURE__ */ jsxs("div", {
646
1410
  className: "flex min-w-0 flex-1 flex-col",
647
- children: [/* @__PURE__ */ jsx(LogsToolbar, { children: !isDesktop && /* @__PURE__ */ jsxs(Button, {
648
- type: "secondary",
649
- theme: "outline",
650
- size: "small",
651
- onClick: () => setFiltersOpen(true),
652
- children: [/* @__PURE__ */ jsx(SlidersHorizontal, { className: "size-4" }), "Filters"]
653
- }) }), /* @__PURE__ */ jsxs("div", {
654
- className: "flex min-h-0 flex-1",
655
- children: [/* @__PURE__ */ jsx(LogsTable, {}), isDesktop && /* @__PURE__ */ jsx(LogsDetail, {})]
656
- })]
1411
+ children: [
1412
+ /* @__PURE__ */ jsx(LogsToolbar, { children: !isDesktop && /* @__PURE__ */ jsxs(Button, {
1413
+ type: "secondary",
1414
+ theme: "outline",
1415
+ size: "small",
1416
+ onClick: () => setFiltersOpen(true),
1417
+ children: [/* @__PURE__ */ jsx(Icon, { icon: SlidersHorizontal }), "Filters"]
1418
+ }) }),
1419
+ /* @__PURE__ */ jsx(LogsTimeline, {}),
1420
+ /* @__PURE__ */ jsxs("div", {
1421
+ className: "relative flex min-h-0 min-w-0 flex-1",
1422
+ children: [/* @__PURE__ */ jsx(LogsTable, {}), isDesktop && /* @__PURE__ */ jsx(LogsDetail, {})]
1423
+ })
1424
+ ]
657
1425
  }),
658
1426
  !isDesktop && /* @__PURE__ */ jsx(LogsDetail, {})
659
1427
  ]
@@ -687,7 +1455,8 @@ const CANONICAL_FACET_NAMES = [
687
1455
  const FACET_LABELS = {
688
1456
  severity: "Severity",
689
1457
  service_name: "Service",
690
- resource_name: "Resource"
1458
+ resource_name: "Resource",
1459
+ host: "Host"
691
1460
  };
692
1461
  const SEVERITY_ORDER = [
693
1462
  "ERROR",
@@ -702,7 +1471,10 @@ const DEFAULT_LOGQL_MATCHER = "{service_name=~\".+\"}";
702
1471
  //#endregion
703
1472
  //#region src/components/features/logs/utils/facets.ts
704
1473
  function facetLabel(name) {
705
- return FACET_LABELS[name] ?? name.replaceAll("_", " ");
1474
+ const mapped = FACET_LABELS[name];
1475
+ if (mapped) return mapped;
1476
+ const raw = name.replaceAll("_", " ");
1477
+ return raw ? raw.charAt(0).toUpperCase() + raw.slice(1) : raw;
706
1478
  }
707
1479
  function compareFacetValues(name, a, b) {
708
1480
  if (name === "severity") {
@@ -754,25 +1526,38 @@ function filterEntries(entries, filters, search = "") {
754
1526
  const label = entry.labels[name];
755
1527
  if (!label || !values.includes(label)) return false;
756
1528
  }
757
- if (query && !entry.line.toLowerCase().includes(query)) return false;
1529
+ if (query && !entryMatchesSearch(entry, query)) return false;
758
1530
  return true;
759
1531
  });
760
1532
  }
761
- //#endregion
762
- //#region src/components/features/logs/utils/time-range.ts
763
- const THIRTY_MINUTES_MS = 18e5;
764
- function lastThirtyMinutes(now = /* @__PURE__ */ new Date()) {
765
- return {
766
- from: (/* @__PURE__ */ new Date(now.getTime() - THIRTY_MINUTES_MS)).toISOString(),
767
- to: now.toISOString()
768
- };
1533
+ /**
1534
+ * Text a search term is matched against: the line, plus the fields the table
1535
+ * actually shows for a label-only access log (method, path, status, host).
1536
+ * Other labels (request ids, user agents, pod names) are deliberately not
1537
+ * searched so a hit is always visible on screen.
1538
+ */
1539
+ function searchableLogText(entry) {
1540
+ const { parsed, path } = logLineDisplay(entry.line, entry.labels);
1541
+ const parts = [entry.line, path ?? ""];
1542
+ if (parsed.kind === "http") parts.push(formatHttpLogLine(parsed));
1543
+ const host = logRequestHost(entry.labels);
1544
+ if (host) parts.push(host);
1545
+ return parts.join("\n").toLowerCase();
769
1546
  }
770
- function filtersAreActive(filters) {
771
- return Object.values(filters).some((values) => values.length > 0);
1547
+ function entryMatchesSearch(entry, query) {
1548
+ return searchableLogText(entry).includes(query);
772
1549
  }
773
1550
  //#endregion
774
1551
  //#region src/components/features/logs/components/root.tsx
775
- function LogsRoot({ entries, facets, timeRange, defaultTimeRange, filters, defaultFilters, search, defaultSearch = "", live, defaultLive = false, selectedId, defaultSelectedId = null, isLoading = false, error, columns = DEFAULT_LOG_COLUMNS, onTimeRangeChange, onFiltersChange, onSearchChange, onLiveChange, onSelectedIdChange, onRefresh, onExport, className, children }) {
1552
+ /** Return the previous array while its items are shallow-equal to the new one. */
1553
+ function useShallowArray(next) {
1554
+ const ref = useRef(next);
1555
+ const prev = ref.current;
1556
+ const same = prev === next || prev.length === next.length && prev.every((item, index) => item === next[index]);
1557
+ if (!same) ref.current = next;
1558
+ return same ? prev : next;
1559
+ }
1560
+ function LogsRoot({ entries, facets, histogram, timeRange, defaultTimeRange, filters, defaultFilters, search, defaultSearch = "", live, defaultLive = false, selectedId, defaultSelectedId = null, isLoading = false, error, columns = DEFAULT_LOG_COLUMNS, onTimeRangeChange, onFiltersChange, onSearchChange, onLiveChange, onSelectedIdChange, onRefresh, onExport, className, children }) {
776
1561
  const [currentTimeRange, setTimeRange] = useControllableState({
777
1562
  value: timeRange,
778
1563
  defaultValue: defaultTimeRange ?? lastThirtyMinutes(),
@@ -799,6 +1584,13 @@ function LogsRoot({ entries, facets, timeRange, defaultTimeRange, filters, defau
799
1584
  onChange: onSelectedIdChange
800
1585
  });
801
1586
  const resolvedFacets = useMemo(() => facets ?? facetsFromEntries(entries), [facets, entries]);
1587
+ const resolvedHistogram = useMemo(() => histogram ?? histogramFromEntries(entries, currentTimeRange), [
1588
+ histogram,
1589
+ entries,
1590
+ currentTimeRange
1591
+ ]);
1592
+ const stableColumns = useShallowArray(columns);
1593
+ const resolvedColumns = useMemo(() => resolveLogColumns(stableColumns), [stableColumns]);
802
1594
  const selectedIndex = useMemo(() => entries.findIndex((entry) => entry.id === currentSelectedId), [entries, currentSelectedId]);
803
1595
  const selectedEntry = selectedIndex >= 0 ? entries[selectedIndex] ?? null : null;
804
1596
  const toggleFilterValue = useCallback((name, value) => {
@@ -833,6 +1625,7 @@ function LogsRoot({ entries, facets, timeRange, defaultTimeRange, filters, defau
833
1625
  const value = useMemo(() => ({
834
1626
  entries,
835
1627
  facets: resolvedFacets,
1628
+ histogram: resolvedHistogram,
836
1629
  timeRange: currentTimeRange,
837
1630
  setTimeRange,
838
1631
  filters: currentFilters,
@@ -851,13 +1644,14 @@ function LogsRoot({ entries, facets, timeRange, defaultTimeRange, filters, defau
851
1644
  selectNext,
852
1645
  isLoading,
853
1646
  error,
854
- columns,
1647
+ columns: resolvedColumns,
855
1648
  hasActiveFilters: filtersAreActive(currentFilters),
856
1649
  onRefresh,
857
1650
  onExport
858
1651
  }), [
859
1652
  entries,
860
1653
  resolvedFacets,
1654
+ resolvedHistogram,
861
1655
  currentTimeRange,
862
1656
  setTimeRange,
863
1657
  currentFilters,
@@ -876,7 +1670,7 @@ function LogsRoot({ entries, facets, timeRange, defaultTimeRange, filters, defau
876
1670
  selectNext,
877
1671
  isLoading,
878
1672
  error,
879
- columns,
1673
+ resolvedColumns,
880
1674
  onRefresh,
881
1675
  onExport
882
1676
  ]);
@@ -900,6 +1694,7 @@ const Logs = {
900
1694
  Toolbar: LogsToolbar,
901
1695
  Search: LogsSearch,
902
1696
  LiveToggle: LogsLiveToggle,
1697
+ Timeline: LogsTimeline,
903
1698
  Table: LogsTable,
904
1699
  Detail: LogsDetail
905
1700
  };
@@ -977,4 +1772,4 @@ function flattenLokiStreams(response) {
977
1772
  return entries;
978
1773
  }
979
1774
  //#endregion
980
- export { Logs, buildLogQL, facetsFromEntries, filterEntries, flattenLokiStreams, httpStatusBadgeType, httpStatusTextClass, lastThirtyMinutes, logLineDisplay, nsToDate, parseLogLine, severityBadgeType, useLogs };
1775
+ export { LOG_TIME_PRESETS, Logs, buildLogQL, facetsFromEntries, filterEntries, flattenLokiStreams, formatHttpLogLine, histogramFromEntries, httpStatusBadgeType, httpStatusTextClass, lastThirtyMinutes, logLineDisplay, logRequestHost, logTimeRangeLabel, nsToDate, parseLogLine, resolveLogColumns, resolveLogTimeRange, searchableLogText, severityBadgeType, splitPathQuery, useLogs };