@datum-cloud/datum-ui 2.5.0 → 2.7.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.
- package/dist/{calendar-date-picker-Cl4lIDl5.mjs → calendar-date-picker-bpTUcGu_.mjs} +1 -1
- package/dist/components/features/logs/components/columns.d.ts +15 -0
- package/dist/components/features/logs/components/root.d.ts +1 -1
- package/dist/components/features/logs/components/status-badge.d.ts +5 -0
- package/dist/components/features/logs/components/timeline.d.ts +3 -0
- package/dist/components/features/logs/fixtures/index.d.ts +2 -0
- package/dist/components/features/logs/index.d.ts +3 -2
- package/dist/components/features/logs/logs.d.ts +2 -0
- package/dist/components/features/logs/types.d.ts +51 -3
- package/dist/components/features/logs/utils/detail-width.d.ts +3 -0
- package/dist/components/features/logs/utils/facets.d.ts +7 -0
- package/dist/components/features/logs/utils/format-timestamp.d.ts +4 -0
- package/dist/components/features/logs/utils/histogram.d.ts +16 -0
- package/dist/components/features/logs/utils/host.d.ts +7 -0
- package/dist/components/features/logs/utils/index.d.ts +8 -4
- package/dist/components/features/logs/utils/parse-log-line.d.ts +17 -3
- package/dist/components/features/logs/utils/time-range.d.ts +19 -0
- package/dist/components/features/logs/utils/user-agent.d.ts +25 -0
- package/dist/data-table/index.mjs +1 -1
- package/dist/{data-table-C37OD6_4.mjs → data-table-BaWOHdZX.mjs} +1 -1
- package/dist/date-picker/index.mjs +3 -3
- package/dist/date-time/index.mjs +1 -1
- package/dist/{date-time-BmYjT5vd.mjs → date-time-lZdefM4H.mjs} +1 -1
- package/dist/date-time-picker/index.mjs +1 -1
- package/dist/{date-time-picker-DlaSizpS.mjs → date-time-picker-Bd7qSZVr.mjs} +1 -1
- package/dist/{date-time-range-picker-CQQLACgN.mjs → date-time-range-picker-DlFTh3DF.mjs} +12 -3
- package/dist/form/index.mjs +1 -1
- package/dist/{form-DDtBJ3Dj.mjs → form-BKxvdIKB.mjs} +3 -3
- package/dist/grouped-table/index.mjs +1 -1
- package/dist/{grouped-table-DImzzehJ.mjs → grouped-table-DWtEx18V.mjs} +1 -1
- package/dist/index.mjs +9 -9
- package/dist/logs/fixtures/index.mjs +770 -3
- package/dist/logs/index.mjs +1321 -252
- package/dist/picker/index.mjs +2 -2
- package/dist/{picker-Bbjl9FOq.mjs → picker-DbKukBkk.mjs} +1 -1
- package/dist/time-picker/index.mjs +1 -1
- package/dist/{time-picker-CoUqZCb7.mjs → time-picker-rVTkmbIG.mjs} +1 -1
- package/dist/{to-api-format-BzKfX7yo.mjs → to-api-format-Bm1VcKkS.mjs} +1 -1
- package/package.json +1 -1
package/dist/logs/index.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { t as cn } from "../utils-Bu32wN-x.mjs";
|
|
2
2
|
import { t as Badge } from "../badge-BTNW_FEU.mjs";
|
|
3
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";
|
|
@@ -8,14 +9,314 @@ 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-
|
|
12
|
-
import { t as DateTime } from "../date-time-
|
|
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 { Bot, ChevronDown, ChevronUp, Clock, Copy, Download, HelpCircle, Inbox, Monitor, RefreshCw, Search, SlidersHorizontal, Smartphone, Tablet, 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
|
+
import { format, isSameDay } from "date-fns";
|
|
20
|
+
//#region src/components/features/logs/utils/format-timestamp.ts
|
|
21
|
+
function formatLogTimestampParts(date) {
|
|
22
|
+
return {
|
|
23
|
+
date: format(date, "MMM dd").toUpperCase(),
|
|
24
|
+
time: format(date, "HH:mm:ss.SS")
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function formatLogTimestamp(date) {
|
|
28
|
+
const { date: day, time } = formatLogTimestampParts(date);
|
|
29
|
+
return `${day} ${time}`;
|
|
30
|
+
}
|
|
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;
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region src/components/features/logs/utils/severity.ts
|
|
44
|
+
const SEVERITY_BADGE = {
|
|
45
|
+
ERROR: "danger",
|
|
46
|
+
FATAL: "danger",
|
|
47
|
+
CRITICAL: "danger",
|
|
48
|
+
WARN: "warning",
|
|
49
|
+
WARNING: "warning",
|
|
50
|
+
INFO: "info",
|
|
51
|
+
DEBUG: "muted",
|
|
52
|
+
TRACE: "muted"
|
|
53
|
+
};
|
|
54
|
+
function severityBadgeType(severity) {
|
|
55
|
+
if (!severity) return "muted";
|
|
56
|
+
return SEVERITY_BADGE[severity.toUpperCase()] ?? "muted";
|
|
57
|
+
}
|
|
58
|
+
function httpStatusBadgeType(status) {
|
|
59
|
+
if (status >= 500) return "danger";
|
|
60
|
+
if (status >= 400) return "warning";
|
|
61
|
+
if (status >= 300) return "info";
|
|
62
|
+
if (status >= 200) return "success";
|
|
63
|
+
return "muted";
|
|
64
|
+
}
|
|
65
|
+
function httpStatusTextClass(status) {
|
|
66
|
+
switch (httpStatusBadgeType(status)) {
|
|
67
|
+
case "danger": return "text-[var(--color-badge-danger)]";
|
|
68
|
+
case "warning": return "text-[var(--color-badge-warning)]";
|
|
69
|
+
case "info": return "text-[var(--color-badge-info)]";
|
|
70
|
+
case "success": return "text-[var(--color-badge-success)]";
|
|
71
|
+
default: return "text-muted-foreground";
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
//#region src/components/features/logs/components/status-badge.tsx
|
|
76
|
+
function LogsSeverityBadge({ severity }) {
|
|
77
|
+
const unlabeled = !severity;
|
|
78
|
+
return /* @__PURE__ */ jsx(Badge, {
|
|
79
|
+
type: severityBadgeType(severity),
|
|
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"),
|
|
82
|
+
children: severity ?? "LOG"
|
|
83
|
+
});
|
|
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
|
+
}
|
|
95
|
+
function LogsStatusBadge({ parsed }) {
|
|
96
|
+
if (parsed.kind !== "http") return /* @__PURE__ */ jsx("span", {
|
|
97
|
+
className: "text-muted-foreground",
|
|
98
|
+
children: "—"
|
|
99
|
+
});
|
|
100
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
101
|
+
className: "inline-flex h-4 items-center gap-1.5 font-mono text-[11px] leading-4 font-medium",
|
|
102
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
103
|
+
className: cn("tabular-nums", httpStatusTextClass(parsed.status)),
|
|
104
|
+
children: parsed.status
|
|
105
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
106
|
+
className: "text-muted-foreground",
|
|
107
|
+
children: parsed.method
|
|
108
|
+
})]
|
|
109
|
+
});
|
|
110
|
+
}
|
|
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
|
|
19
320
|
//#region src/components/features/logs/hooks/use-logs.ts
|
|
20
321
|
const LogsContext = createContext(null);
|
|
21
322
|
function useLogs() {
|
|
@@ -23,24 +324,39 @@ function useLogs() {
|
|
|
23
324
|
if (!context) throw new Error("Logs components must be used within <Logs.Root>");
|
|
24
325
|
return context;
|
|
25
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
|
+
}
|
|
26
335
|
//#endregion
|
|
27
336
|
//#region src/components/features/logs/utils/parse-log-line.ts
|
|
28
337
|
const ACCESS_LOG_RE = /^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s+(\S+)\s+(\d{3})\s+(\d+)ms\b/i;
|
|
29
338
|
const PATH_TOKEN_RE = /(?:^|\s)(\/\S+)/;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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) ?? {
|
|
33
345
|
kind: "text",
|
|
34
346
|
line
|
|
35
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;
|
|
36
355
|
const method = match[1];
|
|
37
356
|
const path = match[2];
|
|
38
357
|
const status = match[3];
|
|
39
358
|
const duration = match[4];
|
|
40
|
-
if (!method || !path || !status || !duration) return
|
|
41
|
-
kind: "text",
|
|
42
|
-
line
|
|
43
|
-
};
|
|
359
|
+
if (!method || !path || !status || !duration) return null;
|
|
44
360
|
return {
|
|
45
361
|
kind: "http",
|
|
46
362
|
method: method.toUpperCase(),
|
|
@@ -49,8 +365,47 @@ function parseLogLine(line) {
|
|
|
49
365
|
durationMs: Number(duration)
|
|
50
366
|
};
|
|
51
367
|
}
|
|
52
|
-
function
|
|
53
|
-
const
|
|
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);
|
|
54
409
|
if (parsed.kind === "http") {
|
|
55
410
|
const match = ACCESS_LOG_RE.exec(line);
|
|
56
411
|
const rest = match ? line.slice(match[0].length).trim() : "";
|
|
@@ -60,7 +415,7 @@ function logLineDisplay(line) {
|
|
|
60
415
|
message: extraMessage(rest)
|
|
61
416
|
};
|
|
62
417
|
}
|
|
63
|
-
const path = PATH_TOKEN_RE.exec(line)?.[1]
|
|
418
|
+
const path = labels.path?.trim() || PATH_TOKEN_RE.exec(line)?.[1] || null;
|
|
64
419
|
if (!path) return {
|
|
65
420
|
parsed,
|
|
66
421
|
path: null,
|
|
@@ -69,7 +424,7 @@ function logLineDisplay(line) {
|
|
|
69
424
|
return {
|
|
70
425
|
parsed,
|
|
71
426
|
path,
|
|
72
|
-
message: extraMessage(stripOnce(line, path))
|
|
427
|
+
message: extraMessage(line ? stripOnce(line, path) : "")
|
|
73
428
|
};
|
|
74
429
|
}
|
|
75
430
|
/** Drop leftover that is only fields already shown (method, status, path, duration, label=value). */
|
|
@@ -83,65 +438,238 @@ function stripOnce(line, token) {
|
|
|
83
438
|
return `${line.slice(0, index)}${line.slice(index + token.length)}`.replace(/\s+/g, " ").trim();
|
|
84
439
|
}
|
|
85
440
|
//#endregion
|
|
86
|
-
//#region src/components/features/logs/utils/
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
441
|
+
//#region src/components/features/logs/utils/user-agent.ts
|
|
442
|
+
/** Label keys, in order, that may carry the client's User-Agent header. */
|
|
443
|
+
const USER_AGENT_LABEL_KEYS = [
|
|
444
|
+
"user_agent",
|
|
445
|
+
"http_user_agent",
|
|
446
|
+
"useragent",
|
|
447
|
+
"ua",
|
|
448
|
+
"http.user_agent"
|
|
449
|
+
];
|
|
450
|
+
/** The User-Agent header from an entry's labels, if any. */
|
|
451
|
+
function logUserAgent(labels) {
|
|
452
|
+
for (const key of USER_AGENT_LABEL_KEYS) {
|
|
453
|
+
const value = labels[key]?.trim();
|
|
454
|
+
if (value) return value;
|
|
455
|
+
}
|
|
100
456
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
457
|
+
const BOT_RE = /bot|crawl|spider|slurp|facebookexternalhit|preview|monitor|uptime|pingdom|kube-probe|prometheus|blackbox|healthcheck|health-check|datadog|newrelic|statuscake/i;
|
|
458
|
+
/** Order matters: forks embed the names of what they fork. */
|
|
459
|
+
const CLIENT_RULES = [
|
|
460
|
+
{
|
|
461
|
+
name: "curl",
|
|
462
|
+
re: /\bcurl\/(\d+(?:\.\d+)*)/i,
|
|
463
|
+
tool: true
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
name: "Wget",
|
|
467
|
+
re: /\bWget\/(\d+(?:\.\d+)*)/i,
|
|
468
|
+
tool: true
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
name: "Python Requests",
|
|
472
|
+
re: /\bpython-requests\/(\d+(?:\.\d+)*)/i,
|
|
473
|
+
tool: true
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
name: "Python urllib",
|
|
477
|
+
re: /\bPython-urllib\/(\d+(?:\.\d+)*)/i,
|
|
478
|
+
tool: true
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
name: "Go HTTP client",
|
|
482
|
+
re: /\bGo-http-client\/(\d+(?:\.\d+)*)/i,
|
|
483
|
+
tool: true
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
name: "okhttp",
|
|
487
|
+
re: /\bokhttp\/(\d+(?:\.\d+)*)/i,
|
|
488
|
+
tool: true
|
|
489
|
+
},
|
|
490
|
+
{
|
|
491
|
+
name: "axios",
|
|
492
|
+
re: /\baxios\/(\d+(?:\.\d+)*)/i,
|
|
493
|
+
tool: true
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
name: "node-fetch",
|
|
497
|
+
re: /\bnode-fetch\/(\d+(?:\.\d+)*)/i,
|
|
498
|
+
tool: true
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
name: "Node.js",
|
|
502
|
+
re: /\bnode\/v?(\d+(?:\.\d+)*)/i,
|
|
503
|
+
tool: true
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
name: "Postman",
|
|
507
|
+
re: /\bPostmanRuntime\/(\d+(?:\.\d+)*)/i,
|
|
508
|
+
tool: true
|
|
509
|
+
},
|
|
510
|
+
{
|
|
511
|
+
name: "Insomnia",
|
|
512
|
+
re: /\binsomnia\/(\d+(?:\.\d+)*)/i,
|
|
513
|
+
tool: true
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
name: "Java",
|
|
517
|
+
re: /\bJava\/(\d+(?:\.\d+)*)/i,
|
|
518
|
+
tool: true
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
name: "Edge",
|
|
522
|
+
re: /\bEdgA?\/(\d+(?:\.\d+)*)/
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
name: "Edge",
|
|
526
|
+
re: /\bEdgiOS\/(\d+(?:\.\d+)*)/
|
|
527
|
+
},
|
|
528
|
+
{
|
|
529
|
+
name: "Opera",
|
|
530
|
+
re: /\bOPR\/(\d+(?:\.\d+)*)/
|
|
531
|
+
},
|
|
532
|
+
{
|
|
533
|
+
name: "Opera",
|
|
534
|
+
re: /\bOpera\/(\d+(?:\.\d+)*)/
|
|
535
|
+
},
|
|
536
|
+
{
|
|
537
|
+
name: "Samsung Internet",
|
|
538
|
+
re: /\bSamsungBrowser\/(\d+(?:\.\d+)*)/
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
name: "Brave",
|
|
542
|
+
re: /\bBrave\/(\d+(?:\.\d+)*)/
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
name: "Vivaldi",
|
|
546
|
+
re: /\bVivaldi\/(\d+(?:\.\d+)*)/
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
name: "Firefox",
|
|
550
|
+
re: /\b(?:Firefox|FxiOS)\/(\d+(?:\.\d+)*)/
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
name: "Chrome",
|
|
554
|
+
re: /\b(?:Chrome|CriOS|HeadlessChrome)\/(\d+(?:\.\d+)*)/
|
|
555
|
+
}
|
|
556
|
+
];
|
|
557
|
+
/** Safari carries its version under `Version/`; WebKit views on iOS have neither. */
|
|
558
|
+
function detectSafari(ua) {
|
|
559
|
+
if (!/\bSafari\//.test(ua) && !/\bAppleWebKit\//.test(ua)) return void 0;
|
|
560
|
+
const version = /\bVersion\/(\d+(?:\.\d+)*)/.exec(ua);
|
|
561
|
+
if (version && /\bSafari\//.test(ua)) return { version: majorVersion(version[1]) };
|
|
562
|
+
if (/\b(?:iPhone|iPad|iPod)\b/.test(ua)) return {};
|
|
107
563
|
}
|
|
108
|
-
function
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
564
|
+
function majorVersion(version) {
|
|
565
|
+
if (!version) return void 0;
|
|
566
|
+
return version.split(".")[0];
|
|
567
|
+
}
|
|
568
|
+
function detectOs(ua) {
|
|
569
|
+
let match;
|
|
570
|
+
if (/\b(?:iPhone|iPad|iPod)\b/i.test(ua)) {
|
|
571
|
+
match = /\bOS (\d+)[._](\d+)(?:[._](\d+))?/.exec(ua);
|
|
572
|
+
if (!match) return { os: "iOS" };
|
|
573
|
+
const patch = match[3] && match[3] !== "0" ? `.${match[3]}` : "";
|
|
574
|
+
return {
|
|
575
|
+
os: "iOS",
|
|
576
|
+
osVersion: `${match[1]}.${match[2]}${patch}`
|
|
577
|
+
};
|
|
115
578
|
}
|
|
579
|
+
match = /\bAndroid[ /](\d+(?:\.\d+)?)/i.exec(ua);
|
|
580
|
+
if (match) return {
|
|
581
|
+
os: "Android",
|
|
582
|
+
osVersion: match[1]
|
|
583
|
+
};
|
|
584
|
+
if (/\bAndroid\b/i.test(ua)) return { os: "Android" };
|
|
585
|
+
if (/\bWindows Phone\b/i.test(ua)) return { os: "Windows Phone" };
|
|
586
|
+
if (/\bWindows\b/i.test(ua)) return { os: "Windows" };
|
|
587
|
+
if (/\bCrOS\b/.test(ua)) return { os: "ChromeOS" };
|
|
588
|
+
if (/\bMac OS X\b|\bMacintosh\b/i.test(ua)) return { os: "macOS" };
|
|
589
|
+
if (/\bLinux\b|\bX11\b/i.test(ua)) return { os: "Linux" };
|
|
590
|
+
if (/\bFreeBSD\b/i.test(ua)) return { os: "FreeBSD" };
|
|
591
|
+
return {};
|
|
116
592
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
});
|
|
593
|
+
function detectDevice(ua, os, bot, tool) {
|
|
594
|
+
if (bot || tool) return "bot";
|
|
595
|
+
if (/\biPad\b|\bTablet\b|\bKindle\b|\bSilk\b/i.test(ua)) return "tablet";
|
|
596
|
+
if (os === "Android" && !/\bMobile\b/i.test(ua)) return "tablet";
|
|
597
|
+
if (/\bMobile\b|\biPhone\b|\biPod\b|\bWindows Phone\b/i.test(ua)) return "mobile";
|
|
598
|
+
if (os === "iOS") return "mobile";
|
|
599
|
+
if (os === "Windows" || os === "macOS" || os === "Linux" || os === "ChromeOS" || os === "FreeBSD") return "desktop";
|
|
600
|
+
return "unknown";
|
|
126
601
|
}
|
|
127
|
-
function
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
602
|
+
function botClient(ua) {
|
|
603
|
+
const match = /([\w.-]*(?:bot|crawl|spider|probe|monitor|uptime|check)[\w.-]*)(?:\/(\d+(?:\.\d+)*))?/i.exec(ua);
|
|
604
|
+
if (!match?.[1]) return void 0;
|
|
605
|
+
return {
|
|
606
|
+
name: match[1],
|
|
607
|
+
version: majorVersion(match[2])
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Parse a User-Agent header into browser, OS and device type. Deliberately
|
|
612
|
+
* small: it recognises the mainstream browsers, mobile platforms, common
|
|
613
|
+
* command-line / SDK clients, and anything that calls itself a bot. Returns
|
|
614
|
+
* `null` for an empty string; unrecognised strings still return `summary`
|
|
615
|
+
* as the raw value with `device: 'unknown'`.
|
|
616
|
+
*/
|
|
617
|
+
function parseUserAgent(raw) {
|
|
618
|
+
const ua = raw.trim();
|
|
619
|
+
if (!ua) return null;
|
|
620
|
+
let browser;
|
|
621
|
+
let browserVersion;
|
|
622
|
+
let tool = false;
|
|
623
|
+
for (const rule of CLIENT_RULES) {
|
|
624
|
+
const match = rule.re.exec(ua);
|
|
625
|
+
if (match) {
|
|
626
|
+
browser = rule.name;
|
|
627
|
+
browserVersion = majorVersion(match[1]);
|
|
628
|
+
tool = Boolean(rule.tool);
|
|
629
|
+
break;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
if (!browser) {
|
|
633
|
+
const safari = detectSafari(ua);
|
|
634
|
+
if (safari) {
|
|
635
|
+
browser = "Safari";
|
|
636
|
+
browserVersion = safari.version;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
const { os, osVersion } = tool ? {} : detectOs(ua);
|
|
640
|
+
const bot = !tool && BOT_RE.test(ua);
|
|
641
|
+
const device = detectDevice(ua, os, bot, tool);
|
|
642
|
+
if (bot && !browser) {
|
|
643
|
+
const named = botClient(ua);
|
|
644
|
+
browser = named?.name;
|
|
645
|
+
browserVersion = named?.version;
|
|
646
|
+
}
|
|
647
|
+
const client = browser ? browserVersion ? `${browser} ${browserVersion}` : browser : void 0;
|
|
648
|
+
const platform = os ? osVersion ? `${os} ${osVersion}` : os : void 0;
|
|
649
|
+
let summary;
|
|
650
|
+
if (client && platform) summary = `${client} on ${platform}`;
|
|
651
|
+
else if (client) summary = client;
|
|
652
|
+
else if (platform) summary = platform;
|
|
653
|
+
else summary = ua;
|
|
654
|
+
return {
|
|
655
|
+
browser,
|
|
656
|
+
browserVersion,
|
|
657
|
+
os,
|
|
658
|
+
osVersion,
|
|
659
|
+
device,
|
|
660
|
+
summary,
|
|
661
|
+
raw: ua
|
|
662
|
+
};
|
|
142
663
|
}
|
|
143
664
|
//#endregion
|
|
144
665
|
//#region src/components/features/logs/components/detail.tsx
|
|
666
|
+
const DEVICE_ICONS = {
|
|
667
|
+
desktop: Monitor,
|
|
668
|
+
mobile: Smartphone,
|
|
669
|
+
tablet: Tablet,
|
|
670
|
+
bot: Bot,
|
|
671
|
+
unknown: HelpCircle
|
|
672
|
+
};
|
|
145
673
|
function MetaCell({ label, children, className }) {
|
|
146
674
|
return /* @__PURE__ */ jsxs("div", {
|
|
147
675
|
className: cn("bg-background min-w-0 px-3 py-2.5", className),
|
|
@@ -154,77 +682,111 @@ function MetaCell({ label, children, className }) {
|
|
|
154
682
|
})]
|
|
155
683
|
});
|
|
156
684
|
}
|
|
157
|
-
|
|
685
|
+
/** Stable React keys for search params, which may repeat `key=value` verbatim. */
|
|
686
|
+
function keyedParams(params) {
|
|
687
|
+
const seen = /* @__PURE__ */ new Map();
|
|
688
|
+
return params.map(([key, value]) => {
|
|
689
|
+
const pair = `${key}=${value}`;
|
|
690
|
+
const count = seen.get(pair) ?? 0;
|
|
691
|
+
seen.set(pair, count + 1);
|
|
692
|
+
return {
|
|
693
|
+
id: count === 0 ? pair : `${pair}#${count}`,
|
|
694
|
+
key,
|
|
695
|
+
value
|
|
696
|
+
};
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
const DetailBody = memo(({ showClose = true }) => {
|
|
158
700
|
const { selectedEntry, selectPrevious, selectNext, setSelectedId } = useLogs();
|
|
159
701
|
const [, copy] = useCopyToClipboard();
|
|
160
702
|
if (!selectedEntry) return null;
|
|
161
|
-
const parsed = parseLogLine(selectedEntry.line);
|
|
162
|
-
const
|
|
163
|
-
const
|
|
703
|
+
const parsed = parseLogLine(selectedEntry.line, selectedEntry.labels);
|
|
704
|
+
const displayLine = parsed.kind === "http" && !selectedEntry.line.trim() ? formatHttpLogLine(parsed) : selectedEntry.line;
|
|
705
|
+
const request = splitPathQuery(parsed.kind === "http" ? parsed.path : "");
|
|
706
|
+
const title = parsed.kind === "http" ? `${parsed.method} ${request.pathname}` : selectedEntry.labels.service_name ?? "Log";
|
|
707
|
+
const service = selectedEntry.labels.service_name ?? logRequestHost(selectedEntry.labels);
|
|
708
|
+
const userAgent = logUserAgent(selectedEntry.labels);
|
|
709
|
+
const client = userAgent ? parseUserAgent(userAgent) : null;
|
|
164
710
|
return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("header", {
|
|
165
|
-
className: "
|
|
711
|
+
className: "border-b px-4 py-3",
|
|
166
712
|
children: [/* @__PURE__ */ jsxs("div", {
|
|
167
|
-
className: "
|
|
713
|
+
className: "flex items-center gap-2",
|
|
168
714
|
children: [
|
|
169
|
-
/* @__PURE__ */ jsx("div", {
|
|
170
|
-
className: "mb-1.5",
|
|
171
|
-
children: /* @__PURE__ */ jsx(LogsSeverityBadge, { severity: selectedEntry.labels.severity })
|
|
172
|
-
}),
|
|
173
715
|
/* @__PURE__ */ jsx("h3", {
|
|
174
|
-
className: "truncate font-mono text-sm font-medium",
|
|
716
|
+
className: "min-w-0 flex-1 truncate font-mono text-sm font-medium",
|
|
717
|
+
title,
|
|
175
718
|
children: title
|
|
176
719
|
}),
|
|
720
|
+
parsed.kind === "http" ? /* @__PURE__ */ jsx(LogsHttpStatusChip, { status: parsed.status }) : /* @__PURE__ */ jsx(LogsSeverityBadge, { severity: selectedEntry.labels.severity }),
|
|
177
721
|
/* @__PURE__ */ jsxs("div", {
|
|
178
|
-
className: "
|
|
179
|
-
children: [
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
722
|
+
className: "-my-1 flex shrink-0 items-center gap-1",
|
|
723
|
+
children: [
|
|
724
|
+
/* @__PURE__ */ jsx(Button, {
|
|
725
|
+
type: "secondary",
|
|
726
|
+
theme: "borderless",
|
|
727
|
+
size: "icon",
|
|
728
|
+
"aria-label": "Previous log",
|
|
729
|
+
onClick: selectPrevious,
|
|
730
|
+
children: /* @__PURE__ */ jsx(Icon, { icon: ChevronUp })
|
|
731
|
+
}),
|
|
732
|
+
/* @__PURE__ */ jsx(Button, {
|
|
733
|
+
type: "secondary",
|
|
734
|
+
theme: "borderless",
|
|
735
|
+
size: "icon",
|
|
736
|
+
"aria-label": "Next log",
|
|
737
|
+
onClick: selectNext,
|
|
738
|
+
children: /* @__PURE__ */ jsx(Icon, { icon: ChevronDown })
|
|
739
|
+
}),
|
|
740
|
+
/* @__PURE__ */ jsx(Button, {
|
|
741
|
+
type: "secondary",
|
|
742
|
+
theme: "borderless",
|
|
743
|
+
size: "icon",
|
|
744
|
+
"aria-label": "Copy log line",
|
|
745
|
+
onClick: () => copy(displayLine, { withToast: true }),
|
|
746
|
+
children: /* @__PURE__ */ jsx(Icon, { icon: Copy })
|
|
747
|
+
}),
|
|
748
|
+
showClose && /* @__PURE__ */ jsx(Button, {
|
|
749
|
+
type: "secondary",
|
|
750
|
+
theme: "borderless",
|
|
751
|
+
size: "icon",
|
|
752
|
+
"aria-label": "Close details",
|
|
753
|
+
onClick: () => setSelectedId(null),
|
|
754
|
+
children: /* @__PURE__ */ jsx(Icon, { icon: X })
|
|
755
|
+
})
|
|
756
|
+
]
|
|
192
757
|
})
|
|
193
758
|
]
|
|
194
759
|
}), /* @__PURE__ */ jsxs("div", {
|
|
195
|
-
className: "flex
|
|
760
|
+
className: "text-muted-foreground mt-1 flex min-w-0 flex-nowrap items-center gap-1.5 text-xs",
|
|
196
761
|
children: [
|
|
197
|
-
/* @__PURE__ */ jsx(
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
/* @__PURE__ */ jsx(
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
"aria-label": "Close details",
|
|
226
|
-
onClick: () => setSelectedId(null),
|
|
227
|
-
children: /* @__PURE__ */ jsx(X, { className: "size-4" })
|
|
762
|
+
parsed.kind === "http" && selectedEntry.labels.severity && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(LogsSeverityBadge, { severity: selectedEntry.labels.severity }), /* @__PURE__ */ jsx("span", {
|
|
763
|
+
className: "shrink-0",
|
|
764
|
+
"aria-hidden": "true",
|
|
765
|
+
children: "·"
|
|
766
|
+
})] }),
|
|
767
|
+
service && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
|
|
768
|
+
className: "min-w-0 truncate font-mono",
|
|
769
|
+
children: service
|
|
770
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
771
|
+
className: "shrink-0",
|
|
772
|
+
"aria-hidden": "true",
|
|
773
|
+
children: "·"
|
|
774
|
+
})] }),
|
|
775
|
+
/* @__PURE__ */ jsx(DateTime, {
|
|
776
|
+
date: selectedEntry.timestamp,
|
|
777
|
+
timestamp: selectedEntry.timestampNs,
|
|
778
|
+
variant: "absolute",
|
|
779
|
+
tooltip: "detailed",
|
|
780
|
+
children: /* @__PURE__ */ jsxs("time", {
|
|
781
|
+
dateTime: selectedEntry.timestamp.toISOString(),
|
|
782
|
+
"data-slot": "logs-detail-time",
|
|
783
|
+
className: "shrink-0 cursor-default font-mono whitespace-nowrap tabular-nums",
|
|
784
|
+
children: [
|
|
785
|
+
formatLogTimestamp(selectedEntry.timestamp),
|
|
786
|
+
" ",
|
|
787
|
+
getTimezoneAbbreviation(selectedEntry.timestamp, getBrowserTimezone())
|
|
788
|
+
]
|
|
789
|
+
})
|
|
228
790
|
})
|
|
229
791
|
]
|
|
230
792
|
})]
|
|
@@ -236,7 +798,7 @@ function DetailBody({ showClose = true }) {
|
|
|
236
798
|
children: "Message"
|
|
237
799
|
}), /* @__PURE__ */ jsx("pre", {
|
|
238
800
|
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:
|
|
801
|
+
children: displayLine
|
|
240
802
|
})] }),
|
|
241
803
|
parsed.kind === "http" && /* @__PURE__ */ jsxs("section", { children: [/* @__PURE__ */ jsx("h4", {
|
|
242
804
|
className: "text-muted-foreground mb-2 text-xs font-medium",
|
|
@@ -262,7 +824,89 @@ function DetailBody({ showClose = true }) {
|
|
|
262
824
|
/* @__PURE__ */ jsx(MetaCell, {
|
|
263
825
|
label: "Path",
|
|
264
826
|
className: "col-span-3",
|
|
265
|
-
children:
|
|
827
|
+
children: request.pathname
|
|
828
|
+
}),
|
|
829
|
+
client && /* @__PURE__ */ jsxs("div", {
|
|
830
|
+
className: "bg-background col-span-3 min-w-0 px-3 py-2.5",
|
|
831
|
+
"data-slot": "logs-client",
|
|
832
|
+
children: [
|
|
833
|
+
/* @__PURE__ */ jsxs("div", {
|
|
834
|
+
className: "mb-1 flex items-center justify-between",
|
|
835
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
836
|
+
className: "text-muted-foreground text-[11px] font-medium",
|
|
837
|
+
children: "Client"
|
|
838
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
839
|
+
type: "secondary",
|
|
840
|
+
theme: "borderless",
|
|
841
|
+
size: "icon",
|
|
842
|
+
className: "-my-1 size-6",
|
|
843
|
+
"aria-label": "Copy user agent",
|
|
844
|
+
onClick: () => copy(client.raw, { withToast: true }),
|
|
845
|
+
children: /* @__PURE__ */ jsx(Icon, {
|
|
846
|
+
icon: Copy,
|
|
847
|
+
size: 14
|
|
848
|
+
})
|
|
849
|
+
})]
|
|
850
|
+
}),
|
|
851
|
+
/* @__PURE__ */ jsxs("div", {
|
|
852
|
+
className: "flex items-center gap-2 text-xs",
|
|
853
|
+
children: [
|
|
854
|
+
/* @__PURE__ */ jsx(Icon, {
|
|
855
|
+
icon: DEVICE_ICONS[client.device],
|
|
856
|
+
className: "text-muted-foreground shrink-0"
|
|
857
|
+
}),
|
|
858
|
+
/* @__PURE__ */ jsx("span", {
|
|
859
|
+
className: "min-w-0 truncate font-medium",
|
|
860
|
+
"data-slot": "logs-client-summary",
|
|
861
|
+
children: client.summary
|
|
862
|
+
}),
|
|
863
|
+
/* @__PURE__ */ jsx("span", {
|
|
864
|
+
className: "text-muted-foreground shrink-0 text-[11px] capitalize",
|
|
865
|
+
children: client.device === "unknown" ? "" : client.device
|
|
866
|
+
})
|
|
867
|
+
]
|
|
868
|
+
}),
|
|
869
|
+
client.summary !== client.raw && /* @__PURE__ */ jsx("p", {
|
|
870
|
+
className: "text-muted-foreground mt-1 font-mono text-[11px] leading-4 break-all",
|
|
871
|
+
title: client.raw,
|
|
872
|
+
children: client.raw
|
|
873
|
+
})
|
|
874
|
+
]
|
|
875
|
+
}),
|
|
876
|
+
request.params.length > 0 && /* @__PURE__ */ jsxs("div", {
|
|
877
|
+
className: "bg-background col-span-3 min-w-0 px-3 py-2.5",
|
|
878
|
+
"data-slot": "logs-search-params",
|
|
879
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
880
|
+
className: "mb-1.5 flex items-center justify-between",
|
|
881
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
882
|
+
className: "text-muted-foreground text-[11px] font-medium",
|
|
883
|
+
children: "Search params"
|
|
884
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
885
|
+
type: "secondary",
|
|
886
|
+
theme: "borderless",
|
|
887
|
+
size: "icon",
|
|
888
|
+
className: "-my-1 size-6",
|
|
889
|
+
"aria-label": "Copy search params",
|
|
890
|
+
onClick: () => copy(`?${request.search}`, { withToast: true }),
|
|
891
|
+
children: /* @__PURE__ */ jsx(Icon, {
|
|
892
|
+
icon: Copy,
|
|
893
|
+
size: 14
|
|
894
|
+
})
|
|
895
|
+
})]
|
|
896
|
+
}), /* @__PURE__ */ jsx("dl", {
|
|
897
|
+
className: "flex flex-wrap gap-1.5",
|
|
898
|
+
children: keyedParams(request.params).map(({ id, key, value }) => /* @__PURE__ */ jsxs("div", {
|
|
899
|
+
className: "bg-muted/60 inline-flex max-w-full items-stretch overflow-hidden rounded-md border font-mono text-xs",
|
|
900
|
+
children: [/* @__PURE__ */ jsx("dt", {
|
|
901
|
+
className: "text-muted-foreground border-r px-2 py-1",
|
|
902
|
+
children: key
|
|
903
|
+
}), /* @__PURE__ */ jsx("dd", {
|
|
904
|
+
className: "min-w-0 truncate px-2 py-1",
|
|
905
|
+
title: value,
|
|
906
|
+
children: value || "—"
|
|
907
|
+
})]
|
|
908
|
+
}, id))
|
|
909
|
+
})]
|
|
266
910
|
})
|
|
267
911
|
]
|
|
268
912
|
})] }),
|
|
@@ -284,6 +928,59 @@ function DetailBody({ showClose = true }) {
|
|
|
284
928
|
})] })
|
|
285
929
|
]
|
|
286
930
|
})] });
|
|
931
|
+
});
|
|
932
|
+
function LogsDetailResizeHandle({ width, onWidthChange, panelRef }) {
|
|
933
|
+
const dragRef = useRef(null);
|
|
934
|
+
const [maxWidth, setMaxWidth] = useState(() => logsDetailMaxWidth(null));
|
|
935
|
+
const onPointerDown = useCallback((event) => {
|
|
936
|
+
if (event.button !== 0) return;
|
|
937
|
+
event.preventDefault();
|
|
938
|
+
const max = logsDetailMaxWidth(panelRef.current);
|
|
939
|
+
setMaxWidth(max);
|
|
940
|
+
dragRef.current = {
|
|
941
|
+
startX: event.clientX,
|
|
942
|
+
startWidth: width,
|
|
943
|
+
maxWidth: max
|
|
944
|
+
};
|
|
945
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
946
|
+
}, [panelRef, width]);
|
|
947
|
+
const onPointerMove = useCallback((event) => {
|
|
948
|
+
const drag = dragRef.current;
|
|
949
|
+
if (!drag) return;
|
|
950
|
+
onWidthChange(clampLogsDetailWidth(drag.startWidth + (drag.startX - event.clientX), drag.maxWidth));
|
|
951
|
+
}, [onWidthChange]);
|
|
952
|
+
const onPointerUp = useCallback((event) => {
|
|
953
|
+
dragRef.current = null;
|
|
954
|
+
if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId);
|
|
955
|
+
}, []);
|
|
956
|
+
return /* @__PURE__ */ jsx("div", {
|
|
957
|
+
role: "separator",
|
|
958
|
+
"aria-orientation": "vertical",
|
|
959
|
+
"aria-label": "Resize details",
|
|
960
|
+
"aria-valuemin": 400,
|
|
961
|
+
"aria-valuemax": maxWidth,
|
|
962
|
+
"aria-valuenow": width,
|
|
963
|
+
"data-slot": "logs-detail-resize",
|
|
964
|
+
tabIndex: 0,
|
|
965
|
+
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"),
|
|
966
|
+
onPointerDown,
|
|
967
|
+
onPointerMove,
|
|
968
|
+
onPointerUp,
|
|
969
|
+
onPointerCancel: onPointerUp,
|
|
970
|
+
onKeyDown: (event) => {
|
|
971
|
+
const step = event.shiftKey ? 40 : 16;
|
|
972
|
+
if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
|
|
973
|
+
event.preventDefault();
|
|
974
|
+
const max = logsDetailMaxWidth(panelRef.current);
|
|
975
|
+
setMaxWidth(max);
|
|
976
|
+
onWidthChange(clampLogsDetailWidth(width + (event.key === "ArrowLeft" ? step : -step), max));
|
|
977
|
+
}
|
|
978
|
+
if (event.key === "Home") {
|
|
979
|
+
event.preventDefault();
|
|
980
|
+
onWidthChange(400);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
});
|
|
287
984
|
}
|
|
288
985
|
function LogsDetail({ className }) {
|
|
289
986
|
const { selectedEntry, setSelectedId } = useLogs();
|
|
@@ -291,6 +988,8 @@ function LogsDetail({ className }) {
|
|
|
291
988
|
const reduceMotion = useReducedMotion();
|
|
292
989
|
const isDesktop = breakpoint === "desktop";
|
|
293
990
|
const open = selectedEntry !== null;
|
|
991
|
+
const panelRef = useRef(null);
|
|
992
|
+
const [width, setWidth] = useState(400);
|
|
294
993
|
if (!isDesktop) return /* @__PURE__ */ jsx(Sheet, {
|
|
295
994
|
open,
|
|
296
995
|
onOpenChange: (next) => {
|
|
@@ -317,23 +1016,101 @@ function LogsDetail({ className }) {
|
|
|
317
1016
|
]
|
|
318
1017
|
})
|
|
319
1018
|
});
|
|
320
|
-
return /* @__PURE__ */ jsx(AnimatePresence, { children: open && /* @__PURE__ */
|
|
1019
|
+
return /* @__PURE__ */ jsx(AnimatePresence, { children: open && /* @__PURE__ */ jsxs(motion.aside, {
|
|
1020
|
+
ref: panelRef,
|
|
321
1021
|
"data-slot": "logs-detail",
|
|
322
|
-
initial: reduceMotion ? false : {
|
|
323
|
-
animate: {
|
|
324
|
-
exit: {
|
|
1022
|
+
initial: reduceMotion ? false : { x: "100%" },
|
|
1023
|
+
animate: { x: 0 },
|
|
1024
|
+
exit: { x: "100%" },
|
|
325
1025
|
transition: reduceMotion ? { duration: 0 } : {
|
|
326
1026
|
duration: .2,
|
|
327
1027
|
ease: "easeOut"
|
|
328
1028
|
},
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
1029
|
+
style: { width },
|
|
1030
|
+
className: cn("bg-background absolute inset-y-0 right-0 z-20 flex h-full flex-col overflow-hidden border-l shadow-lg", className),
|
|
1031
|
+
children: [/* @__PURE__ */ jsx(LogsDetailResizeHandle, {
|
|
1032
|
+
width,
|
|
1033
|
+
onWidthChange: setWidth,
|
|
1034
|
+
panelRef
|
|
1035
|
+
}), /* @__PURE__ */ jsx(DetailBody, {})]
|
|
334
1036
|
}) });
|
|
335
1037
|
}
|
|
336
1038
|
//#endregion
|
|
1039
|
+
//#region src/components/features/logs/utils/time-range.ts
|
|
1040
|
+
const MINUTE_MS$1 = 6e4;
|
|
1041
|
+
const HOUR_MS = 60 * MINUTE_MS$1;
|
|
1042
|
+
const DAY_MS$1 = 24 * HOUR_MS;
|
|
1043
|
+
function relativePreset(key, label, durationMs, shortcut) {
|
|
1044
|
+
return {
|
|
1045
|
+
key,
|
|
1046
|
+
label,
|
|
1047
|
+
shortcut,
|
|
1048
|
+
getRange: () => {
|
|
1049
|
+
const now = /* @__PURE__ */ new Date();
|
|
1050
|
+
return {
|
|
1051
|
+
from: new Date(now.getTime() - durationMs),
|
|
1052
|
+
to: now
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
/**
|
|
1058
|
+
* Relative "Last N" windows offered by `Logs.Filters`. Keys are persisted on
|
|
1059
|
+
* `LogTimeRange.preset` so hosts can slide the window forward with
|
|
1060
|
+
* `resolveLogTimeRange` on refresh or while live tailing.
|
|
1061
|
+
*
|
|
1062
|
+
* Shortcuts follow the `picker` datetime presets (`1` / `2` / `H` / `W`) that
|
|
1063
|
+
* the logs picker is built on. Keys that mean something else in the pickers
|
|
1064
|
+
* (`3` is "Last 30 minutes" in `time-range-picker`, `D` is "Today") are not
|
|
1065
|
+
* reused, so muscle memory carries over between the two.
|
|
1066
|
+
*/
|
|
1067
|
+
const LOG_TIME_PRESETS = [
|
|
1068
|
+
relativePreset("last-15m", "Last 15 minutes", 15 * MINUTE_MS$1, "1"),
|
|
1069
|
+
relativePreset("last-30m", "Last 30 minutes", 30 * MINUTE_MS$1, "2"),
|
|
1070
|
+
relativePreset("last-1h", "Last hour", HOUR_MS, "H"),
|
|
1071
|
+
relativePreset("last-3h", "Last 3 hours", 3 * HOUR_MS),
|
|
1072
|
+
relativePreset("last-6h", "Last 6 hours", 6 * HOUR_MS, "6"),
|
|
1073
|
+
relativePreset("last-12h", "Last 12 hours", 12 * HOUR_MS),
|
|
1074
|
+
relativePreset("last-24h", "Last 24 hours", DAY_MS$1),
|
|
1075
|
+
relativePreset("last-7d", "Last 7 days", 7 * DAY_MS$1, "W")
|
|
1076
|
+
];
|
|
1077
|
+
function lastThirtyMinutes(now = /* @__PURE__ */ new Date()) {
|
|
1078
|
+
return {
|
|
1079
|
+
from: (/* @__PURE__ */ new Date(now.getTime() - 30 * MINUTE_MS$1)).toISOString(),
|
|
1080
|
+
to: now.toISOString(),
|
|
1081
|
+
preset: "last-30m"
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
/**
|
|
1085
|
+
* Re-anchor a preset range to `now`. Absolute ranges (no `preset`, or an
|
|
1086
|
+
* unknown key) are returned unchanged.
|
|
1087
|
+
*/
|
|
1088
|
+
function resolveLogTimeRange(range, now = /* @__PURE__ */ new Date()) {
|
|
1089
|
+
if (!range.preset) return range;
|
|
1090
|
+
const preset = LOG_TIME_PRESETS.find((item) => item.key === range.preset);
|
|
1091
|
+
if (!preset) return range;
|
|
1092
|
+
const { from, to } = preset.getRange("UTC");
|
|
1093
|
+
const shift = now.getTime() - to.getTime();
|
|
1094
|
+
return {
|
|
1095
|
+
from: new Date(from.getTime() + shift).toISOString(),
|
|
1096
|
+
to: now.toISOString(),
|
|
1097
|
+
preset: range.preset
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
/** Human label for the time-range trigger: preset name, or a compact absolute range. */
|
|
1101
|
+
function logTimeRangeLabel(range) {
|
|
1102
|
+
const preset = range.preset ? LOG_TIME_PRESETS.find((item) => item.key === range.preset) : void 0;
|
|
1103
|
+
if (preset) return preset.label;
|
|
1104
|
+
const from = new Date(range.from);
|
|
1105
|
+
const to = new Date(range.to);
|
|
1106
|
+
if (Number.isNaN(from.getTime()) || Number.isNaN(to.getTime())) return "Select range";
|
|
1107
|
+
if (isSameDay(from, to)) return `${format(from, "MMM d, HH:mm")} – ${format(to, "HH:mm")}`;
|
|
1108
|
+
return `${format(from, "MMM d, HH:mm")} – ${format(to, "MMM d, HH:mm")}`;
|
|
1109
|
+
}
|
|
1110
|
+
function filtersAreActive(filters) {
|
|
1111
|
+
return Object.values(filters).some((values) => values.length > 0);
|
|
1112
|
+
}
|
|
1113
|
+
//#endregion
|
|
337
1114
|
//#region src/components/features/logs/components/filters.tsx
|
|
338
1115
|
function LogsFilterGroup({ facet, defaultOpen }) {
|
|
339
1116
|
const { filters, toggleFilterValue } = useLogs();
|
|
@@ -343,7 +1120,10 @@ function LogsFilterGroup({ facet, defaultOpen }) {
|
|
|
343
1120
|
className: "border-b border-border",
|
|
344
1121
|
children: [/* @__PURE__ */ jsxs(CollapsibleTrigger, {
|
|
345
1122
|
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(
|
|
1123
|
+
children: [facet.label, /* @__PURE__ */ jsx(Icon, {
|
|
1124
|
+
icon: ChevronDown,
|
|
1125
|
+
className: "text-muted-foreground transition-transform [[data-state=open]_&]:rotate-180"
|
|
1126
|
+
})]
|
|
347
1127
|
}), /* @__PURE__ */ jsx(CollapsibleContent, { children: /* @__PURE__ */ jsx("ul", {
|
|
348
1128
|
className: "flex flex-col gap-1 px-3 pb-3",
|
|
349
1129
|
children: facet.options.map((option) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("label", {
|
|
@@ -367,6 +1147,9 @@ function LogsFilterGroup({ facet, defaultOpen }) {
|
|
|
367
1147
|
}) })]
|
|
368
1148
|
});
|
|
369
1149
|
}
|
|
1150
|
+
function isLogTimeRange(value) {
|
|
1151
|
+
return typeof value === "object" && value !== null && typeof value.from === "string" && typeof value.to === "string";
|
|
1152
|
+
}
|
|
370
1153
|
function LogsTimeRangeFilter({ className }) {
|
|
371
1154
|
const { timeRange, setTimeRange } = useLogs();
|
|
372
1155
|
return /* @__PURE__ */ jsxs("div", {
|
|
@@ -377,23 +1160,83 @@ function LogsTimeRangeFilter({ className }) {
|
|
|
377
1160
|
}), /* @__PURE__ */ jsx(DateTimeRangePicker, {
|
|
378
1161
|
value: timeRange,
|
|
379
1162
|
onChange: (next) => {
|
|
380
|
-
if (next)
|
|
1163
|
+
if (!next) return;
|
|
1164
|
+
const preset = next.preset;
|
|
1165
|
+
setTimeRange(preset ? {
|
|
1166
|
+
from: next.from,
|
|
1167
|
+
to: next.to,
|
|
1168
|
+
preset
|
|
1169
|
+
} : {
|
|
381
1170
|
from: next.from,
|
|
382
1171
|
to: next.to
|
|
383
1172
|
});
|
|
384
1173
|
},
|
|
1174
|
+
presets: LOG_TIME_PRESETS,
|
|
1175
|
+
triggerLabel: (value) => isLogTimeRange(value) ? logTimeRangeLabel(value) : "Select range",
|
|
1176
|
+
icon: /* @__PURE__ */ jsx(Icon, {
|
|
1177
|
+
icon: Clock,
|
|
1178
|
+
className: "text-muted-foreground shrink-0"
|
|
1179
|
+
}),
|
|
385
1180
|
placeholder: "Select range",
|
|
1181
|
+
clearable: false,
|
|
386
1182
|
disableFuture: true,
|
|
387
1183
|
hideTimezone: true,
|
|
388
1184
|
numberOfMonths: 1,
|
|
389
1185
|
className: "w-full",
|
|
390
1186
|
triggerClassName: "w-full justify-start text-xs",
|
|
1187
|
+
popoverClassName: "w-auto",
|
|
391
1188
|
sheetTitle: "Select time range"
|
|
392
1189
|
})]
|
|
393
1190
|
});
|
|
394
1191
|
}
|
|
1192
|
+
const FILTER_LABEL_CHS = [
|
|
1193
|
+
6,
|
|
1194
|
+
11,
|
|
1195
|
+
4
|
|
1196
|
+
];
|
|
1197
|
+
const FILTER_VALUE_CHS = [
|
|
1198
|
+
3,
|
|
1199
|
+
8,
|
|
1200
|
+
14,
|
|
1201
|
+
6,
|
|
1202
|
+
20,
|
|
1203
|
+
11,
|
|
1204
|
+
4,
|
|
1205
|
+
16
|
|
1206
|
+
];
|
|
1207
|
+
function LogsFiltersSkeleton() {
|
|
1208
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1209
|
+
"data-slot": "logs-filters-skeleton",
|
|
1210
|
+
className: "flex flex-col",
|
|
1211
|
+
"aria-hidden": true,
|
|
1212
|
+
children: Array.from({ length: 3 }, (_, group) => /* @__PURE__ */ jsxs("div", {
|
|
1213
|
+
className: "border-b border-border",
|
|
1214
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1215
|
+
className: "flex w-full items-center justify-between px-3 py-2",
|
|
1216
|
+
children: [/* @__PURE__ */ jsx(Skeleton, {
|
|
1217
|
+
className: "h-[21px] rounded-sm font-medium",
|
|
1218
|
+
style: { width: `${FILTER_LABEL_CHS[group] ?? 8}ch` }
|
|
1219
|
+
}), /* @__PURE__ */ jsx(Skeleton, { className: "size-4 shrink-0 rounded-sm" })]
|
|
1220
|
+
}), /* @__PURE__ */ jsx("ul", {
|
|
1221
|
+
className: "flex flex-col gap-1 px-3 pb-3",
|
|
1222
|
+
children: Array.from({ length: 4 }, (_, row) => /* @__PURE__ */ jsxs("li", {
|
|
1223
|
+
className: "flex items-center gap-2 px-1 py-1",
|
|
1224
|
+
children: [
|
|
1225
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "size-4 shrink-0 rounded-[4px]" }),
|
|
1226
|
+
/* @__PURE__ */ jsx(Skeleton, {
|
|
1227
|
+
className: "h-4 max-w-full rounded-sm font-mono text-xs leading-4",
|
|
1228
|
+
style: { width: `${FILTER_VALUE_CHS[(group * 4 + row) % FILTER_VALUE_CHS.length]}ch` }
|
|
1229
|
+
}),
|
|
1230
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "ml-auto h-4 w-[2ch] rounded-sm" })
|
|
1231
|
+
]
|
|
1232
|
+
}, row))
|
|
1233
|
+
})]
|
|
1234
|
+
}, group))
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
395
1237
|
function LogsFilters({ className }) {
|
|
396
|
-
const { facets, hasActiveFilters, resetFilters } = useLogs();
|
|
1238
|
+
const { facets, hasActiveFilters, resetFilters, isLoading } = useLogs();
|
|
1239
|
+
const showFacetSkeleton = isLoading && facets.length === 0;
|
|
397
1240
|
return /* @__PURE__ */ jsxs("aside", {
|
|
398
1241
|
"data-slot": "logs-filters",
|
|
399
1242
|
className: cn("bg-background flex h-full min-h-0 w-64 shrink-0 flex-col border-r", className),
|
|
@@ -411,7 +1254,7 @@ function LogsFilters({ className }) {
|
|
|
411
1254
|
})]
|
|
412
1255
|
}), /* @__PURE__ */ jsxs("div", {
|
|
413
1256
|
className: "min-h-0 flex-1 overflow-y-auto",
|
|
414
|
-
children: [/* @__PURE__ */ jsx(LogsTimeRangeFilter, {}), facets.map((facet, index) => /* @__PURE__ */ jsx(LogsFilterGroup, {
|
|
1257
|
+
children: [/* @__PURE__ */ jsx(LogsTimeRangeFilter, {}), showFacetSkeleton ? /* @__PURE__ */ jsx(LogsFiltersSkeleton, {}) : facets.map((facet, index) => /* @__PURE__ */ jsx(LogsFilterGroup, {
|
|
415
1258
|
facet,
|
|
416
1259
|
defaultOpen: index === 0
|
|
417
1260
|
}, facet.name))]
|
|
@@ -419,61 +1262,74 @@ function LogsFilters({ className }) {
|
|
|
419
1262
|
});
|
|
420
1263
|
}
|
|
421
1264
|
//#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
1265
|
//#region src/components/features/logs/components/table.tsx
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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
|
-
}
|
|
1266
|
+
const SKELETON_ROWS = 12;
|
|
1267
|
+
/** Centred message shared by the empty and error states. */
|
|
1268
|
+
function LogsTableState({ icon, title, description, action, slot, role, tone = "muted" }) {
|
|
1269
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1270
|
+
className: "flex min-h-48 flex-1 items-center justify-center px-4 py-8 text-center",
|
|
1271
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
1272
|
+
role,
|
|
1273
|
+
"data-slot": slot,
|
|
1274
|
+
className: "flex max-w-md flex-col items-center gap-2",
|
|
1275
|
+
children: [
|
|
1276
|
+
/* @__PURE__ */ jsx("span", {
|
|
1277
|
+
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"),
|
|
1278
|
+
children: icon
|
|
1279
|
+
}),
|
|
1280
|
+
/* @__PURE__ */ jsx("p", {
|
|
1281
|
+
className: "text-foreground text-sm font-medium",
|
|
1282
|
+
children: title
|
|
1283
|
+
}),
|
|
1284
|
+
description && /* @__PURE__ */ jsx("p", {
|
|
1285
|
+
className: "text-muted-foreground text-xs",
|
|
1286
|
+
children: description
|
|
1287
|
+
}),
|
|
1288
|
+
action && /* @__PURE__ */ jsx("div", {
|
|
1289
|
+
className: "mt-1.5",
|
|
1290
|
+
children: action
|
|
1291
|
+
})
|
|
1292
|
+
]
|
|
1293
|
+
})
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
function LogCell({ column, children }) {
|
|
1297
|
+
return /* @__PURE__ */ jsx(TableCell, {
|
|
1298
|
+
className: cn(columnCellClass(column), column.className),
|
|
1299
|
+
style: columnWidthStyle(column),
|
|
1300
|
+
children
|
|
1301
|
+
});
|
|
473
1302
|
}
|
|
474
1303
|
function LogsTable({ className }) {
|
|
475
|
-
const { entries, columns, selectedId, setSelectedId, isLoading, error, selectPrevious, selectNext } = useLogs();
|
|
1304
|
+
const { entries, columns, selectedId, setSelectedId, isLoading, error, selectPrevious, selectNext, onRefresh, hasActiveFilters, resetFilters, search, setSearch } = useLogs();
|
|
476
1305
|
const scrollerRef = useRef(null);
|
|
1306
|
+
const rows = useMemo(() => entries.map((entry) => {
|
|
1307
|
+
const { parsed, path, message } = logLineDisplay(entry.line, entry.labels);
|
|
1308
|
+
return {
|
|
1309
|
+
entry,
|
|
1310
|
+
ctx: {
|
|
1311
|
+
entry,
|
|
1312
|
+
parsed,
|
|
1313
|
+
path,
|
|
1314
|
+
message
|
|
1315
|
+
}
|
|
1316
|
+
};
|
|
1317
|
+
}), [entries]);
|
|
1318
|
+
const hasRows = entries.length > 0;
|
|
1319
|
+
const showError = Boolean(error) && !hasRows;
|
|
1320
|
+
const showErrorBanner = Boolean(error) && hasRows;
|
|
1321
|
+
const showSkeleton = isLoading && !hasRows && !error;
|
|
1322
|
+
const showEmpty = !isLoading && !hasRows && !error;
|
|
1323
|
+
const retry = onRefresh && /* @__PURE__ */ jsxs(Button, {
|
|
1324
|
+
type: "secondary",
|
|
1325
|
+
theme: "outline",
|
|
1326
|
+
size: "small",
|
|
1327
|
+
onClick: onRefresh,
|
|
1328
|
+
children: [/* @__PURE__ */ jsx(Icon, {
|
|
1329
|
+
icon: RefreshCw,
|
|
1330
|
+
size: 14
|
|
1331
|
+
}), "Retry"]
|
|
1332
|
+
});
|
|
477
1333
|
function onKeyDown(event) {
|
|
478
1334
|
if (event.key === "ArrowDown" || event.key === "j") {
|
|
479
1335
|
event.preventDefault();
|
|
@@ -488,53 +1344,232 @@ function LogsTable({ className }) {
|
|
|
488
1344
|
return /* @__PURE__ */ jsxs("div", {
|
|
489
1345
|
ref: scrollerRef,
|
|
490
1346
|
"data-slot": "logs-table",
|
|
491
|
-
className: cn("min-h-0 flex-1 overflow-auto outline-none", className),
|
|
1347
|
+
className: cn("flex min-h-0 flex-1 flex-col overflow-auto outline-none", className),
|
|
492
1348
|
tabIndex: 0,
|
|
493
1349
|
"aria-busy": isLoading || void 0,
|
|
494
1350
|
onKeyDown,
|
|
495
|
-
children: [
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
1351
|
+
children: [
|
|
1352
|
+
showErrorBanner && /* @__PURE__ */ jsxs("div", {
|
|
1353
|
+
role: "alert",
|
|
1354
|
+
"data-slot": "logs-error-banner",
|
|
1355
|
+
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",
|
|
1356
|
+
children: [
|
|
1357
|
+
/* @__PURE__ */ jsx(Icon, {
|
|
1358
|
+
icon: TriangleAlert,
|
|
1359
|
+
size: 14,
|
|
1360
|
+
className: "text-destructive shrink-0"
|
|
1361
|
+
}),
|
|
1362
|
+
/* @__PURE__ */ jsxs("span", {
|
|
1363
|
+
className: "min-w-0 truncate",
|
|
1364
|
+
children: [
|
|
1365
|
+
/* @__PURE__ */ jsx("span", {
|
|
1366
|
+
className: "font-medium",
|
|
1367
|
+
children: "Refresh failed."
|
|
1368
|
+
}),
|
|
1369
|
+
" ",
|
|
1370
|
+
/* @__PURE__ */ jsx("span", {
|
|
1371
|
+
className: "text-muted-foreground",
|
|
1372
|
+
children: "Showing the last successful result."
|
|
1373
|
+
}),
|
|
1374
|
+
" ",
|
|
1375
|
+
/* @__PURE__ */ jsx("code", {
|
|
1376
|
+
className: "font-mono",
|
|
1377
|
+
children: error
|
|
1378
|
+
})
|
|
1379
|
+
]
|
|
1380
|
+
}),
|
|
1381
|
+
/* @__PURE__ */ jsx("span", {
|
|
1382
|
+
className: "ml-auto shrink-0",
|
|
1383
|
+
children: retry
|
|
1384
|
+
})
|
|
1385
|
+
]
|
|
1386
|
+
}),
|
|
1387
|
+
/* @__PURE__ */ jsxs("table", {
|
|
1388
|
+
className: "w-full shrink-0 caption-bottom table-auto text-sm [&_th]:px-3 [&_th]:py-2 [&_td]:px-3 [&_td]:py-1.5",
|
|
1389
|
+
children: [/* @__PURE__ */ jsx(TableHeader, {
|
|
1390
|
+
className: "bg-background sticky top-0 z-10",
|
|
1391
|
+
children: /* @__PURE__ */ jsx(TableRow, { children: columns.map((column) => /* @__PURE__ */ jsx(TableHead, {
|
|
1392
|
+
className: cn(columnHeadClass(column), column.headerClassName),
|
|
1393
|
+
style: columnWidthStyle(column),
|
|
1394
|
+
children: columnHeader(column)
|
|
1395
|
+
}, column.id)) })
|
|
1396
|
+
}), /* @__PURE__ */ jsxs(TableBody, { children: [showSkeleton && Array.from({ length: SKELETON_ROWS }, (_, index) => /* @__PURE__ */ jsx(TableRow, {
|
|
1397
|
+
"data-slot": "logs-skeleton-row",
|
|
1398
|
+
children: columns.map((column) => /* @__PURE__ */ jsx(LogCell, {
|
|
1399
|
+
column,
|
|
1400
|
+
children: columnSkeleton(column, index)
|
|
1401
|
+
}, column.id))
|
|
1402
|
+
}, `skeleton-${index}`)), rows.map(({ entry, ctx }) => {
|
|
519
1403
|
const selected = entry.id === selectedId;
|
|
520
1404
|
return /* @__PURE__ */ jsx(TableRow, {
|
|
521
1405
|
"data-state": selected ? "selected" : void 0,
|
|
522
1406
|
"aria-selected": selected,
|
|
523
|
-
className: cn("cursor-pointer hover:bg-foreground/
|
|
1407
|
+
className: cn("cursor-pointer hover:bg-foreground/5", "data-[state=selected]:bg-foreground/10", "data-[state=selected]:hover:bg-foreground/10"),
|
|
524
1408
|
onClick: () => {
|
|
525
1409
|
setSelectedId(selected ? null : entry.id);
|
|
526
1410
|
scrollerRef.current?.focus();
|
|
527
1411
|
},
|
|
528
1412
|
children: columns.map((column) => /* @__PURE__ */ jsx(LogCell, {
|
|
529
1413
|
column,
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
path,
|
|
533
|
-
message
|
|
534
|
-
}, column))
|
|
1414
|
+
children: column.cell(ctx)
|
|
1415
|
+
}, column.id))
|
|
535
1416
|
}, entry.id);
|
|
1417
|
+
})] })]
|
|
1418
|
+
}),
|
|
1419
|
+
showError && /* @__PURE__ */ jsx(LogsTableState, {
|
|
1420
|
+
role: "alert",
|
|
1421
|
+
slot: "logs-error",
|
|
1422
|
+
tone: "destructive",
|
|
1423
|
+
icon: /* @__PURE__ */ jsx(Icon, { icon: TriangleAlert }),
|
|
1424
|
+
title: "Couldn't load logs",
|
|
1425
|
+
description: /* @__PURE__ */ jsx("code", {
|
|
1426
|
+
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",
|
|
1427
|
+
children: error
|
|
1428
|
+
}),
|
|
1429
|
+
action: retry
|
|
1430
|
+
}),
|
|
1431
|
+
showEmpty && /* @__PURE__ */ jsx(LogsTableState, {
|
|
1432
|
+
slot: "logs-empty",
|
|
1433
|
+
icon: /* @__PURE__ */ jsx(Icon, { icon: Inbox }),
|
|
1434
|
+
title: "No logs in this time range",
|
|
1435
|
+
description: hasActiveFilters || search ? "Try widening the range or clearing the filters." : "Try widening the range or check back in a moment.",
|
|
1436
|
+
action: (hasActiveFilters || search) && /* @__PURE__ */ jsx(Button, {
|
|
1437
|
+
type: "secondary",
|
|
1438
|
+
theme: "outline",
|
|
1439
|
+
size: "small",
|
|
1440
|
+
onClick: () => {
|
|
1441
|
+
resetFilters();
|
|
1442
|
+
setSearch("");
|
|
1443
|
+
},
|
|
1444
|
+
children: "Clear filters"
|
|
536
1445
|
})
|
|
537
|
-
|
|
1446
|
+
})
|
|
1447
|
+
]
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
const MINUTE_MS = 6e4;
|
|
1451
|
+
const DAY_MS = 24 * (60 * MINUTE_MS);
|
|
1452
|
+
/**
|
|
1453
|
+
* Bucket `entries` into `bucketCount` equal-width bars across `range`.
|
|
1454
|
+
* Entries outside the range are ignored. Returns `[]` when the range is
|
|
1455
|
+
* invalid or empty so the timeline can fall back to an axis-only state.
|
|
1456
|
+
*/
|
|
1457
|
+
function histogramFromEntries(entries, range, bucketCount = 60) {
|
|
1458
|
+
const from = new Date(range.from).getTime();
|
|
1459
|
+
const to = new Date(range.to).getTime();
|
|
1460
|
+
if (Number.isNaN(from) || Number.isNaN(to) || to <= from || bucketCount < 1) return [];
|
|
1461
|
+
const width = (to - from) / bucketCount;
|
|
1462
|
+
const counts = Array.from({ length: bucketCount }).fill(0);
|
|
1463
|
+
for (const entry of entries) {
|
|
1464
|
+
const at = entry.timestamp.getTime();
|
|
1465
|
+
if (at < from || at > to) continue;
|
|
1466
|
+
const index = Math.min(Math.floor((at - from) / width), bucketCount - 1);
|
|
1467
|
+
counts[index] += 1;
|
|
1468
|
+
}
|
|
1469
|
+
return counts.map((count, index) => ({
|
|
1470
|
+
start: new Date(from + index * width).toISOString(),
|
|
1471
|
+
end: new Date(index === bucketCount - 1 ? to : from + (index + 1) * width).toISOString(),
|
|
1472
|
+
count
|
|
1473
|
+
}));
|
|
1474
|
+
}
|
|
1475
|
+
/** Index of the bucket containing `date`, or `-1`. */
|
|
1476
|
+
function histogramBucketIndex(histogram, date) {
|
|
1477
|
+
const at = date.getTime();
|
|
1478
|
+
const last = histogram.length - 1;
|
|
1479
|
+
return histogram.findIndex((bucket, index) => {
|
|
1480
|
+
const start = new Date(bucket.start).getTime();
|
|
1481
|
+
const end = new Date(bucket.end).getTime();
|
|
1482
|
+
return at >= start && (at < end || index === last && at === end);
|
|
1483
|
+
});
|
|
1484
|
+
}
|
|
1485
|
+
/** Total span covered by a histogram, in ms. `0` when empty. */
|
|
1486
|
+
function histogramSpanMs(histogram) {
|
|
1487
|
+
const first = histogram[0];
|
|
1488
|
+
const last = histogram[histogram.length - 1];
|
|
1489
|
+
if (!first || !last) return 0;
|
|
1490
|
+
return new Date(last.end).getTime() - new Date(first.start).getTime();
|
|
1491
|
+
}
|
|
1492
|
+
/** Axis tick label whose precision follows the span being displayed. */
|
|
1493
|
+
function formatTimelineTick(date, spanMs) {
|
|
1494
|
+
if (spanMs <= 2 * MINUTE_MS) return format(date, "HH:mm:ss");
|
|
1495
|
+
if (spanMs < DAY_MS) return format(date, "HH:mm");
|
|
1496
|
+
return format(date, "MMM d HH:mm");
|
|
1497
|
+
}
|
|
1498
|
+
/** Tooltip text for a bucket: count plus its window. */
|
|
1499
|
+
function formatBucketSummary(bucket, spanMs) {
|
|
1500
|
+
const start = new Date(bucket.start);
|
|
1501
|
+
const end = new Date(bucket.end);
|
|
1502
|
+
const tickSpan = end.getTime() - start.getTime() < MINUTE_MS ? MINUTE_MS : spanMs;
|
|
1503
|
+
const noun = bucket.count === 1 ? "log" : "logs";
|
|
1504
|
+
return `${bucket.count} ${noun} · ${formatTimelineTick(start, tickSpan)} – ${formatTimelineTick(end, tickSpan)}`;
|
|
1505
|
+
}
|
|
1506
|
+
//#endregion
|
|
1507
|
+
//#region src/components/features/logs/components/timeline.tsx
|
|
1508
|
+
const SKELETON_HEIGHTS = [
|
|
1509
|
+
18,
|
|
1510
|
+
34,
|
|
1511
|
+
22,
|
|
1512
|
+
48,
|
|
1513
|
+
30,
|
|
1514
|
+
60,
|
|
1515
|
+
26,
|
|
1516
|
+
40,
|
|
1517
|
+
20,
|
|
1518
|
+
52,
|
|
1519
|
+
36,
|
|
1520
|
+
28
|
|
1521
|
+
];
|
|
1522
|
+
function TimelineSkeleton() {
|
|
1523
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1524
|
+
"data-slot": "logs-timeline-skeleton",
|
|
1525
|
+
className: "flex h-10 items-end gap-px",
|
|
1526
|
+
"aria-hidden": true,
|
|
1527
|
+
children: Array.from({ length: 60 }, (_, index) => /* @__PURE__ */ jsx(Skeleton, {
|
|
1528
|
+
className: "min-w-0 flex-1 rounded-[1px]",
|
|
1529
|
+
style: { height: `${SKELETON_HEIGHTS[index % SKELETON_HEIGHTS.length]}%` }
|
|
1530
|
+
}, index))
|
|
1531
|
+
});
|
|
1532
|
+
}
|
|
1533
|
+
function LogsTimeline({ className }) {
|
|
1534
|
+
const { histogram, isLoading, selectedEntry, timeRange } = useLogs();
|
|
1535
|
+
const spanMs = histogramSpanMs(histogram);
|
|
1536
|
+
const max = useMemo(() => histogram.reduce((peak, bucket) => Math.max(peak, bucket.count), 0), [histogram]);
|
|
1537
|
+
const total = useMemo(() => histogram.reduce((sum, bucket) => sum + bucket.count, 0), [histogram]);
|
|
1538
|
+
const selectedIndex = selectedEntry ? histogramBucketIndex(histogram, selectedEntry.timestamp) : -1;
|
|
1539
|
+
const titles = useMemo(() => histogram.map((bucket) => formatBucketSummary(bucket, spanMs)), [histogram, spanMs]);
|
|
1540
|
+
const from = histogram[0] ? new Date(histogram[0].start) : new Date(timeRange.from);
|
|
1541
|
+
const to = histogram.length > 0 ? new Date(histogram[histogram.length - 1].end) : new Date(timeRange.to);
|
|
1542
|
+
const axisSpan = spanMs > 0 ? spanMs : to.getTime() - from.getTime();
|
|
1543
|
+
const mid = /* @__PURE__ */ new Date((from.getTime() + to.getTime()) / 2);
|
|
1544
|
+
const showSkeleton = isLoading && histogram.length === 0;
|
|
1545
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1546
|
+
"data-slot": "logs-timeline",
|
|
1547
|
+
className: cn("border-b px-3 pt-2 pb-1", className),
|
|
1548
|
+
children: [showSkeleton ? /* @__PURE__ */ jsx(TimelineSkeleton, {}) : /* @__PURE__ */ jsx("div", {
|
|
1549
|
+
role: "img",
|
|
1550
|
+
"aria-label": `${total} logs between ${formatTimelineTick(from, axisSpan)} and ${formatTimelineTick(to, axisSpan)}`,
|
|
1551
|
+
className: "flex h-10 items-end gap-px",
|
|
1552
|
+
children: histogram.map((bucket, index) => {
|
|
1553
|
+
const height = max > 0 ? bucket.count / max * 100 : 0;
|
|
1554
|
+
const selected = index === selectedIndex;
|
|
1555
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1556
|
+
title: titles[index],
|
|
1557
|
+
"data-slot": "logs-timeline-bucket",
|
|
1558
|
+
"data-selected": selected || void 0,
|
|
1559
|
+
className: "relative flex h-full min-w-0 flex-1 items-end",
|
|
1560
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
1561
|
+
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"),
|
|
1562
|
+
style: { height: bucket.count > 0 ? `max(${height}%, 2px)` : "1px" }
|
|
1563
|
+
})
|
|
1564
|
+
}, bucket.start);
|
|
1565
|
+
})
|
|
1566
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
1567
|
+
className: "text-muted-foreground mt-1 flex justify-between font-mono text-[10px] tabular-nums",
|
|
1568
|
+
children: [
|
|
1569
|
+
/* @__PURE__ */ jsx("span", { children: formatTimelineTick(from, axisSpan) }),
|
|
1570
|
+
/* @__PURE__ */ jsx("span", { children: formatTimelineTick(mid, axisSpan) }),
|
|
1571
|
+
/* @__PURE__ */ jsx("span", { children: formatTimelineTick(to, axisSpan) })
|
|
1572
|
+
]
|
|
538
1573
|
})]
|
|
539
1574
|
});
|
|
540
1575
|
}
|
|
@@ -563,7 +1598,10 @@ function LogsSearch({ placeholder = "Search logs...", className }) {
|
|
|
563
1598
|
}, [value, setSearch]);
|
|
564
1599
|
return /* @__PURE__ */ jsxs("div", {
|
|
565
1600
|
className: cn("relative min-w-0 flex-1", className),
|
|
566
|
-
children: [/* @__PURE__ */ jsx(
|
|
1601
|
+
children: [/* @__PURE__ */ jsx(Icon, {
|
|
1602
|
+
icon: Search,
|
|
1603
|
+
className: "text-muted-foreground pointer-events-none absolute top-1/2 left-3 -translate-y-1/2"
|
|
1604
|
+
}), /* @__PURE__ */ jsx(Input, {
|
|
567
1605
|
value,
|
|
568
1606
|
onChange: (event) => setValue(event.target.value),
|
|
569
1607
|
placeholder,
|
|
@@ -601,7 +1639,7 @@ function LogsToolbar({ children, className }) {
|
|
|
601
1639
|
size: "icon",
|
|
602
1640
|
"aria-label": "Refresh logs",
|
|
603
1641
|
onClick: onRefresh,
|
|
604
|
-
children: /* @__PURE__ */ jsx(
|
|
1642
|
+
children: /* @__PURE__ */ jsx(Icon, { icon: RefreshCw })
|
|
605
1643
|
}),
|
|
606
1644
|
onExport && /* @__PURE__ */ jsx(Button, {
|
|
607
1645
|
type: "secondary",
|
|
@@ -609,7 +1647,7 @@ function LogsToolbar({ children, className }) {
|
|
|
609
1647
|
size: "icon",
|
|
610
1648
|
"aria-label": "Export logs",
|
|
611
1649
|
onClick: () => onExport(entries),
|
|
612
|
-
children: /* @__PURE__ */ jsx(
|
|
1650
|
+
children: /* @__PURE__ */ jsx(Icon, { icon: Download })
|
|
613
1651
|
})
|
|
614
1652
|
]
|
|
615
1653
|
});
|
|
@@ -644,16 +1682,20 @@ function LogsExplorer({ className }) {
|
|
|
644
1682
|
}),
|
|
645
1683
|
/* @__PURE__ */ jsxs("div", {
|
|
646
1684
|
className: "flex min-w-0 flex-1 flex-col",
|
|
647
|
-
children: [
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
1685
|
+
children: [
|
|
1686
|
+
/* @__PURE__ */ jsx(LogsToolbar, { children: !isDesktop && /* @__PURE__ */ jsxs(Button, {
|
|
1687
|
+
type: "secondary",
|
|
1688
|
+
theme: "outline",
|
|
1689
|
+
size: "small",
|
|
1690
|
+
onClick: () => setFiltersOpen(true),
|
|
1691
|
+
children: [/* @__PURE__ */ jsx(Icon, { icon: SlidersHorizontal }), "Filters"]
|
|
1692
|
+
}) }),
|
|
1693
|
+
/* @__PURE__ */ jsx(LogsTimeline, {}),
|
|
1694
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1695
|
+
className: "relative flex min-h-0 min-w-0 flex-1",
|
|
1696
|
+
children: [/* @__PURE__ */ jsx(LogsTable, {}), isDesktop && /* @__PURE__ */ jsx(LogsDetail, {})]
|
|
1697
|
+
})
|
|
1698
|
+
]
|
|
657
1699
|
}),
|
|
658
1700
|
!isDesktop && /* @__PURE__ */ jsx(LogsDetail, {})
|
|
659
1701
|
]
|
|
@@ -687,7 +1729,8 @@ const CANONICAL_FACET_NAMES = [
|
|
|
687
1729
|
const FACET_LABELS = {
|
|
688
1730
|
severity: "Severity",
|
|
689
1731
|
service_name: "Service",
|
|
690
|
-
resource_name: "Resource"
|
|
1732
|
+
resource_name: "Resource",
|
|
1733
|
+
host: "Host"
|
|
691
1734
|
};
|
|
692
1735
|
const SEVERITY_ORDER = [
|
|
693
1736
|
"ERROR",
|
|
@@ -702,7 +1745,10 @@ const DEFAULT_LOGQL_MATCHER = "{service_name=~\".+\"}";
|
|
|
702
1745
|
//#endregion
|
|
703
1746
|
//#region src/components/features/logs/utils/facets.ts
|
|
704
1747
|
function facetLabel(name) {
|
|
705
|
-
|
|
1748
|
+
const mapped = FACET_LABELS[name];
|
|
1749
|
+
if (mapped) return mapped;
|
|
1750
|
+
const raw = name.replaceAll("_", " ");
|
|
1751
|
+
return raw ? raw.charAt(0).toUpperCase() + raw.slice(1) : raw;
|
|
706
1752
|
}
|
|
707
1753
|
function compareFacetValues(name, a, b) {
|
|
708
1754
|
if (name === "severity") {
|
|
@@ -754,25 +1800,38 @@ function filterEntries(entries, filters, search = "") {
|
|
|
754
1800
|
const label = entry.labels[name];
|
|
755
1801
|
if (!label || !values.includes(label)) return false;
|
|
756
1802
|
}
|
|
757
|
-
if (query && !entry
|
|
1803
|
+
if (query && !entryMatchesSearch(entry, query)) return false;
|
|
758
1804
|
return true;
|
|
759
1805
|
});
|
|
760
1806
|
}
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
};
|
|
1807
|
+
/**
|
|
1808
|
+
* Text a search term is matched against: the line, plus the fields the table
|
|
1809
|
+
* actually shows for a label-only access log (method, path, status, host).
|
|
1810
|
+
* Other labels (request ids, user agents, pod names) are deliberately not
|
|
1811
|
+
* searched so a hit is always visible on screen.
|
|
1812
|
+
*/
|
|
1813
|
+
function searchableLogText(entry) {
|
|
1814
|
+
const { parsed, path } = logLineDisplay(entry.line, entry.labels);
|
|
1815
|
+
const parts = [entry.line, path ?? ""];
|
|
1816
|
+
if (parsed.kind === "http") parts.push(formatHttpLogLine(parsed));
|
|
1817
|
+
const host = logRequestHost(entry.labels);
|
|
1818
|
+
if (host) parts.push(host);
|
|
1819
|
+
return parts.join("\n").toLowerCase();
|
|
769
1820
|
}
|
|
770
|
-
function
|
|
771
|
-
return
|
|
1821
|
+
function entryMatchesSearch(entry, query) {
|
|
1822
|
+
return searchableLogText(entry).includes(query);
|
|
772
1823
|
}
|
|
773
1824
|
//#endregion
|
|
774
1825
|
//#region src/components/features/logs/components/root.tsx
|
|
775
|
-
|
|
1826
|
+
/** Return the previous array while its items are shallow-equal to the new one. */
|
|
1827
|
+
function useShallowArray(next) {
|
|
1828
|
+
const ref = useRef(next);
|
|
1829
|
+
const prev = ref.current;
|
|
1830
|
+
const same = prev === next || prev.length === next.length && prev.every((item, index) => item === next[index]);
|
|
1831
|
+
if (!same) ref.current = next;
|
|
1832
|
+
return same ? prev : next;
|
|
1833
|
+
}
|
|
1834
|
+
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
1835
|
const [currentTimeRange, setTimeRange] = useControllableState({
|
|
777
1836
|
value: timeRange,
|
|
778
1837
|
defaultValue: defaultTimeRange ?? lastThirtyMinutes(),
|
|
@@ -799,6 +1858,13 @@ function LogsRoot({ entries, facets, timeRange, defaultTimeRange, filters, defau
|
|
|
799
1858
|
onChange: onSelectedIdChange
|
|
800
1859
|
});
|
|
801
1860
|
const resolvedFacets = useMemo(() => facets ?? facetsFromEntries(entries), [facets, entries]);
|
|
1861
|
+
const resolvedHistogram = useMemo(() => histogram ?? histogramFromEntries(entries, currentTimeRange), [
|
|
1862
|
+
histogram,
|
|
1863
|
+
entries,
|
|
1864
|
+
currentTimeRange
|
|
1865
|
+
]);
|
|
1866
|
+
const stableColumns = useShallowArray(columns);
|
|
1867
|
+
const resolvedColumns = useMemo(() => resolveLogColumns(stableColumns), [stableColumns]);
|
|
802
1868
|
const selectedIndex = useMemo(() => entries.findIndex((entry) => entry.id === currentSelectedId), [entries, currentSelectedId]);
|
|
803
1869
|
const selectedEntry = selectedIndex >= 0 ? entries[selectedIndex] ?? null : null;
|
|
804
1870
|
const toggleFilterValue = useCallback((name, value) => {
|
|
@@ -833,6 +1899,7 @@ function LogsRoot({ entries, facets, timeRange, defaultTimeRange, filters, defau
|
|
|
833
1899
|
const value = useMemo(() => ({
|
|
834
1900
|
entries,
|
|
835
1901
|
facets: resolvedFacets,
|
|
1902
|
+
histogram: resolvedHistogram,
|
|
836
1903
|
timeRange: currentTimeRange,
|
|
837
1904
|
setTimeRange,
|
|
838
1905
|
filters: currentFilters,
|
|
@@ -851,13 +1918,14 @@ function LogsRoot({ entries, facets, timeRange, defaultTimeRange, filters, defau
|
|
|
851
1918
|
selectNext,
|
|
852
1919
|
isLoading,
|
|
853
1920
|
error,
|
|
854
|
-
columns,
|
|
1921
|
+
columns: resolvedColumns,
|
|
855
1922
|
hasActiveFilters: filtersAreActive(currentFilters),
|
|
856
1923
|
onRefresh,
|
|
857
1924
|
onExport
|
|
858
1925
|
}), [
|
|
859
1926
|
entries,
|
|
860
1927
|
resolvedFacets,
|
|
1928
|
+
resolvedHistogram,
|
|
861
1929
|
currentTimeRange,
|
|
862
1930
|
setTimeRange,
|
|
863
1931
|
currentFilters,
|
|
@@ -876,7 +1944,7 @@ function LogsRoot({ entries, facets, timeRange, defaultTimeRange, filters, defau
|
|
|
876
1944
|
selectNext,
|
|
877
1945
|
isLoading,
|
|
878
1946
|
error,
|
|
879
|
-
|
|
1947
|
+
resolvedColumns,
|
|
880
1948
|
onRefresh,
|
|
881
1949
|
onExport
|
|
882
1950
|
]);
|
|
@@ -900,6 +1968,7 @@ const Logs = {
|
|
|
900
1968
|
Toolbar: LogsToolbar,
|
|
901
1969
|
Search: LogsSearch,
|
|
902
1970
|
LiveToggle: LogsLiveToggle,
|
|
1971
|
+
Timeline: LogsTimeline,
|
|
903
1972
|
Table: LogsTable,
|
|
904
1973
|
Detail: LogsDetail
|
|
905
1974
|
};
|
|
@@ -977,4 +2046,4 @@ function flattenLokiStreams(response) {
|
|
|
977
2046
|
return entries;
|
|
978
2047
|
}
|
|
979
2048
|
//#endregion
|
|
980
|
-
export { Logs, buildLogQL, facetsFromEntries, filterEntries, flattenLokiStreams, httpStatusBadgeType, httpStatusTextClass, lastThirtyMinutes, logLineDisplay, nsToDate, parseLogLine, severityBadgeType, useLogs };
|
|
2049
|
+
export { LOG_TIME_PRESETS, Logs, buildLogQL, facetsFromEntries, filterEntries, flattenLokiStreams, formatHttpLogLine, histogramFromEntries, httpStatusBadgeType, httpStatusTextClass, lastThirtyMinutes, logLineDisplay, logRequestHost, logTimeRangeLabel, logUserAgent, nsToDate, parseLogLine, parseUserAgent, resolveLogColumns, resolveLogTimeRange, searchableLogText, severityBadgeType, splitPathQuery, useLogs };
|