@datum-cloud/datum-ui 2.0.0 → 2.3.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/app-navigation/index.mjs +2 -2
- package/dist/{app-navigation-CNig4Maj.mjs → app-navigation--tEBCrfl.mjs} +180 -164
- package/dist/autocomplete/index.mjs +1 -1
- package/dist/{autocomplete-DM6JYV4W.mjs → autocomplete-DZ_HSBak.mjs} +1 -1
- package/dist/autosearch/index.mjs +1 -1
- package/dist/{calendar-date-picker-AzvSqjeS.mjs → calendar-date-picker-ynSGADfR.mjs} +1 -1
- package/dist/components/base/sheet/sheet.d.ts +2 -1
- package/dist/components/base/sidebar/context.d.ts +3 -0
- package/dist/components/base/sidebar/shell.d.ts +2 -2
- package/dist/components/features/app-navigation/nav-menu/index.d.ts +1 -1
- package/dist/components/features/app-navigation/nav-menu/nav-menu-context.d.ts +4 -0
- package/dist/components/features/app-navigation/nav-menu/nav-menu-item.d.ts +3 -0
- package/dist/components/features/app-navigation/nav-menu/types.d.ts +18 -0
- package/dist/components/features/date-time/date-time.d.ts +6 -0
- package/dist/components/features/date-time/formatters.d.ts +13 -0
- package/dist/components/features/date-time/index.d.ts +3 -0
- package/dist/components/features/date-time/types.d.ts +47 -0
- package/dist/components/features/index.d.ts +1 -0
- package/dist/components/features/logs/components/detail.d.ts +3 -0
- package/dist/components/features/logs/components/explorer.d.ts +3 -0
- package/dist/components/features/logs/components/filters.d.ts +11 -0
- package/dist/components/features/logs/components/root.d.ts +2 -0
- package/dist/components/features/logs/components/status-badge.d.ts +7 -0
- package/dist/components/features/logs/components/table.d.ts +3 -0
- package/dist/components/features/logs/components/toolbar.d.ts +12 -0
- package/dist/components/features/logs/fixtures/index.d.ts +2 -0
- package/dist/components/features/logs/hooks/use-controllable-state.d.ts +5 -0
- package/dist/components/features/logs/hooks/use-logs.d.ts +3 -0
- package/dist/components/features/logs/index.d.ts +4 -0
- package/dist/components/features/logs/logs.d.ts +18 -0
- package/dist/components/features/logs/types.d.ts +105 -0
- package/dist/components/features/logs/utils/build-logql.d.ts +2 -0
- package/dist/components/features/logs/utils/constants.d.ts +6 -0
- package/dist/components/features/logs/utils/facets.d.ts +3 -0
- package/dist/components/features/logs/utils/flatten-loki.d.ts +3 -0
- package/dist/components/features/logs/utils/format-timestamp.d.ts +4 -0
- package/dist/components/features/logs/utils/index.d.ts +8 -0
- package/dist/components/features/logs/utils/parse-log-line.d.ts +7 -0
- package/dist/components/features/logs/utils/severity.d.ts +5 -0
- package/dist/components/features/logs/utils/time-range.d.ts +3 -0
- package/dist/data-table/index.mjs +1 -1
- package/dist/{data-table-DjGvAdO_.mjs → data-table-Du7_FLBi.mjs} +4 -4
- package/dist/date-picker/index.mjs +3 -3
- package/dist/date-time/index.mjs +2 -0
- package/dist/date-time-CA-09lnw.mjs +134 -0
- package/dist/date-time-picker/index.mjs +1 -1
- package/dist/{date-time-picker-D3-DeNo8.mjs → date-time-picker-BzcHrf-A.mjs} +1 -1
- package/dist/{picker-b5-x-EHs.mjs → date-time-range-picker-Dog33Ry0.mjs} +22 -456
- package/dist/form/index.mjs +1 -1
- package/dist/{form-BUUNQfC9.mjs → form-BhBsoMFA.mjs} +4 -4
- package/dist/grouped-table/index.mjs +1 -1
- package/dist/{grouped-table-D4J3ioHb.mjs → grouped-table--jejU1Jy.mjs} +1 -1
- package/dist/index.mjs +21 -20
- package/dist/logs/fixtures/index.mjs +231 -0
- package/dist/logs/index.mjs +980 -0
- package/dist/mobile-sheet/index.mjs +1 -1
- package/dist/{mobile-sheet-D9L-JOee.mjs → mobile-sheet-BYp-QQmn.mjs} +1 -1
- package/dist/more-actions/index.mjs +1 -1
- package/dist/{more-actions-Bx443Ccx.mjs → more-actions-B1FUf2rm.mjs} +1 -1
- package/dist/multi-select/index.mjs +1 -1
- package/dist/{multi-select-PHaMEbY-.mjs → multi-select-PtdMChPF.mjs} +1 -1
- package/dist/picker/index.mjs +2 -1
- package/dist/picker-raVuDxsh.mjs +453 -0
- package/dist/responsive-dropdown/index.mjs +1 -1
- package/dist/{responsive-dropdown-fCmduKaz.mjs → responsive-dropdown-n0Txkax5.mjs} +1 -1
- package/dist/responsive-popover/index.mjs +1 -1
- package/dist/{responsive-popover-DJZgXWIE.mjs → responsive-popover-C_1nzIvm.mjs} +1 -1
- package/dist/rich-text-editor/index.mjs +1 -1
- package/dist/{rich-text-editor-CGuSn8xi.mjs → rich-text-editor-Dohp-pbf.mjs} +1 -1
- package/dist/sheet/index.mjs +1 -1
- package/dist/{sheet-BZViad2U.mjs → sheet-C2i2k5i2.mjs} +2 -2
- package/dist/sidebar/index.mjs +1 -1
- package/dist/{sidebar-BRUNOqXi.mjs → sidebar-D6BoAr6C.mjs} +53 -16
- package/dist/styles/shadcn/animations.css +2 -2
- package/dist/task-queue/index.mjs +1 -1
- package/dist/{task-queue-dropdown-pqFd4rSf.mjs → task-queue-dropdown-ClLLTMxU.mjs} +1 -1
- package/dist/time-picker/index.mjs +1 -1
- package/dist/{time-picker-B_fN5vhO.mjs → time-picker-CFhaHTtI.mjs} +1 -1
- package/dist/{to-api-format-43Boq7OR.mjs → to-api-format-WC7b9Mcv.mjs} +2 -2
- package/package.json +16 -1
|
@@ -0,0 +1,980 @@
|
|
|
1
|
+
import { t as cn } from "../utils-Bu32wN-x.mjs";
|
|
2
|
+
import { t as Badge } from "../badge-BTNW_FEU.mjs";
|
|
3
|
+
import { t as Button } from "../button-A1qTjZ8U.mjs";
|
|
4
|
+
import { t as Checkbox } from "../checkbox-C6P12fio.mjs";
|
|
5
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../collapsible/index.mjs";
|
|
6
|
+
import { t as Input } from "../input-j4tbmYYz.mjs";
|
|
7
|
+
import { t as Sheet } from "../sheet-C2i2k5i2.mjs";
|
|
8
|
+
import { t as useBreakpoint } from "../use-breakpoint-CwiUJSmk.mjs";
|
|
9
|
+
import { Skeleton } from "../skeleton/index.mjs";
|
|
10
|
+
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";
|
|
13
|
+
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";
|
|
16
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
17
|
+
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
|
+
};
|
|
69
|
+
return {
|
|
70
|
+
parsed,
|
|
71
|
+
path,
|
|
72
|
+
message: extraMessage(stripOnce(line, path))
|
|
73
|
+
};
|
|
74
|
+
}
|
|
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 : "";
|
|
79
|
+
}
|
|
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();
|
|
84
|
+
}
|
|
85
|
+
//#endregion
|
|
86
|
+
//#region src/components/features/logs/utils/severity.ts
|
|
87
|
+
const SEVERITY_BADGE = {
|
|
88
|
+
ERROR: "danger",
|
|
89
|
+
FATAL: "danger",
|
|
90
|
+
CRITICAL: "danger",
|
|
91
|
+
WARN: "warning",
|
|
92
|
+
WARNING: "warning",
|
|
93
|
+
INFO: "info",
|
|
94
|
+
DEBUG: "muted",
|
|
95
|
+
TRACE: "muted"
|
|
96
|
+
};
|
|
97
|
+
function severityBadgeType(severity) {
|
|
98
|
+
if (!severity) return "muted";
|
|
99
|
+
return SEVERITY_BADGE[severity.toUpperCase()] ?? "muted";
|
|
100
|
+
}
|
|
101
|
+
function httpStatusBadgeType(status) {
|
|
102
|
+
if (status >= 500) return "danger";
|
|
103
|
+
if (status >= 400) return "warning";
|
|
104
|
+
if (status >= 300) return "info";
|
|
105
|
+
if (status >= 200) return "success";
|
|
106
|
+
return "muted";
|
|
107
|
+
}
|
|
108
|
+
function httpStatusTextClass(status) {
|
|
109
|
+
switch (httpStatusBadgeType(status)) {
|
|
110
|
+
case "danger": return "text-[var(--color-badge-danger)]";
|
|
111
|
+
case "warning": return "text-[var(--color-badge-warning)]";
|
|
112
|
+
case "info": return "text-[var(--color-badge-info)]";
|
|
113
|
+
case "success": return "text-[var(--color-badge-success)]";
|
|
114
|
+
default: return "text-muted-foreground";
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
//#endregion
|
|
118
|
+
//#region src/components/features/logs/components/status-badge.tsx
|
|
119
|
+
function LogsSeverityBadge({ severity }) {
|
|
120
|
+
return /* @__PURE__ */ jsx(Badge, {
|
|
121
|
+
type: severityBadgeType(severity),
|
|
122
|
+
theme: "light",
|
|
123
|
+
className: "h-4 px-1.5 py-0 font-mono text-[10px] leading-4 font-medium",
|
|
124
|
+
children: severity ?? "LOG"
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
function LogsStatusBadge({ parsed }) {
|
|
128
|
+
if (parsed.kind !== "http") return /* @__PURE__ */ jsx("span", {
|
|
129
|
+
className: "text-muted-foreground",
|
|
130
|
+
children: "—"
|
|
131
|
+
});
|
|
132
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
133
|
+
className: "inline-grid grid-cols-[7ch_auto] gap-x-2 font-mono text-[11px] font-medium",
|
|
134
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
135
|
+
className: "text-muted-foreground",
|
|
136
|
+
children: parsed.method
|
|
137
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
138
|
+
className: cn("tabular-nums", httpStatusTextClass(parsed.status)),
|
|
139
|
+
children: parsed.status
|
|
140
|
+
})]
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
//#endregion
|
|
144
|
+
//#region src/components/features/logs/components/detail.tsx
|
|
145
|
+
function MetaCell({ label, children, className }) {
|
|
146
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
147
|
+
className: cn("bg-background min-w-0 px-3 py-2.5", className),
|
|
148
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
149
|
+
className: "text-muted-foreground mb-1 text-[11px] font-medium",
|
|
150
|
+
children: label
|
|
151
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
152
|
+
className: "font-mono text-xs break-all",
|
|
153
|
+
children
|
|
154
|
+
})]
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
function DetailBody({ showClose = true }) {
|
|
158
|
+
const { selectedEntry, selectPrevious, selectNext, setSelectedId } = useLogs();
|
|
159
|
+
const [, copy] = useCopyToClipboard();
|
|
160
|
+
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;
|
|
164
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("header", {
|
|
165
|
+
className: "flex items-start gap-2 border-b px-4 py-3",
|
|
166
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
167
|
+
className: "min-w-0 flex-1",
|
|
168
|
+
children: [
|
|
169
|
+
/* @__PURE__ */ jsx("div", {
|
|
170
|
+
className: "mb-1.5",
|
|
171
|
+
children: /* @__PURE__ */ jsx(LogsSeverityBadge, { severity: selectedEntry.labels.severity })
|
|
172
|
+
}),
|
|
173
|
+
/* @__PURE__ */ jsx("h3", {
|
|
174
|
+
className: "truncate font-mono text-sm font-medium",
|
|
175
|
+
children: title
|
|
176
|
+
}),
|
|
177
|
+
/* @__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
|
+
})]
|
|
192
|
+
})
|
|
193
|
+
]
|
|
194
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
195
|
+
className: "flex shrink-0 items-center gap-1",
|
|
196
|
+
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" })
|
|
228
|
+
})
|
|
229
|
+
]
|
|
230
|
+
})]
|
|
231
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
232
|
+
className: "flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto px-4 py-4",
|
|
233
|
+
children: [
|
|
234
|
+
/* @__PURE__ */ jsxs("section", { children: [/* @__PURE__ */ jsx("h4", {
|
|
235
|
+
className: "text-muted-foreground mb-2 text-xs font-medium",
|
|
236
|
+
children: "Message"
|
|
237
|
+
}), /* @__PURE__ */ jsx("pre", {
|
|
238
|
+
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
|
|
240
|
+
})] }),
|
|
241
|
+
parsed.kind === "http" && /* @__PURE__ */ jsxs("section", { children: [/* @__PURE__ */ jsx("h4", {
|
|
242
|
+
className: "text-muted-foreground mb-2 text-xs font-medium",
|
|
243
|
+
children: "Request"
|
|
244
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
245
|
+
className: "bg-border grid grid-cols-3 gap-px overflow-hidden rounded-md border",
|
|
246
|
+
children: [
|
|
247
|
+
/* @__PURE__ */ jsx(MetaCell, {
|
|
248
|
+
label: "Method",
|
|
249
|
+
children: parsed.method
|
|
250
|
+
}),
|
|
251
|
+
/* @__PURE__ */ jsx(MetaCell, {
|
|
252
|
+
label: "Status",
|
|
253
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
254
|
+
className: cn("font-medium tabular-nums", httpStatusTextClass(parsed.status)),
|
|
255
|
+
children: parsed.status
|
|
256
|
+
})
|
|
257
|
+
}),
|
|
258
|
+
/* @__PURE__ */ jsxs(MetaCell, {
|
|
259
|
+
label: "Duration",
|
|
260
|
+
children: [parsed.durationMs, "ms"]
|
|
261
|
+
}),
|
|
262
|
+
/* @__PURE__ */ jsx(MetaCell, {
|
|
263
|
+
label: "Path",
|
|
264
|
+
className: "col-span-3",
|
|
265
|
+
children: parsed.path
|
|
266
|
+
})
|
|
267
|
+
]
|
|
268
|
+
})] }),
|
|
269
|
+
/* @__PURE__ */ jsxs("section", { children: [/* @__PURE__ */ jsx("h4", {
|
|
270
|
+
className: "text-muted-foreground mb-2 text-xs font-medium",
|
|
271
|
+
children: "Labels"
|
|
272
|
+
}), /* @__PURE__ */ jsx("dl", {
|
|
273
|
+
className: "overflow-hidden rounded-md border",
|
|
274
|
+
children: Object.entries(selectedEntry.labels).map(([key, value]) => /* @__PURE__ */ jsxs("div", {
|
|
275
|
+
className: "grid grid-cols-[minmax(0,8.5rem)_minmax(0,1fr)] gap-x-3 border-b px-3 py-2 last:border-b-0",
|
|
276
|
+
children: [/* @__PURE__ */ jsx("dt", {
|
|
277
|
+
className: "text-muted-foreground truncate font-mono text-[11px]",
|
|
278
|
+
children: key
|
|
279
|
+
}), /* @__PURE__ */ jsx("dd", {
|
|
280
|
+
className: "min-w-0 font-mono text-xs break-all",
|
|
281
|
+
children: value
|
|
282
|
+
})]
|
|
283
|
+
}, key))
|
|
284
|
+
})] })
|
|
285
|
+
]
|
|
286
|
+
})] });
|
|
287
|
+
}
|
|
288
|
+
function LogsDetail({ className }) {
|
|
289
|
+
const { selectedEntry, setSelectedId } = useLogs();
|
|
290
|
+
const breakpoint = useBreakpoint();
|
|
291
|
+
const reduceMotion = useReducedMotion();
|
|
292
|
+
const isDesktop = breakpoint === "desktop";
|
|
293
|
+
const open = selectedEntry !== null;
|
|
294
|
+
if (!isDesktop) return /* @__PURE__ */ jsx(Sheet, {
|
|
295
|
+
open,
|
|
296
|
+
onOpenChange: (next) => {
|
|
297
|
+
if (!next) setSelectedId(null);
|
|
298
|
+
},
|
|
299
|
+
children: /* @__PURE__ */ jsxs(Sheet.Content, {
|
|
300
|
+
side: "right",
|
|
301
|
+
showCloseButton: false,
|
|
302
|
+
className: "sm:max-w-md w-full gap-0 p-0",
|
|
303
|
+
"data-slot": "logs-detail",
|
|
304
|
+
children: [
|
|
305
|
+
/* @__PURE__ */ jsx(Sheet.Title, {
|
|
306
|
+
className: "sr-only",
|
|
307
|
+
children: "Log details"
|
|
308
|
+
}),
|
|
309
|
+
/* @__PURE__ */ jsx(Sheet.Description, {
|
|
310
|
+
className: "sr-only",
|
|
311
|
+
children: "Selected log entry"
|
|
312
|
+
}),
|
|
313
|
+
/* @__PURE__ */ jsx("div", {
|
|
314
|
+
className: "flex h-full flex-col",
|
|
315
|
+
children: /* @__PURE__ */ jsx(DetailBody, {})
|
|
316
|
+
})
|
|
317
|
+
]
|
|
318
|
+
})
|
|
319
|
+
});
|
|
320
|
+
return /* @__PURE__ */ jsx(AnimatePresence, { children: open && /* @__PURE__ */ jsx(motion.aside, {
|
|
321
|
+
"data-slot": "logs-detail",
|
|
322
|
+
initial: reduceMotion ? false : { width: 0 },
|
|
323
|
+
animate: { width: 400 },
|
|
324
|
+
exit: { width: 0 },
|
|
325
|
+
transition: reduceMotion ? { duration: 0 } : {
|
|
326
|
+
duration: .2,
|
|
327
|
+
ease: "easeOut"
|
|
328
|
+
},
|
|
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
|
+
})
|
|
334
|
+
}) });
|
|
335
|
+
}
|
|
336
|
+
//#endregion
|
|
337
|
+
//#region src/components/features/logs/components/filters.tsx
|
|
338
|
+
function LogsFilterGroup({ facet, defaultOpen }) {
|
|
339
|
+
const { filters, toggleFilterValue } = useLogs();
|
|
340
|
+
const selected = filters[facet.name] ?? [];
|
|
341
|
+
return /* @__PURE__ */ jsxs(Collapsible, {
|
|
342
|
+
defaultOpen,
|
|
343
|
+
className: "border-b border-border",
|
|
344
|
+
children: [/* @__PURE__ */ jsxs(CollapsibleTrigger, {
|
|
345
|
+
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" })]
|
|
347
|
+
}), /* @__PURE__ */ jsx(CollapsibleContent, { children: /* @__PURE__ */ jsx("ul", {
|
|
348
|
+
className: "flex flex-col gap-1 px-3 pb-3",
|
|
349
|
+
children: facet.options.map((option) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("label", {
|
|
350
|
+
className: "hover:bg-muted/40 flex cursor-pointer items-center gap-2 rounded-md px-1 py-1 text-sm",
|
|
351
|
+
children: [
|
|
352
|
+
/* @__PURE__ */ jsx(Checkbox, {
|
|
353
|
+
checked: selected.includes(option.value),
|
|
354
|
+
onCheckedChange: () => toggleFilterValue(facet.name, option.value),
|
|
355
|
+
"aria-label": option.value
|
|
356
|
+
}),
|
|
357
|
+
/* @__PURE__ */ jsx("span", {
|
|
358
|
+
className: "min-w-0 flex-1 truncate font-mono text-xs",
|
|
359
|
+
children: option.value
|
|
360
|
+
}),
|
|
361
|
+
option.count !== void 0 && /* @__PURE__ */ jsx("span", {
|
|
362
|
+
className: "text-muted-foreground tabular-nums text-xs",
|
|
363
|
+
children: option.count
|
|
364
|
+
})
|
|
365
|
+
]
|
|
366
|
+
}) }, option.value))
|
|
367
|
+
}) })]
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
function LogsTimeRangeFilter({ className }) {
|
|
371
|
+
const { timeRange, setTimeRange } = useLogs();
|
|
372
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
373
|
+
className: cn("border-b border-border px-3 py-3", className),
|
|
374
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
375
|
+
className: "text-muted-foreground mb-2 text-xs font-medium",
|
|
376
|
+
children: "Time range"
|
|
377
|
+
}), /* @__PURE__ */ jsx(DateTimeRangePicker, {
|
|
378
|
+
value: timeRange,
|
|
379
|
+
onChange: (next) => {
|
|
380
|
+
if (next) setTimeRange({
|
|
381
|
+
from: next.from,
|
|
382
|
+
to: next.to
|
|
383
|
+
});
|
|
384
|
+
},
|
|
385
|
+
placeholder: "Select range",
|
|
386
|
+
disableFuture: true,
|
|
387
|
+
hideTimezone: true,
|
|
388
|
+
numberOfMonths: 1,
|
|
389
|
+
className: "w-full",
|
|
390
|
+
triggerClassName: "w-full justify-start text-xs",
|
|
391
|
+
sheetTitle: "Select time range"
|
|
392
|
+
})]
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
function LogsFilters({ className }) {
|
|
396
|
+
const { facets, hasActiveFilters, resetFilters } = useLogs();
|
|
397
|
+
return /* @__PURE__ */ jsxs("aside", {
|
|
398
|
+
"data-slot": "logs-filters",
|
|
399
|
+
className: cn("bg-background flex h-full min-h-0 w-64 shrink-0 flex-col border-r", className),
|
|
400
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
401
|
+
className: "flex items-center justify-between px-3 py-3",
|
|
402
|
+
children: [/* @__PURE__ */ jsx("h2", {
|
|
403
|
+
className: "text-sm font-semibold",
|
|
404
|
+
children: "Filters"
|
|
405
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
406
|
+
type: "button",
|
|
407
|
+
className: "text-muted-foreground hover:text-foreground text-xs disabled:opacity-40",
|
|
408
|
+
disabled: !hasActiveFilters,
|
|
409
|
+
onClick: resetFilters,
|
|
410
|
+
children: "Reset"
|
|
411
|
+
})]
|
|
412
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
413
|
+
className: "min-h-0 flex-1 overflow-y-auto",
|
|
414
|
+
children: [/* @__PURE__ */ jsx(LogsTimeRangeFilter, {}), facets.map((facet, index) => /* @__PURE__ */ jsx(LogsFilterGroup, {
|
|
415
|
+
facet,
|
|
416
|
+
defaultOpen: index === 0
|
|
417
|
+
}, facet.name))]
|
|
418
|
+
})]
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
//#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
|
+
//#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
|
+
}
|
|
473
|
+
}
|
|
474
|
+
function LogsTable({ className }) {
|
|
475
|
+
const { entries, columns, selectedId, setSelectedId, isLoading, error, selectPrevious, selectNext } = useLogs();
|
|
476
|
+
const scrollerRef = useRef(null);
|
|
477
|
+
function onKeyDown(event) {
|
|
478
|
+
if (event.key === "ArrowDown" || event.key === "j") {
|
|
479
|
+
event.preventDefault();
|
|
480
|
+
selectNext();
|
|
481
|
+
}
|
|
482
|
+
if (event.key === "ArrowUp" || event.key === "k") {
|
|
483
|
+
event.preventDefault();
|
|
484
|
+
selectPrevious();
|
|
485
|
+
}
|
|
486
|
+
if (event.key === "Escape") setSelectedId(null);
|
|
487
|
+
}
|
|
488
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
489
|
+
ref: scrollerRef,
|
|
490
|
+
"data-slot": "logs-table",
|
|
491
|
+
className: cn("min-h-0 flex-1 overflow-auto outline-none", className),
|
|
492
|
+
tabIndex: 0,
|
|
493
|
+
"aria-busy": isLoading || void 0,
|
|
494
|
+
onKeyDown,
|
|
495
|
+
children: [error && /* @__PURE__ */ jsx("div", {
|
|
496
|
+
role: "alert",
|
|
497
|
+
className: "text-destructive px-4 py-3 text-sm",
|
|
498
|
+
children: error
|
|
499
|
+
}), /* @__PURE__ */ jsxs("table", {
|
|
500
|
+
className: "w-full caption-bottom table-fixed text-sm",
|
|
501
|
+
children: [/* @__PURE__ */ jsx(TableHeader, {
|
|
502
|
+
className: "bg-background sticky top-0 z-10",
|
|
503
|
+
children: /* @__PURE__ */ jsx(TableRow, { children: columns.map((column) => /* @__PURE__ */ jsx(TableHead, {
|
|
504
|
+
className: COLUMN_HEAD_CLASS[column],
|
|
505
|
+
children: COLUMN_LABEL[column]
|
|
506
|
+
}, column)) })
|
|
507
|
+
}), /* @__PURE__ */ jsxs(TableBody, { children: [
|
|
508
|
+
isLoading && entries.length === 0 && Array.from({ length: 8 }, (_, index) => /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(TableCell, {
|
|
509
|
+
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, {
|
|
513
|
+
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);
|
|
519
|
+
const selected = entry.id === selectedId;
|
|
520
|
+
return /* @__PURE__ */ jsx(TableRow, {
|
|
521
|
+
"data-state": selected ? "selected" : void 0,
|
|
522
|
+
"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]"),
|
|
524
|
+
onClick: () => {
|
|
525
|
+
setSelectedId(selected ? null : entry.id);
|
|
526
|
+
scrollerRef.current?.focus();
|
|
527
|
+
},
|
|
528
|
+
children: columns.map((column) => /* @__PURE__ */ jsx(LogCell, {
|
|
529
|
+
column,
|
|
530
|
+
entry,
|
|
531
|
+
parsed,
|
|
532
|
+
path,
|
|
533
|
+
message
|
|
534
|
+
}, column))
|
|
535
|
+
}, entry.id);
|
|
536
|
+
})
|
|
537
|
+
] })]
|
|
538
|
+
})]
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
//#endregion
|
|
542
|
+
//#region src/components/features/logs/components/toolbar.tsx
|
|
543
|
+
function LogsSearch({ placeholder = "Search logs...", className }) {
|
|
544
|
+
const { search, setSearch } = useLogs();
|
|
545
|
+
const [value, setValue] = useState(search);
|
|
546
|
+
const searchRef = useRef(search);
|
|
547
|
+
const skipNextSync = useRef(false);
|
|
548
|
+
searchRef.current = search;
|
|
549
|
+
useEffect(() => {
|
|
550
|
+
if (skipNextSync.current) {
|
|
551
|
+
skipNextSync.current = false;
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
setValue(search);
|
|
555
|
+
}, [search]);
|
|
556
|
+
useEffect(() => {
|
|
557
|
+
if (value === searchRef.current) return;
|
|
558
|
+
const timer = setTimeout(() => {
|
|
559
|
+
skipNextSync.current = true;
|
|
560
|
+
setSearch(value);
|
|
561
|
+
}, 250);
|
|
562
|
+
return () => clearTimeout(timer);
|
|
563
|
+
}, [value, setSearch]);
|
|
564
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
565
|
+
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, {
|
|
567
|
+
value,
|
|
568
|
+
onChange: (event) => setValue(event.target.value),
|
|
569
|
+
placeholder,
|
|
570
|
+
"aria-label": placeholder,
|
|
571
|
+
className: "pl-9",
|
|
572
|
+
"data-slot": "logs-search"
|
|
573
|
+
})]
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
function LogsLiveToggle({ className }) {
|
|
577
|
+
const { live, setLive } = useLogs();
|
|
578
|
+
return /* @__PURE__ */ jsxs(Button, {
|
|
579
|
+
type: "secondary",
|
|
580
|
+
theme: "outline",
|
|
581
|
+
size: "small",
|
|
582
|
+
"aria-pressed": live,
|
|
583
|
+
onClick: () => setLive(!live),
|
|
584
|
+
className: cn("gap-2", className),
|
|
585
|
+
"data-slot": "logs-live",
|
|
586
|
+
children: [/* @__PURE__ */ jsx("span", { className: cn("size-2 rounded-full", live ? "bg-destructive animate-pulse" : "bg-muted-foreground/40") }), "Live"]
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
function LogsToolbar({ children, className }) {
|
|
590
|
+
const { onRefresh, onExport, entries } = useLogs();
|
|
591
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
592
|
+
"data-slot": "logs-toolbar",
|
|
593
|
+
className: cn("flex items-center gap-2 border-b px-3 py-2", className),
|
|
594
|
+
children: [
|
|
595
|
+
/* @__PURE__ */ jsx(LogsSearch, {}),
|
|
596
|
+
children,
|
|
597
|
+
/* @__PURE__ */ jsx(LogsLiveToggle, {}),
|
|
598
|
+
onRefresh && /* @__PURE__ */ jsx(Button, {
|
|
599
|
+
type: "secondary",
|
|
600
|
+
theme: "outline",
|
|
601
|
+
size: "icon",
|
|
602
|
+
"aria-label": "Refresh logs",
|
|
603
|
+
onClick: onRefresh,
|
|
604
|
+
children: /* @__PURE__ */ jsx(RefreshCw, { className: "size-4" })
|
|
605
|
+
}),
|
|
606
|
+
onExport && /* @__PURE__ */ jsx(Button, {
|
|
607
|
+
type: "secondary",
|
|
608
|
+
theme: "outline",
|
|
609
|
+
size: "icon",
|
|
610
|
+
"aria-label": "Export logs",
|
|
611
|
+
onClick: () => onExport(entries),
|
|
612
|
+
children: /* @__PURE__ */ jsx(Download, { className: "size-4" })
|
|
613
|
+
})
|
|
614
|
+
]
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
//#endregion
|
|
618
|
+
//#region src/components/features/logs/components/explorer.tsx
|
|
619
|
+
function LogsExplorer({ className }) {
|
|
620
|
+
const isDesktop = useBreakpoint() === "desktop";
|
|
621
|
+
const [filtersOpen, setFiltersOpen] = useState(false);
|
|
622
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
623
|
+
"data-slot": "logs-explorer",
|
|
624
|
+
className: cn("flex min-h-0 flex-1 overflow-hidden", className),
|
|
625
|
+
children: [
|
|
626
|
+
isDesktop ? /* @__PURE__ */ jsx(LogsFilters, {}) : /* @__PURE__ */ jsx(Sheet, {
|
|
627
|
+
open: filtersOpen,
|
|
628
|
+
onOpenChange: setFiltersOpen,
|
|
629
|
+
children: /* @__PURE__ */ jsxs(Sheet.Content, {
|
|
630
|
+
side: "left",
|
|
631
|
+
className: "w-72 p-0 sm:max-w-72",
|
|
632
|
+
children: [
|
|
633
|
+
/* @__PURE__ */ jsx(Sheet.Title, {
|
|
634
|
+
className: "sr-only",
|
|
635
|
+
children: "Filters"
|
|
636
|
+
}),
|
|
637
|
+
/* @__PURE__ */ jsx(Sheet.Description, {
|
|
638
|
+
className: "sr-only",
|
|
639
|
+
children: "Log filters"
|
|
640
|
+
}),
|
|
641
|
+
/* @__PURE__ */ jsx(LogsFilters, { className: "w-full border-r-0 [&>div:first-child]:pr-12" })
|
|
642
|
+
]
|
|
643
|
+
})
|
|
644
|
+
}),
|
|
645
|
+
/* @__PURE__ */ jsxs("div", {
|
|
646
|
+
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
|
+
})]
|
|
657
|
+
}),
|
|
658
|
+
!isDesktop && /* @__PURE__ */ jsx(LogsDetail, {})
|
|
659
|
+
]
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
//#endregion
|
|
663
|
+
//#region src/components/features/logs/hooks/use-controllable-state.ts
|
|
664
|
+
function useControllableState({ value, defaultValue, onChange }) {
|
|
665
|
+
const [internal, setInternal] = useState(defaultValue);
|
|
666
|
+
const isControlled = value !== void 0;
|
|
667
|
+
return [isControlled ? value : internal, useCallback((next) => {
|
|
668
|
+
if (!isControlled) setInternal(next);
|
|
669
|
+
onChange?.(next);
|
|
670
|
+
}, [isControlled, onChange])];
|
|
671
|
+
}
|
|
672
|
+
//#endregion
|
|
673
|
+
//#region src/components/features/logs/utils/constants.ts
|
|
674
|
+
const DEFAULT_LOG_COLUMNS = [
|
|
675
|
+
"time",
|
|
676
|
+
"severity",
|
|
677
|
+
"status",
|
|
678
|
+
"service",
|
|
679
|
+
"path",
|
|
680
|
+
"message"
|
|
681
|
+
];
|
|
682
|
+
const CANONICAL_FACET_NAMES = [
|
|
683
|
+
"severity",
|
|
684
|
+
"service_name",
|
|
685
|
+
"resource_name"
|
|
686
|
+
];
|
|
687
|
+
const FACET_LABELS = {
|
|
688
|
+
severity: "Severity",
|
|
689
|
+
service_name: "Service",
|
|
690
|
+
resource_name: "Resource"
|
|
691
|
+
};
|
|
692
|
+
const SEVERITY_ORDER = [
|
|
693
|
+
"ERROR",
|
|
694
|
+
"FATAL",
|
|
695
|
+
"WARN",
|
|
696
|
+
"WARNING",
|
|
697
|
+
"INFO",
|
|
698
|
+
"DEBUG",
|
|
699
|
+
"TRACE"
|
|
700
|
+
];
|
|
701
|
+
const DEFAULT_LOGQL_MATCHER = "{service_name=~\".+\"}";
|
|
702
|
+
//#endregion
|
|
703
|
+
//#region src/components/features/logs/utils/facets.ts
|
|
704
|
+
function facetLabel(name) {
|
|
705
|
+
return FACET_LABELS[name] ?? name.replaceAll("_", " ");
|
|
706
|
+
}
|
|
707
|
+
function compareFacetValues(name, a, b) {
|
|
708
|
+
if (name === "severity") {
|
|
709
|
+
const aIndex = SEVERITY_ORDER.indexOf(a);
|
|
710
|
+
const bIndex = SEVERITY_ORDER.indexOf(b);
|
|
711
|
+
const aRank = aIndex === -1 ? SEVERITY_ORDER.length : aIndex;
|
|
712
|
+
const bRank = bIndex === -1 ? SEVERITY_ORDER.length : bIndex;
|
|
713
|
+
if (aRank !== bRank) return aRank - bRank;
|
|
714
|
+
}
|
|
715
|
+
return a.localeCompare(b);
|
|
716
|
+
}
|
|
717
|
+
function compareFacetNames(a, b) {
|
|
718
|
+
const aIndex = CANONICAL_FACET_NAMES.indexOf(a);
|
|
719
|
+
const bIndex = CANONICAL_FACET_NAMES.indexOf(b);
|
|
720
|
+
const aRank = aIndex === -1 ? CANONICAL_FACET_NAMES.length : aIndex;
|
|
721
|
+
const bRank = bIndex === -1 ? CANONICAL_FACET_NAMES.length : bIndex;
|
|
722
|
+
if (aRank !== bRank) return aRank - bRank;
|
|
723
|
+
return a.localeCompare(b);
|
|
724
|
+
}
|
|
725
|
+
function facetsFromEntries(entries, names = CANONICAL_FACET_NAMES) {
|
|
726
|
+
const allow = new Set(names);
|
|
727
|
+
const counts = /* @__PURE__ */ new Map();
|
|
728
|
+
for (const entry of entries) for (const [name, value] of Object.entries(entry.labels)) {
|
|
729
|
+
if (!value || !allow.has(name)) continue;
|
|
730
|
+
let values = counts.get(name);
|
|
731
|
+
if (!values) {
|
|
732
|
+
values = /* @__PURE__ */ new Map();
|
|
733
|
+
counts.set(name, values);
|
|
734
|
+
}
|
|
735
|
+
values.set(value, (values.get(value) ?? 0) + 1);
|
|
736
|
+
}
|
|
737
|
+
return [...counts.keys()].sort(compareFacetNames).map((name) => {
|
|
738
|
+
const values = counts.get(name) ?? /* @__PURE__ */ new Map();
|
|
739
|
+
return {
|
|
740
|
+
name,
|
|
741
|
+
label: facetLabel(name),
|
|
742
|
+
options: [...values.entries()].sort(([a], [b]) => compareFacetValues(name, a, b)).map(([value, count]) => ({
|
|
743
|
+
value,
|
|
744
|
+
count
|
|
745
|
+
}))
|
|
746
|
+
};
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
function filterEntries(entries, filters, search = "") {
|
|
750
|
+
const query = search.trim().toLowerCase();
|
|
751
|
+
const active = Object.entries(filters).filter(([, values]) => values.length > 0);
|
|
752
|
+
return entries.filter((entry) => {
|
|
753
|
+
for (const [name, values] of active) {
|
|
754
|
+
const label = entry.labels[name];
|
|
755
|
+
if (!label || !values.includes(label)) return false;
|
|
756
|
+
}
|
|
757
|
+
if (query && !entry.line.toLowerCase().includes(query)) return false;
|
|
758
|
+
return true;
|
|
759
|
+
});
|
|
760
|
+
}
|
|
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
|
+
};
|
|
769
|
+
}
|
|
770
|
+
function filtersAreActive(filters) {
|
|
771
|
+
return Object.values(filters).some((values) => values.length > 0);
|
|
772
|
+
}
|
|
773
|
+
//#endregion
|
|
774
|
+
//#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 }) {
|
|
776
|
+
const [currentTimeRange, setTimeRange] = useControllableState({
|
|
777
|
+
value: timeRange,
|
|
778
|
+
defaultValue: defaultTimeRange ?? lastThirtyMinutes(),
|
|
779
|
+
onChange: onTimeRangeChange
|
|
780
|
+
});
|
|
781
|
+
const [currentFilters, setFilters] = useControllableState({
|
|
782
|
+
value: filters,
|
|
783
|
+
defaultValue: defaultFilters ?? {},
|
|
784
|
+
onChange: onFiltersChange
|
|
785
|
+
});
|
|
786
|
+
const [currentSearch, setSearch] = useControllableState({
|
|
787
|
+
value: search,
|
|
788
|
+
defaultValue: defaultSearch,
|
|
789
|
+
onChange: onSearchChange
|
|
790
|
+
});
|
|
791
|
+
const [currentLive, setLive] = useControllableState({
|
|
792
|
+
value: live,
|
|
793
|
+
defaultValue: defaultLive,
|
|
794
|
+
onChange: onLiveChange
|
|
795
|
+
});
|
|
796
|
+
const [currentSelectedId, setSelectedId] = useControllableState({
|
|
797
|
+
value: selectedId,
|
|
798
|
+
defaultValue: defaultSelectedId,
|
|
799
|
+
onChange: onSelectedIdChange
|
|
800
|
+
});
|
|
801
|
+
const resolvedFacets = useMemo(() => facets ?? facetsFromEntries(entries), [facets, entries]);
|
|
802
|
+
const selectedIndex = useMemo(() => entries.findIndex((entry) => entry.id === currentSelectedId), [entries, currentSelectedId]);
|
|
803
|
+
const selectedEntry = selectedIndex >= 0 ? entries[selectedIndex] ?? null : null;
|
|
804
|
+
const toggleFilterValue = useCallback((name, value) => {
|
|
805
|
+
const current = currentFilters[name] ?? [];
|
|
806
|
+
const nextValues = current.includes(value) ? current.filter((item) => item !== value) : [...current, value];
|
|
807
|
+
const next = { ...currentFilters };
|
|
808
|
+
if (nextValues.length === 0) delete next[name];
|
|
809
|
+
else next[name] = nextValues;
|
|
810
|
+
setFilters(next);
|
|
811
|
+
}, [currentFilters, setFilters]);
|
|
812
|
+
const resetFilters = useCallback(() => {
|
|
813
|
+
setFilters({});
|
|
814
|
+
}, [setFilters]);
|
|
815
|
+
const selectPrevious = useCallback(() => {
|
|
816
|
+
if (entries.length === 0) return;
|
|
817
|
+
if (selectedIndex <= 0) setSelectedId(entries[0].id);
|
|
818
|
+
else setSelectedId(entries[selectedIndex - 1].id);
|
|
819
|
+
}, [
|
|
820
|
+
entries,
|
|
821
|
+
selectedIndex,
|
|
822
|
+
setSelectedId
|
|
823
|
+
]);
|
|
824
|
+
const selectNext = useCallback(() => {
|
|
825
|
+
if (entries.length === 0) return;
|
|
826
|
+
if (selectedIndex < 0) setSelectedId(entries[0].id);
|
|
827
|
+
else if (selectedIndex < entries.length - 1) setSelectedId(entries[selectedIndex + 1].id);
|
|
828
|
+
}, [
|
|
829
|
+
entries,
|
|
830
|
+
selectedIndex,
|
|
831
|
+
setSelectedId
|
|
832
|
+
]);
|
|
833
|
+
const value = useMemo(() => ({
|
|
834
|
+
entries,
|
|
835
|
+
facets: resolvedFacets,
|
|
836
|
+
timeRange: currentTimeRange,
|
|
837
|
+
setTimeRange,
|
|
838
|
+
filters: currentFilters,
|
|
839
|
+
setFilters,
|
|
840
|
+
toggleFilterValue,
|
|
841
|
+
resetFilters,
|
|
842
|
+
search: currentSearch,
|
|
843
|
+
setSearch,
|
|
844
|
+
live: currentLive,
|
|
845
|
+
setLive,
|
|
846
|
+
selectedId: currentSelectedId,
|
|
847
|
+
setSelectedId,
|
|
848
|
+
selectedEntry,
|
|
849
|
+
selectedIndex,
|
|
850
|
+
selectPrevious,
|
|
851
|
+
selectNext,
|
|
852
|
+
isLoading,
|
|
853
|
+
error,
|
|
854
|
+
columns,
|
|
855
|
+
hasActiveFilters: filtersAreActive(currentFilters),
|
|
856
|
+
onRefresh,
|
|
857
|
+
onExport
|
|
858
|
+
}), [
|
|
859
|
+
entries,
|
|
860
|
+
resolvedFacets,
|
|
861
|
+
currentTimeRange,
|
|
862
|
+
setTimeRange,
|
|
863
|
+
currentFilters,
|
|
864
|
+
setFilters,
|
|
865
|
+
toggleFilterValue,
|
|
866
|
+
resetFilters,
|
|
867
|
+
currentSearch,
|
|
868
|
+
setSearch,
|
|
869
|
+
currentLive,
|
|
870
|
+
setLive,
|
|
871
|
+
currentSelectedId,
|
|
872
|
+
setSelectedId,
|
|
873
|
+
selectedEntry,
|
|
874
|
+
selectedIndex,
|
|
875
|
+
selectPrevious,
|
|
876
|
+
selectNext,
|
|
877
|
+
isLoading,
|
|
878
|
+
error,
|
|
879
|
+
columns,
|
|
880
|
+
onRefresh,
|
|
881
|
+
onExport
|
|
882
|
+
]);
|
|
883
|
+
return /* @__PURE__ */ jsx(LogsContext, {
|
|
884
|
+
value,
|
|
885
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
886
|
+
"data-slot": "logs-root",
|
|
887
|
+
className: cn("flex h-full min-h-0 w-full flex-col", className),
|
|
888
|
+
children
|
|
889
|
+
})
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
//#endregion
|
|
893
|
+
//#region src/components/features/logs/logs.tsx
|
|
894
|
+
const Logs = {
|
|
895
|
+
Root: LogsRoot,
|
|
896
|
+
Explorer: LogsExplorer,
|
|
897
|
+
Filters: LogsFilters,
|
|
898
|
+
FilterGroup: LogsFilterGroup,
|
|
899
|
+
TimeRangeFilter: LogsTimeRangeFilter,
|
|
900
|
+
Toolbar: LogsToolbar,
|
|
901
|
+
Search: LogsSearch,
|
|
902
|
+
LiveToggle: LogsLiveToggle,
|
|
903
|
+
Table: LogsTable,
|
|
904
|
+
Detail: LogsDetail
|
|
905
|
+
};
|
|
906
|
+
//#endregion
|
|
907
|
+
//#region src/components/features/logs/utils/build-logql.ts
|
|
908
|
+
/** Loki/Prometheus label names. Anything else is dropped, not interpolated. */
|
|
909
|
+
const LOKI_LABEL_NAME = /^[a-z_]\w*$/i;
|
|
910
|
+
function escapeQuoted(value) {
|
|
911
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
912
|
+
}
|
|
913
|
+
function escapeRegex(value) {
|
|
914
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
915
|
+
}
|
|
916
|
+
function matcherClause(label, values) {
|
|
917
|
+
if (!LOKI_LABEL_NAME.test(label)) return null;
|
|
918
|
+
const unique = [...new Set(values.map((v) => v.trim()).filter(Boolean))];
|
|
919
|
+
if (unique.length === 0) return null;
|
|
920
|
+
if (unique.length === 1) return `${label}="${escapeQuoted(unique[0])}"`;
|
|
921
|
+
return `${label}=~"${escapeQuoted(unique.map(escapeRegex).join("|"))}"`;
|
|
922
|
+
}
|
|
923
|
+
function selectorFromMatchers(matchers) {
|
|
924
|
+
if (!matchers) return DEFAULT_LOGQL_MATCHER;
|
|
925
|
+
const clauses = Object.entries(matchers).map(([label, values]) => matcherClause(label, values)).filter((clause) => clause !== null);
|
|
926
|
+
if (clauses.length === 0) return DEFAULT_LOGQL_MATCHER;
|
|
927
|
+
return `{${clauses.join(", ")}}`;
|
|
928
|
+
}
|
|
929
|
+
function buildLogQL({ matchers, lineContains } = {}) {
|
|
930
|
+
const selector = selectorFromMatchers(matchers);
|
|
931
|
+
const text = lineContains?.trim();
|
|
932
|
+
if (!text) return selector;
|
|
933
|
+
return `${selector} |= "${escapeQuoted(text)}"`;
|
|
934
|
+
}
|
|
935
|
+
//#endregion
|
|
936
|
+
//#region src/components/features/logs/utils/flatten-loki.ts
|
|
937
|
+
function nsToDate(timestampNs) {
|
|
938
|
+
try {
|
|
939
|
+
const ms = BigInt(timestampNs) / 1000000n;
|
|
940
|
+
return new Date(Number(ms));
|
|
941
|
+
} catch {
|
|
942
|
+
return /* @__PURE__ */ new Date(NaN);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
function flattenStream(stream, streamIndex) {
|
|
946
|
+
const labels = stream.stream ?? {};
|
|
947
|
+
const values = stream.values ?? [];
|
|
948
|
+
const entries = [];
|
|
949
|
+
for (const [valueIndex, pair] of values.entries()) {
|
|
950
|
+
const timestampNs = pair[0];
|
|
951
|
+
const line = pair[1];
|
|
952
|
+
if (!timestampNs || line === void 0) continue;
|
|
953
|
+
const timestamp = nsToDate(timestampNs);
|
|
954
|
+
if (Number.isNaN(timestamp.getTime())) continue;
|
|
955
|
+
entries.push({
|
|
956
|
+
id: `${timestampNs}-${streamIndex}-${valueIndex}`,
|
|
957
|
+
timestamp,
|
|
958
|
+
timestampNs,
|
|
959
|
+
line,
|
|
960
|
+
labels: { ...labels }
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
return entries;
|
|
964
|
+
}
|
|
965
|
+
function flattenLokiStreams(response) {
|
|
966
|
+
if (response.status !== "success" || response.data?.resultType !== "streams" || !response.data.result) return [];
|
|
967
|
+
const entries = response.data.result.flatMap(flattenStream);
|
|
968
|
+
entries.sort((a, b) => {
|
|
969
|
+
try {
|
|
970
|
+
const delta = BigInt(b.timestampNs) - BigInt(a.timestampNs);
|
|
971
|
+
if (delta === 0n) return a.id < b.id ? 1 : -1;
|
|
972
|
+
return delta > 0n ? 1 : -1;
|
|
973
|
+
} catch {
|
|
974
|
+
return a.timestampNs < b.timestampNs ? 1 : -1;
|
|
975
|
+
}
|
|
976
|
+
});
|
|
977
|
+
return entries;
|
|
978
|
+
}
|
|
979
|
+
//#endregion
|
|
980
|
+
export { Logs, buildLogQL, facetsFromEntries, filterEntries, flattenLokiStreams, httpStatusBadgeType, httpStatusTextClass, lastThirtyMinutes, logLineDisplay, nsToDate, parseLogLine, severityBadgeType, useLogs };
|