@flanksource/clicky-ui 0.2.4 → 0.2.6
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/components/Combobox.cjs +291 -0
- package/dist/components/Combobox.cjs.map +1 -0
- package/dist/components/Combobox.d.ts +59 -0
- package/dist/components/Combobox.d.ts.map +1 -0
- package/dist/components/Combobox.js +291 -0
- package/dist/components/Combobox.js.map +1 -0
- package/dist/components/FilterBar.cjs +153 -124
- package/dist/components/FilterBar.cjs.map +1 -1
- package/dist/components/FilterBar.d.ts +25 -1
- package/dist/components/FilterBar.d.ts.map +1 -1
- package/dist/components/FilterBar.js +153 -124
- package/dist/components/FilterBar.js.map +1 -1
- package/dist/components/FormatOptionsDropdown.cjs +62 -0
- package/dist/components/FormatOptionsDropdown.cjs.map +1 -0
- package/dist/components/FormatOptionsDropdown.d.ts +37 -0
- package/dist/components/FormatOptionsDropdown.d.ts.map +1 -0
- package/dist/components/FormatOptionsDropdown.js +62 -0
- package/dist/components/FormatOptionsDropdown.js.map +1 -0
- package/dist/components/SplitButton.cjs +87 -0
- package/dist/components/SplitButton.cjs.map +1 -0
- package/dist/components/SplitButton.d.ts +32 -0
- package/dist/components/SplitButton.d.ts.map +1 -0
- package/dist/components/SplitButton.js +87 -0
- package/dist/components/SplitButton.js.map +1 -0
- package/dist/components/icon-menu-picker.cjs +1 -1
- package/dist/components/icon-menu-picker.cjs.map +1 -1
- package/dist/components/icon-menu-picker.js +1 -1
- package/dist/components/icon-menu-picker.js.map +1 -1
- package/dist/components.cjs +7 -0
- package/dist/components.cjs.map +1 -1
- package/dist/components.d.ts +3 -0
- package/dist/components.d.ts.map +1 -1
- package/dist/components.js +7 -0
- package/dist/components.js.map +1 -1
- package/dist/data/Badge.d.ts +1 -1
- package/dist/data/ProgressBar.cjs +4 -2
- package/dist/data/ProgressBar.cjs.map +1 -1
- package/dist/data/ProgressBar.d.ts +1 -1
- package/dist/data/ProgressBar.d.ts.map +1 -1
- package/dist/data/ProgressBar.js +4 -2
- package/dist/data/ProgressBar.js.map +1 -1
- package/dist/data/StatusBreakdown.cjs +80 -0
- package/dist/data/StatusBreakdown.cjs.map +1 -0
- package/dist/data/StatusBreakdown.d.ts +36 -0
- package/dist/data/StatusBreakdown.d.ts.map +1 -0
- package/dist/data/StatusBreakdown.js +80 -0
- package/dist/data/StatusBreakdown.js.map +1 -0
- package/dist/data/TaskManager.cjs +162 -0
- package/dist/data/TaskManager.cjs.map +1 -0
- package/dist/data/TaskManager.d.ts +21 -0
- package/dist/data/TaskManager.d.ts.map +1 -0
- package/dist/data/TaskManager.js +162 -0
- package/dist/data/TaskManager.js.map +1 -0
- package/dist/data/TaskProgress.cjs +125 -0
- package/dist/data/TaskProgress.cjs.map +1 -0
- package/dist/data/TaskProgress.d.ts +20 -0
- package/dist/data/TaskProgress.d.ts.map +1 -0
- package/dist/data/TaskProgress.js +125 -0
- package/dist/data/TaskProgress.js.map +1 -0
- package/dist/data/TaskSnapshot.cjs +23 -0
- package/dist/data/TaskSnapshot.cjs.map +1 -0
- package/dist/data/TaskSnapshot.d.ts +49 -0
- package/dist/data/TaskSnapshot.d.ts.map +1 -0
- package/dist/data/TaskSnapshot.js +23 -0
- package/dist/data/TaskSnapshot.js.map +1 -0
- package/dist/data/TimeseriesGauge.cjs +144 -0
- package/dist/data/TimeseriesGauge.cjs.map +1 -0
- package/dist/data/TimeseriesGauge.d.ts +56 -0
- package/dist/data/TimeseriesGauge.d.ts.map +1 -0
- package/dist/data/TimeseriesGauge.js +144 -0
- package/dist/data/TimeseriesGauge.js.map +1 -0
- package/dist/data/TimeseriesPanel.axes.cjs +39 -0
- package/dist/data/TimeseriesPanel.axes.cjs.map +1 -0
- package/dist/data/TimeseriesPanel.axes.d.ts +28 -0
- package/dist/data/TimeseriesPanel.axes.d.ts.map +1 -0
- package/dist/data/TimeseriesPanel.axes.js +39 -0
- package/dist/data/TimeseriesPanel.axes.js.map +1 -0
- package/dist/data/TimeseriesPanel.cjs +324 -0
- package/dist/data/TimeseriesPanel.cjs.map +1 -0
- package/dist/data/TimeseriesPanel.d.ts +141 -0
- package/dist/data/TimeseriesPanel.d.ts.map +1 -0
- package/dist/data/TimeseriesPanel.js +324 -0
- package/dist/data/TimeseriesPanel.js.map +1 -0
- package/dist/data/Tree.cjs.map +1 -1
- package/dist/data/Tree.d.ts.map +1 -1
- package/dist/data/Tree.js.map +1 -1
- package/dist/data/TreeNode.cjs +44 -7
- package/dist/data/TreeNode.cjs.map +1 -1
- package/dist/data/TreeNode.d.ts +19 -1
- package/dist/data/TreeNode.d.ts.map +1 -1
- package/dist/data/TreeNode.js +44 -7
- package/dist/data/TreeNode.js.map +1 -1
- package/dist/data/diagnostics/DiagnosticsDetailPanel.cjs +66 -44
- package/dist/data/diagnostics/DiagnosticsDetailPanel.cjs.map +1 -1
- package/dist/data/diagnostics/DiagnosticsDetailPanel.d.ts +21 -1
- package/dist/data/diagnostics/DiagnosticsDetailPanel.d.ts.map +1 -1
- package/dist/data/diagnostics/DiagnosticsDetailPanel.js +65 -43
- package/dist/data/diagnostics/DiagnosticsDetailPanel.js.map +1 -1
- package/dist/data/diagnostics/DiagnosticsTree.cjs +2 -1
- package/dist/data/diagnostics/DiagnosticsTree.cjs.map +1 -1
- package/dist/data/diagnostics/DiagnosticsTree.js +2 -1
- package/dist/data/diagnostics/DiagnosticsTree.js.map +1 -1
- package/dist/data/diagnostics/FrameSourceWindow.cjs +37 -0
- package/dist/data/diagnostics/FrameSourceWindow.cjs.map +1 -0
- package/dist/data/diagnostics/FrameSourceWindow.d.ts +15 -0
- package/dist/data/diagnostics/FrameSourceWindow.d.ts.map +1 -0
- package/dist/data/diagnostics/FrameSourceWindow.js +37 -0
- package/dist/data/diagnostics/FrameSourceWindow.js.map +1 -0
- package/dist/data/diagnostics/JvmStackTrace.cjs +25 -12
- package/dist/data/diagnostics/JvmStackTrace.cjs.map +1 -1
- package/dist/data/diagnostics/JvmStackTrace.d.ts +8 -1
- package/dist/data/diagnostics/JvmStackTrace.d.ts.map +1 -1
- package/dist/data/diagnostics/JvmStackTrace.js +26 -13
- package/dist/data/diagnostics/JvmStackTrace.js.map +1 -1
- package/dist/data/diagnostics/ThreadCard.cjs +9 -2
- package/dist/data/diagnostics/ThreadCard.cjs.map +1 -1
- package/dist/data/diagnostics/ThreadCard.d.ts +3 -1
- package/dist/data/diagnostics/ThreadCard.d.ts.map +1 -1
- package/dist/data/diagnostics/ThreadCard.js +9 -2
- package/dist/data/diagnostics/ThreadCard.js.map +1 -1
- package/dist/data/diagnostics/jvm-stacktrace.cjs +10 -1
- package/dist/data/diagnostics/jvm-stacktrace.cjs.map +1 -1
- package/dist/data/diagnostics/jvm-stacktrace.d.ts.map +1 -1
- package/dist/data/diagnostics/jvm-stacktrace.js +10 -1
- package/dist/data/diagnostics/jvm-stacktrace.js.map +1 -1
- package/dist/data/diagnostics/utils.cjs +0 -12
- package/dist/data/diagnostics/utils.cjs.map +1 -1
- package/dist/data/diagnostics/utils.d.ts +1 -1
- package/dist/data/diagnostics/utils.d.ts.map +1 -1
- package/dist/data/diagnostics/utils.js +0 -12
- package/dist/data/diagnostics/utils.js.map +1 -1
- package/dist/data/task-status.cjs +108 -0
- package/dist/data/task-status.cjs.map +1 -0
- package/dist/data/task-status.d.ts +21 -0
- package/dist/data/task-status.d.ts.map +1 -0
- package/dist/data/task-status.js +108 -0
- package/dist/data/task-status.js.map +1 -0
- package/dist/data.cjs +27 -0
- package/dist/data.cjs.map +1 -1
- package/dist/data.d.ts +9 -1
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +27 -0
- package/dist/data.js.map +1 -1
- package/dist/hooks/use-task-run.cjs +137 -0
- package/dist/hooks/use-task-run.cjs.map +1 -0
- package/dist/hooks/use-task-run.d.ts +35 -0
- package/dist/hooks/use-task-run.d.ts.map +1 -0
- package/dist/hooks/use-task-run.js +137 -0
- package/dist/hooks/use-task-run.js.map +1 -0
- package/dist/hooks.cjs +3 -0
- package/dist/hooks.cjs.map +1 -1
- package/dist/hooks.d.ts +1 -0
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +3 -0
- package/dist/hooks.js.map +1 -1
- package/dist/index.cjs +43 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/color.cjs +33 -0
- package/dist/lib/color.cjs.map +1 -0
- package/dist/lib/color.d.ts +10 -0
- package/dist/lib/color.d.ts.map +1 -0
- package/dist/lib/color.js +33 -0
- package/dist/lib/color.js.map +1 -0
- package/dist/lib/format.cjs +77 -0
- package/dist/lib/format.cjs.map +1 -0
- package/dist/lib/format.d.ts +30 -0
- package/dist/lib/format.d.ts.map +1 -0
- package/dist/lib/format.js +77 -0
- package/dist/lib/format.js.map +1 -0
- package/dist/overlay/DropdownMenu.cjs +18 -2
- package/dist/overlay/DropdownMenu.cjs.map +1 -1
- package/dist/overlay/DropdownMenu.d.ts +3 -1
- package/dist/overlay/DropdownMenu.d.ts.map +1 -1
- package/dist/overlay/DropdownMenu.js +18 -2
- package/dist/overlay/DropdownMenu.js.map +1 -1
- package/dist/rpc/types.cjs.map +1 -1
- package/dist/rpc/types.d.ts +4 -0
- package/dist/rpc/types.d.ts.map +1 -1
- package/dist/rpc/types.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/utils.cjs +6 -0
- package/dist/utils.cjs.map +1 -1
- package/dist/utils.d.ts +1 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +6 -0
- package/dist/utils.js.map +1 -1
- package/package.json +4 -1
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useQueries } from "@tanstack/react-query";
|
|
3
|
+
import { useState, useMemo } from "react";
|
|
4
|
+
import { CartesianGrid, XAxis, YAxis, Tooltip, ReferenceLine, ResponsiveContainer, LineChart, Line, AreaChart, Area } from "recharts";
|
|
5
|
+
import { cn } from "../lib/utils.js";
|
|
6
|
+
import { resolveCssColor } from "../lib/color.js";
|
|
7
|
+
import { formatUnit } from "../lib/format.js";
|
|
8
|
+
import { Modal } from "../overlay/Modal.js";
|
|
9
|
+
import { ProgressBar } from "./ProgressBar.js";
|
|
10
|
+
import { Icon } from "./Icon.js";
|
|
11
|
+
import { latestOf, assignAxes } from "./TimeseriesPanel.axes.js";
|
|
12
|
+
import { UiFullscreen } from "../icons/components/UiFullscreen.js";
|
|
13
|
+
const SERIES_PALETTE = [
|
|
14
|
+
"var(--chart-1, #3b82f6)",
|
|
15
|
+
"var(--chart-2, #ef4444)",
|
|
16
|
+
"var(--chart-3, #10b981)",
|
|
17
|
+
"var(--chart-4, #f59e0b)"
|
|
18
|
+
];
|
|
19
|
+
function paletteColor(index) {
|
|
20
|
+
return SERIES_PALETTE[index % SERIES_PALETTE.length] ?? SERIES_PALETTE[0];
|
|
21
|
+
}
|
|
22
|
+
function resolveSeries(props) {
|
|
23
|
+
const base = props.baseUrl ?? "";
|
|
24
|
+
const list = props.series ?? (props.url ? [{ id: props.url }] : []);
|
|
25
|
+
return list.map((s, i) => ({
|
|
26
|
+
key: s.id,
|
|
27
|
+
url: base + s.id,
|
|
28
|
+
label: s.label ?? s.id,
|
|
29
|
+
color: s.color ?? paletteColor(i),
|
|
30
|
+
...s.unit ?? props.unit ? { unit: s.unit ?? props.unit } : {},
|
|
31
|
+
...s.transform ? { transform: s.transform } : {},
|
|
32
|
+
...s.current !== void 0 ? { current: s.current } : {}
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
function resolveBreakdown(series, rows, total) {
|
|
36
|
+
const withValue = series.map((s) => ({ s, value: s.current ?? latestOf(rows, s.key) ?? 0 }));
|
|
37
|
+
const denom = total ?? withValue.reduce((acc, w) => acc + w.value, 0);
|
|
38
|
+
const items = withValue.map(({ s, value }) => ({
|
|
39
|
+
key: s.key,
|
|
40
|
+
label: s.label,
|
|
41
|
+
color: s.color,
|
|
42
|
+
...s.unit ? { unit: s.unit } : {},
|
|
43
|
+
value,
|
|
44
|
+
percent: denom > 0 ? Math.min(100, Math.max(0, value / denom * 100)) : 0
|
|
45
|
+
}));
|
|
46
|
+
return { items, total: denom };
|
|
47
|
+
}
|
|
48
|
+
const defaultFetcher = async (url) => {
|
|
49
|
+
const res = await fetch(url);
|
|
50
|
+
if (!res.ok) throw new Error(`metrics request failed: ${res.status}`);
|
|
51
|
+
return res.json();
|
|
52
|
+
};
|
|
53
|
+
function formatClock(at) {
|
|
54
|
+
const date = new Date(at);
|
|
55
|
+
if (Number.isNaN(date.getTime())) return "";
|
|
56
|
+
return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
57
|
+
}
|
|
58
|
+
function mergeSeries(series, pointsBySeries) {
|
|
59
|
+
const rows = /* @__PURE__ */ new Map();
|
|
60
|
+
series.forEach((s, i) => {
|
|
61
|
+
for (const p of pointsBySeries[i] ?? []) {
|
|
62
|
+
const row = rows.get(p.at) ?? { at: p.at };
|
|
63
|
+
row[s.key] = s.transform ? s.transform(p.value) : p.value;
|
|
64
|
+
rows.set(p.at, row);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
return [...rows.values()].sort((a, b) => a.at.localeCompare(b.at));
|
|
68
|
+
}
|
|
69
|
+
function TimeseriesPanel(props) {
|
|
70
|
+
var _a;
|
|
71
|
+
const {
|
|
72
|
+
title,
|
|
73
|
+
icon,
|
|
74
|
+
unit,
|
|
75
|
+
range = "1h",
|
|
76
|
+
refreshMs = 5e3,
|
|
77
|
+
height = 160,
|
|
78
|
+
variant = "area",
|
|
79
|
+
expandable = true,
|
|
80
|
+
total,
|
|
81
|
+
fetcher = defaultFetcher,
|
|
82
|
+
className
|
|
83
|
+
} = props;
|
|
84
|
+
const isBreakdown = variant === "breakdown";
|
|
85
|
+
const chartVariant = isBreakdown ? "area" : variant;
|
|
86
|
+
const expandVariant = props.expandVariant ?? chartVariant;
|
|
87
|
+
const [expanded, setExpanded] = useState(false);
|
|
88
|
+
const series = useMemo(() => resolveSeries(props), [props]);
|
|
89
|
+
const requestUrls = useMemo(
|
|
90
|
+
() => series.map((s) => {
|
|
91
|
+
const u = new URL(s.url, window.location.origin);
|
|
92
|
+
if (range) u.searchParams.set("since", range);
|
|
93
|
+
return u.pathname + u.search;
|
|
94
|
+
}),
|
|
95
|
+
[series, range]
|
|
96
|
+
);
|
|
97
|
+
const results = useQueries({
|
|
98
|
+
queries: requestUrls.map((requestUrl) => ({
|
|
99
|
+
queryKey: ["timeseries", requestUrl],
|
|
100
|
+
queryFn: () => fetcher(requestUrl),
|
|
101
|
+
refetchInterval: refreshMs > 0 ? refreshMs : false,
|
|
102
|
+
staleTime: 0,
|
|
103
|
+
retry: 0
|
|
104
|
+
}))
|
|
105
|
+
});
|
|
106
|
+
const isError = results.some((r) => r.isError);
|
|
107
|
+
const error = (_a = results.find((r) => r.isError)) == null ? void 0 : _a.error;
|
|
108
|
+
const isLoading = results.some((r) => r.isLoading) && results.every((r) => !r.data);
|
|
109
|
+
const pointsBySeries = results.map((r) => {
|
|
110
|
+
var _a2;
|
|
111
|
+
return (_a2 = r.data) == null ? void 0 : _a2.points;
|
|
112
|
+
});
|
|
113
|
+
const rows = useMemo(() => mergeSeries(series, pointsBySeries), [series, pointsBySeries]);
|
|
114
|
+
const hasMirrored = series.some((s) => s.transform);
|
|
115
|
+
const breakdown = useMemo(
|
|
116
|
+
() => isBreakdown ? resolveBreakdown(series, rows, total) : null,
|
|
117
|
+
[isBreakdown, series, rows, total]
|
|
118
|
+
);
|
|
119
|
+
const canExpand = expandable && rows.length >= 2;
|
|
120
|
+
return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col rounded-lg border border-border bg-card p-3", className), children: [
|
|
121
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2 text-xs text-muted-foreground", children: [
|
|
122
|
+
/* @__PURE__ */ jsxs("span", { className: "inline-flex min-w-0 items-center gap-1.5", children: [
|
|
123
|
+
icon ? /* @__PURE__ */ jsx(PanelIcon, { icon }) : null,
|
|
124
|
+
/* @__PURE__ */ jsx("span", { className: "truncate font-medium text-foreground", children: title })
|
|
125
|
+
] }),
|
|
126
|
+
/* @__PURE__ */ jsxs("span", { className: "inline-flex shrink-0 items-center gap-1.5", children: [
|
|
127
|
+
breakdown ? /* @__PURE__ */ jsx(BreakdownReadout, { total: breakdown.total, unit, items: breakdown.items }) : /* @__PURE__ */ jsx(LatestReadout, { series, rows }),
|
|
128
|
+
canExpand ? /* @__PURE__ */ jsx(
|
|
129
|
+
"button",
|
|
130
|
+
{
|
|
131
|
+
type: "button",
|
|
132
|
+
"aria-label": "Expand chart",
|
|
133
|
+
onClick: () => setExpanded(true),
|
|
134
|
+
className: "text-muted-foreground transition-colors hover:text-foreground",
|
|
135
|
+
children: /* @__PURE__ */ jsx(Icon, { icon: UiFullscreen, width: 14, height: 14 })
|
|
136
|
+
}
|
|
137
|
+
) : null
|
|
138
|
+
] })
|
|
139
|
+
] }),
|
|
140
|
+
/* @__PURE__ */ jsx("div", { className: "mt-2", style: breakdown ? void 0 : { height }, children: breakdown ? /* @__PURE__ */ jsx(BreakdownView, { total: breakdown.total, unit, items: breakdown.items }) : isError ? /* @__PURE__ */ jsx(PanelMessage, { children: (error == null ? void 0 : error.message) ?? "Failed to load metric" }) : isLoading ? /* @__PURE__ */ jsx(PanelMessage, { children: "Loading…" }) : rows.length < 2 ? /* @__PURE__ */ jsx(PanelMessage, { children: "Collecting data…" }) : /* @__PURE__ */ jsx(Chart, { rows, series, variant: chartVariant, unit, showZeroLine: hasMirrored }) }),
|
|
141
|
+
!breakdown && !isError && !isLoading && rows.length >= 2 && series.length > 1 ? /* @__PURE__ */ jsx(ChartLegend, { series, rows }) : null,
|
|
142
|
+
canExpand ? /* @__PURE__ */ jsxs(Modal, { open: expanded, onClose: () => setExpanded(false), title, size: "xl", children: [
|
|
143
|
+
/* @__PURE__ */ jsx("div", { style: { height: 360 }, children: /* @__PURE__ */ jsx(Chart, { rows, series, variant: expandVariant, unit, showZeroLine: hasMirrored }) }),
|
|
144
|
+
series.length > 1 ? /* @__PURE__ */ jsx(ChartLegend, { series, rows }) : null
|
|
145
|
+
] }) : null
|
|
146
|
+
] });
|
|
147
|
+
}
|
|
148
|
+
function LatestReadout({ series, rows }) {
|
|
149
|
+
const only = series.length === 1 ? series[0] : void 0;
|
|
150
|
+
if (!only) return null;
|
|
151
|
+
const v = latestOf(rows, only.key);
|
|
152
|
+
if (v === void 0) return null;
|
|
153
|
+
return /* @__PURE__ */ jsx("span", { className: "tabular-nums", children: formatUnit(Math.abs(v), only.unit) });
|
|
154
|
+
}
|
|
155
|
+
function ChartLegend({ series, rows }) {
|
|
156
|
+
return /* @__PURE__ */ jsx("ul", { className: "mt-1.5 flex flex-wrap gap-x-3 gap-y-1 text-xs text-muted-foreground", children: series.map((s) => {
|
|
157
|
+
const v = latestOf(rows, s.key);
|
|
158
|
+
return /* @__PURE__ */ jsxs("li", { className: "inline-flex min-w-0 items-center gap-1.5", children: [
|
|
159
|
+
/* @__PURE__ */ jsx(
|
|
160
|
+
"span",
|
|
161
|
+
{
|
|
162
|
+
className: "h-2 w-2 shrink-0 rounded-full",
|
|
163
|
+
style: { backgroundColor: resolveCssColor(s.color) },
|
|
164
|
+
"aria-hidden": true
|
|
165
|
+
}
|
|
166
|
+
),
|
|
167
|
+
/* @__PURE__ */ jsx("span", { className: "truncate text-foreground", children: s.label }),
|
|
168
|
+
v !== void 0 ? /* @__PURE__ */ jsx("span", { className: "tabular-nums", children: formatUnit(Math.abs(v), s.unit) }) : null
|
|
169
|
+
] }, s.key);
|
|
170
|
+
}) });
|
|
171
|
+
}
|
|
172
|
+
function BreakdownReadout({
|
|
173
|
+
total,
|
|
174
|
+
unit,
|
|
175
|
+
items
|
|
176
|
+
}) {
|
|
177
|
+
const used = items.reduce((acc, it) => acc + it.value, 0);
|
|
178
|
+
const pct = total > 0 ? Math.min(100, used / total * 100) : 0;
|
|
179
|
+
return /* @__PURE__ */ jsxs("span", { className: "font-mono tabular-nums", children: [
|
|
180
|
+
formatUnit(used, unit),
|
|
181
|
+
" / ",
|
|
182
|
+
formatUnit(total, unit),
|
|
183
|
+
" · ",
|
|
184
|
+
pct.toFixed(0),
|
|
185
|
+
"%"
|
|
186
|
+
] });
|
|
187
|
+
}
|
|
188
|
+
function BreakdownView({
|
|
189
|
+
total,
|
|
190
|
+
unit,
|
|
191
|
+
items
|
|
192
|
+
}) {
|
|
193
|
+
const segments = items.map((it) => ({
|
|
194
|
+
count: it.value,
|
|
195
|
+
color: it.color,
|
|
196
|
+
label: it.label
|
|
197
|
+
}));
|
|
198
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
|
|
199
|
+
/* @__PURE__ */ jsx(ProgressBar, { segments, total: total > 0 ? total : 1, height: "h-3" }),
|
|
200
|
+
items.length > 0 ? /* @__PURE__ */ jsx("ul", { className: "grid grid-cols-1 gap-y-1 text-xs", children: items.map((it) => /* @__PURE__ */ jsxs("li", { className: "flex items-center gap-2", children: [
|
|
201
|
+
/* @__PURE__ */ jsx(
|
|
202
|
+
"span",
|
|
203
|
+
{
|
|
204
|
+
className: "h-2 w-2 shrink-0 rounded-full",
|
|
205
|
+
style: { backgroundColor: resolveCssColor(it.color) },
|
|
206
|
+
"aria-hidden": true
|
|
207
|
+
}
|
|
208
|
+
),
|
|
209
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate font-mono text-muted-foreground", children: it.label }),
|
|
210
|
+
/* @__PURE__ */ jsx("span", { className: "font-mono tabular-nums text-foreground", children: formatUnit(it.value, it.unit ?? unit) }),
|
|
211
|
+
/* @__PURE__ */ jsxs("span", { className: "w-10 text-right font-mono tabular-nums text-muted-foreground", children: [
|
|
212
|
+
it.percent.toFixed(0),
|
|
213
|
+
"%"
|
|
214
|
+
] })
|
|
215
|
+
] }, it.key)) }) : null
|
|
216
|
+
] });
|
|
217
|
+
}
|
|
218
|
+
function Chart({
|
|
219
|
+
rows,
|
|
220
|
+
series,
|
|
221
|
+
variant,
|
|
222
|
+
unit,
|
|
223
|
+
showZeroLine
|
|
224
|
+
}) {
|
|
225
|
+
const stacked = variant === "stacked";
|
|
226
|
+
const seriesByKey = new Map(series.map((s) => [s.key, s]));
|
|
227
|
+
const strokeOf = (s) => resolveCssColor(s.color);
|
|
228
|
+
const { axes: yAxes, axisOfKey } = useMemo(() => assignAxes(series), [series]);
|
|
229
|
+
const axisOf = (key) => axisOfKey.get(key) ?? "left";
|
|
230
|
+
const axes = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
231
|
+
/* @__PURE__ */ jsx(CartesianGrid, { strokeOpacity: 0.12, vertical: false }),
|
|
232
|
+
/* @__PURE__ */ jsx(
|
|
233
|
+
XAxis,
|
|
234
|
+
{
|
|
235
|
+
dataKey: "at",
|
|
236
|
+
tickFormatter: formatClock,
|
|
237
|
+
tick: { fontSize: 10 },
|
|
238
|
+
stroke: "currentColor",
|
|
239
|
+
strokeOpacity: 0.35,
|
|
240
|
+
minTickGap: 32
|
|
241
|
+
}
|
|
242
|
+
),
|
|
243
|
+
yAxes.map((ax) => /* @__PURE__ */ jsx(
|
|
244
|
+
YAxis,
|
|
245
|
+
{
|
|
246
|
+
yAxisId: ax.id,
|
|
247
|
+
orientation: ax.orientation,
|
|
248
|
+
tick: { fontSize: 10 },
|
|
249
|
+
stroke: "currentColor",
|
|
250
|
+
strokeOpacity: 0.35,
|
|
251
|
+
width: 40,
|
|
252
|
+
tickFormatter: (v) => formatUnit(Math.abs(v), ax.unit)
|
|
253
|
+
},
|
|
254
|
+
ax.id
|
|
255
|
+
)),
|
|
256
|
+
/* @__PURE__ */ jsx(
|
|
257
|
+
Tooltip,
|
|
258
|
+
{
|
|
259
|
+
labelFormatter: (label) => formatClock(String(label)),
|
|
260
|
+
formatter: (value, name) => {
|
|
261
|
+
const s = seriesByKey.get(String(name));
|
|
262
|
+
return [formatUnit(Math.abs(Number(value) || 0), (s == null ? void 0 : s.unit) ?? unit), (s == null ? void 0 : s.label) ?? String(name)];
|
|
263
|
+
},
|
|
264
|
+
contentStyle: { fontSize: 12 }
|
|
265
|
+
}
|
|
266
|
+
),
|
|
267
|
+
showZeroLine ? /* @__PURE__ */ jsx(ReferenceLine, { yAxisId: "left", y: 0, stroke: "currentColor", strokeOpacity: 0.35 }) : null
|
|
268
|
+
] });
|
|
269
|
+
return /* @__PURE__ */ jsx(ResponsiveContainer, { width: "100%", height: "100%", children: variant === "line" ? /* @__PURE__ */ jsxs(LineChart, { data: rows, margin: { top: 4, right: 8, bottom: 0, left: 0 }, children: [
|
|
270
|
+
axes,
|
|
271
|
+
series.map((s) => /* @__PURE__ */ jsx(
|
|
272
|
+
Line,
|
|
273
|
+
{
|
|
274
|
+
yAxisId: axisOf(s.key),
|
|
275
|
+
type: "monotone",
|
|
276
|
+
dataKey: s.key,
|
|
277
|
+
name: s.key,
|
|
278
|
+
stroke: strokeOf(s),
|
|
279
|
+
strokeWidth: 2,
|
|
280
|
+
dot: false,
|
|
281
|
+
connectNulls: true,
|
|
282
|
+
isAnimationActive: false
|
|
283
|
+
},
|
|
284
|
+
s.key
|
|
285
|
+
))
|
|
286
|
+
] }) : /* @__PURE__ */ jsxs(AreaChart, { data: rows, margin: { top: 4, right: 8, bottom: 0, left: 0 }, children: [
|
|
287
|
+
axes,
|
|
288
|
+
series.map((s) => /* @__PURE__ */ jsx(
|
|
289
|
+
Area,
|
|
290
|
+
{
|
|
291
|
+
yAxisId: axisOf(s.key),
|
|
292
|
+
type: "monotone",
|
|
293
|
+
dataKey: s.key,
|
|
294
|
+
name: s.key,
|
|
295
|
+
stroke: strokeOf(s),
|
|
296
|
+
fill: strokeOf(s),
|
|
297
|
+
...stacked ? { stackId: `stack-${axisOf(s.key)}` } : {},
|
|
298
|
+
fillOpacity: stacked ? 0.7 : 0.12,
|
|
299
|
+
strokeWidth: stacked ? 0 : 2,
|
|
300
|
+
connectNulls: true,
|
|
301
|
+
isAnimationActive: false
|
|
302
|
+
},
|
|
303
|
+
s.key
|
|
304
|
+
))
|
|
305
|
+
] }) });
|
|
306
|
+
}
|
|
307
|
+
function PanelMessage({ children }) {
|
|
308
|
+
return /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center text-xs text-muted-foreground", children });
|
|
309
|
+
}
|
|
310
|
+
function PanelIcon({ icon }) {
|
|
311
|
+
if (typeof icon === "string") {
|
|
312
|
+
return /* @__PURE__ */ jsx(Icon, { name: icon, width: 16, height: 16, className: "text-muted-foreground" });
|
|
313
|
+
}
|
|
314
|
+
return /* @__PURE__ */ jsx(Icon, { icon, width: 16, height: 16, className: "text-muted-foreground" });
|
|
315
|
+
}
|
|
316
|
+
export {
|
|
317
|
+
TimeseriesPanel,
|
|
318
|
+
assignAxes,
|
|
319
|
+
latestOf,
|
|
320
|
+
mergeSeries,
|
|
321
|
+
resolveBreakdown,
|
|
322
|
+
resolveSeries
|
|
323
|
+
};
|
|
324
|
+
//# sourceMappingURL=TimeseriesPanel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TimeseriesPanel.js","sources":["../../src/data/TimeseriesPanel.tsx"],"sourcesContent":["import { useQueries } from \"@tanstack/react-query\";\nimport { useMemo, useState } from \"react\";\nimport {\n Area,\n AreaChart,\n CartesianGrid,\n Line,\n LineChart,\n ReferenceLine,\n ResponsiveContainer,\n Tooltip,\n XAxis,\n YAxis,\n} from \"recharts\";\nimport { cn } from \"../lib/utils\";\nimport { resolveCssColor } from \"../lib/color\";\nimport { formatUnit } from \"../lib/format\";\nimport { Modal } from \"../overlay/Modal\";\nimport { ProgressBar, type ProgressSegment } from \"./ProgressBar\";\nimport { UiFullscreen } from \"../icons\";\nimport { Icon, type StaticIconComponent } from \"./Icon\";\nimport { assignAxes, latestOf } from \"./TimeseriesPanel.axes\";\n\nexport { assignAxes, latestOf } from \"./TimeseriesPanel.axes\";\nexport type { AxisAssignment, AxisOrientation, AxisSpec } from \"./TimeseriesPanel.axes\";\n\nexport interface TimeseriesPoint {\n at: string;\n value: number;\n}\n\nexport interface TimeseriesResponse {\n id: string;\n points: TimeseriesPoint[];\n}\n\n/** One metric plotted on a panel. Its request URL is `baseUrl + id`. */\nexport interface TimeseriesSeries {\n /** Metric id appended to the panel's baseUrl, e.g. \"sqlserver.iops.read\". */\n id: string;\n /** Tooltip/legend label; defaults to the id. */\n label?: string;\n /**\n * Maps each point's value before plotting — e.g. writes pass `(v) => -v` to\n * render below the zero axis. Identity when omitted.\n */\n transform?: (value: number) => number;\n /**\n * Stroke/fill override as a CSS color value (e.g. \"#10b981\", \"var(--chart-3)\")\n * or a Tailwind color class (e.g. \"bg-emerald-500\"); Tailwind classes are\n * resolved to a CSS value for the chart. Defaults cycle a small palette by index.\n */\n color?: string;\n /**\n * Grafana-style unit key for this series' readout/tooltip (e.g. \"percent\",\n * \"short\"). Falls back to the panel-level `unit`. Lets one panel mix units\n * (e.g. a percent series alongside a plain-count series).\n */\n unit?: string;\n /**\n * Instantaneous value for the `breakdown` variant — the size of this series'\n * bar segment and its legend readout. When omitted, the breakdown falls back\n * to the latest polled point. Ignored by the time-series variants.\n */\n current?: number;\n}\n\nexport interface TimeseriesPanelProps {\n /**\n * Single-metric endpoint returning a TimeseriesResponse,\n * e.g. \"/api/v1/metrics/sqlserver.cpu\". Mutually exclusive with baseUrl/series.\n */\n url?: string;\n /** Common prefix for a multi-series panel; each series requests `baseUrl + series.id`. */\n baseUrl?: string;\n /** Metrics to plot on one chart. Provide together with baseUrl. */\n series?: TimeseriesSeries[];\n title: string;\n /** Iconify name or static icon component shown beside the title. */\n icon?: string | StaticIconComponent;\n /**\n * Default Grafana-style unit key for the panel (e.g. \"bytes\", \"percent\",\n * \"ms\", \"short\"); each series may override it. Unknown strings are appended\n * as a raw suffix (so \"%\" still works). Used for the readout, axis, and tooltip.\n */\n unit?: string;\n /** Look-back window passed as ?since=; defaults to \"1h\". */\n range?: string;\n /** Poll interval in ms; defaults to 5000. Pass 0 to disable polling. */\n refreshMs?: number;\n /** Chart height in px; defaults to 160. */\n height?: number;\n /**\n * \"area\" (overlaid, default), \"line\", \"stacked\" (areas sum vertically), or\n * \"breakdown\" (an instantaneous stacked bar + legend; the expand modal still\n * shows a time-series graph of the polled history).\n */\n variant?: \"area\" | \"line\" | \"stacked\" | \"breakdown\";\n /**\n * Chart variant rendered in the expand modal. Defaults to `variant` for the\n * time-series variants and to \"area\" for `breakdown` (which has no inline chart).\n */\n expandVariant?: \"area\" | \"line\" | \"stacked\";\n /**\n * Denominator for the `breakdown` bar and per-segment percentages. Defaults to\n * the sum of the series' `current` values when omitted.\n */\n total?: number;\n /** Show a header button that opens the chart larger in a modal. Default true. */\n expandable?: boolean;\n /** Override the default fetch (e.g. to route through an app's API client). */\n fetcher?: (url: string) => Promise<TimeseriesResponse>;\n className?: string;\n}\n\nconst SERIES_PALETTE = [\n \"var(--chart-1, #3b82f6)\",\n \"var(--chart-2, #ef4444)\",\n \"var(--chart-3, #10b981)\",\n \"var(--chart-4, #f59e0b)\",\n] as const;\n\nfunction paletteColor(index: number): string {\n return SERIES_PALETTE[index % SERIES_PALETTE.length] ?? SERIES_PALETTE[0];\n}\n\n/** A series with its full request URL, resolved label, color, and unit. Exported for tests. */\nexport interface ResolvedSeries {\n /** Stable key used as the merged-row field and recharts dataKey. */\n key: string;\n url: string;\n label: string;\n color: string;\n /** Resolved Grafana unit key: the series' own unit, else the panel default. */\n unit?: string;\n transform?: (value: number) => number;\n /** Instantaneous value used by the `breakdown` variant. */\n current?: number;\n}\n\n/** Normalises the single-url and baseUrl+series forms to one list. Exported for tests. */\nexport function resolveSeries(props: TimeseriesPanelProps): ResolvedSeries[] {\n const base = props.baseUrl ?? \"\";\n const list = props.series ?? (props.url ? [{ id: props.url }] : []);\n return list.map((s, i) => ({\n key: s.id,\n url: base + s.id,\n label: s.label ?? s.id,\n color: s.color ?? paletteColor(i),\n ...(s.unit ?? props.unit ? { unit: s.unit ?? props.unit } : {}),\n ...(s.transform ? { transform: s.transform } : {}),\n ...(s.current !== undefined ? { current: s.current } : {}),\n }));\n}\n\n/** One legend/bar row of the breakdown variant: resolved value, color, label, unit, percentage. */\nexport interface BreakdownItem {\n key: string;\n label: string;\n color: string;\n unit?: string;\n value: number;\n /** Share of `total`, clamped to 0..100. */\n percent: number;\n}\n\n/**\n * Derives the breakdown bar segments and per-item percentages from the resolved\n * series' `current` values. `total` defaults to the sum of those values; the\n * percentage is each value's share of that denominator, clamped to 0..100.\n * Latest polled point fills in for a series with no explicit `current`. Pure —\n * exported for tests.\n */\nexport function resolveBreakdown(\n series: ResolvedSeries[],\n rows: MergedRow[],\n total?: number,\n): { items: BreakdownItem[]; total: number } {\n const withValue = series.map((s) => ({ s, value: s.current ?? latestOf(rows, s.key) ?? 0 }));\n const denom = total ?? withValue.reduce((acc, w) => acc + w.value, 0);\n const items = withValue.map(({ s, value }) => ({\n key: s.key,\n label: s.label,\n color: s.color,\n ...(s.unit ? { unit: s.unit } : {}),\n value,\n percent: denom > 0 ? Math.min(100, Math.max(0, (value / denom) * 100)) : 0,\n }));\n return { items, total: denom };\n}\n\nconst defaultFetcher = async (url: string): Promise<TimeseriesResponse> => {\n const res = await fetch(url);\n if (!res.ok) throw new Error(`metrics request failed: ${res.status}`);\n return res.json();\n};\n\nfunction formatClock(at: string): string {\n const date = new Date(at);\n if (Number.isNaN(date.getTime())) return \"\";\n return date.toLocaleTimeString([], { hour: \"2-digit\", minute: \"2-digit\" });\n}\n\n/**\n * One row of the merged chart data: the timestamp in `at` plus each series'\n * value under its key. Values are absent (undefined) where a series has no\n * point at that timestamp.\n */\nexport interface MergedRow {\n at: string;\n [seriesKey: string]: string | number | undefined;\n}\n\n/**\n * Joins per-series point lists into rows keyed by timestamp and applies each\n * series' transform. Timestamps present in any series produce a row; missing\n * values are left undefined so recharts gaps the line rather than drawing zero.\n */\nexport function mergeSeries(\n series: ResolvedSeries[],\n pointsBySeries: (TimeseriesPoint[] | undefined)[],\n): MergedRow[] {\n const rows = new Map<string, MergedRow>();\n series.forEach((s, i) => {\n for (const p of pointsBySeries[i] ?? []) {\n const row = rows.get(p.at) ?? { at: p.at };\n row[s.key] = s.transform ? s.transform(p.value) : p.value;\n rows.set(p.at, row);\n }\n });\n return [...rows.values()].sort((a, b) => a.at.localeCompare(b.at));\n}\n\nexport function TimeseriesPanel(props: TimeseriesPanelProps) {\n const {\n title,\n icon,\n unit,\n range = \"1h\",\n refreshMs = 5000,\n height = 160,\n variant = \"area\",\n expandable = true,\n total,\n fetcher = defaultFetcher,\n className,\n } = props;\n const isBreakdown = variant === \"breakdown\";\n // The inline chart (non-breakdown only) and the expand modal both need a\n // time-series variant; breakdown has no inline chart and expands to \"area\".\n const chartVariant: ChartVariant = isBreakdown ? \"area\" : variant;\n const expandVariant = props.expandVariant ?? chartVariant;\n\n const [expanded, setExpanded] = useState(false);\n\n const series = useMemo(() => resolveSeries(props), [props]);\n const requestUrls = useMemo(\n () =>\n series.map((s) => {\n const u = new URL(s.url, window.location.origin);\n if (range) u.searchParams.set(\"since\", range);\n return u.pathname + u.search;\n }),\n [series, range],\n );\n\n const results = useQueries({\n queries: requestUrls.map((requestUrl) => ({\n queryKey: [\"timeseries\", requestUrl],\n queryFn: () => fetcher(requestUrl),\n refetchInterval: refreshMs > 0 ? refreshMs : false,\n staleTime: 0,\n retry: 0,\n })),\n });\n\n const isError = results.some((r) => r.isError);\n const error = results.find((r) => r.isError)?.error;\n const isLoading = results.some((r) => r.isLoading) && results.every((r) => !r.data);\n const pointsBySeries = results.map((r) => r.data?.points);\n const rows = useMemo(() => mergeSeries(series, pointsBySeries), [series, pointsBySeries]);\n const hasMirrored = series.some((s) => s.transform);\n const breakdown = useMemo(\n () => (isBreakdown ? resolveBreakdown(series, rows, total) : null),\n [isBreakdown, series, rows, total],\n );\n\n // Breakdown renders instantly from `current`; the modal still needs polled\n // history. Other variants gate on having ≥2 points to draw a line.\n const canExpand = expandable && rows.length >= 2;\n\n return (\n <div className={cn(\"flex flex-col rounded-lg border border-border bg-card p-3\", className)}>\n <div className=\"flex items-center justify-between gap-2 text-xs text-muted-foreground\">\n <span className=\"inline-flex min-w-0 items-center gap-1.5\">\n {icon ? <PanelIcon icon={icon} /> : null}\n <span className=\"truncate font-medium text-foreground\">{title}</span>\n </span>\n <span className=\"inline-flex shrink-0 items-center gap-1.5\">\n {breakdown ? (\n <BreakdownReadout total={breakdown.total} unit={unit} items={breakdown.items} />\n ) : (\n <LatestReadout series={series} rows={rows} />\n )}\n {canExpand ? (\n <button\n type=\"button\"\n aria-label=\"Expand chart\"\n onClick={() => setExpanded(true)}\n className=\"text-muted-foreground transition-colors hover:text-foreground\"\n >\n <Icon icon={UiFullscreen} width={14} height={14} />\n </button>\n ) : null}\n </span>\n </div>\n <div className=\"mt-2\" style={breakdown ? undefined : { height }}>\n {breakdown ? (\n <BreakdownView total={breakdown.total} unit={unit} items={breakdown.items} />\n ) : isError ? (\n <PanelMessage>{(error as Error)?.message ?? \"Failed to load metric\"}</PanelMessage>\n ) : isLoading ? (\n <PanelMessage>Loading…</PanelMessage>\n ) : rows.length < 2 ? (\n <PanelMessage>Collecting data…</PanelMessage>\n ) : (\n <Chart rows={rows} series={series} variant={chartVariant} unit={unit} showZeroLine={hasMirrored} />\n )}\n </div>\n {!breakdown && !isError && !isLoading && rows.length >= 2 && series.length > 1 ? (\n <ChartLegend series={series} rows={rows} />\n ) : null}\n {canExpand ? (\n <Modal open={expanded} onClose={() => setExpanded(false)} title={title} size=\"xl\">\n <div style={{ height: 360 }}>\n <Chart rows={rows} series={series} variant={expandVariant} unit={unit} showZeroLine={hasMirrored} />\n </div>\n {series.length > 1 ? <ChartLegend series={series} rows={rows} /> : null}\n </Modal>\n ) : null}\n </div>\n );\n}\n\n/**\n * Header readout for a single-series panel: the latest value, formatted with the\n * series' unit. Multi-series panels render the per-series values in {@link ChartLegend}\n * below the chart instead, so this returns null for them to avoid overlapping the title.\n */\nfunction LatestReadout({ series, rows }: { series: ResolvedSeries[]; rows: MergedRow[] }) {\n const only = series.length === 1 ? series[0] : undefined;\n if (!only) return null;\n const v = latestOf(rows, only.key);\n if (v === undefined) return null;\n return <span className=\"tabular-nums\">{formatUnit(Math.abs(v), only.unit)}</span>;\n}\n\n/** Wrapping color-keyed legend for the time-series variants: dot + label + latest value. */\nfunction ChartLegend({ series, rows }: { series: ResolvedSeries[]; rows: MergedRow[] }) {\n return (\n <ul className=\"mt-1.5 flex flex-wrap gap-x-3 gap-y-1 text-xs text-muted-foreground\">\n {series.map((s) => {\n const v = latestOf(rows, s.key);\n return (\n <li key={s.key} className=\"inline-flex min-w-0 items-center gap-1.5\">\n <span\n className=\"h-2 w-2 shrink-0 rounded-full\"\n style={{ backgroundColor: resolveCssColor(s.color) }}\n aria-hidden\n />\n <span className=\"truncate text-foreground\">{s.label}</span>\n {v !== undefined ? (\n <span className=\"tabular-nums\">{formatUnit(Math.abs(v), s.unit)}</span>\n ) : null}\n </li>\n );\n })}\n </ul>\n );\n}\n\n/** Breakdown header readout: `used / total · pct%`, where used is the sum of segments. */\nfunction BreakdownReadout({\n total,\n unit,\n items,\n}: {\n total: number;\n unit: string | undefined;\n items: BreakdownItem[];\n}) {\n const used = items.reduce((acc, it) => acc + it.value, 0);\n const pct = total > 0 ? Math.min(100, (used / total) * 100) : 0;\n return (\n <span className=\"font-mono tabular-nums\">\n {formatUnit(used, unit)} / {formatUnit(total, unit)} · {pct.toFixed(0)}%\n </span>\n );\n}\n\n/** Instantaneous stacked bar + legend list. The expand button (in the panel header) opens the graph. */\nfunction BreakdownView({\n total,\n unit,\n items,\n}: {\n total: number;\n unit: string | undefined;\n items: BreakdownItem[];\n}) {\n const segments: ProgressSegment[] = items.map((it) => ({\n count: it.value,\n color: it.color,\n label: it.label,\n }));\n return (\n <div className=\"flex flex-col gap-2\">\n <ProgressBar segments={segments} total={total > 0 ? total : 1} height=\"h-3\" />\n {items.length > 0 ? (\n <ul className=\"grid grid-cols-1 gap-y-1 text-xs\">\n {items.map((it) => (\n <li key={it.key} className=\"flex items-center gap-2\">\n <span\n className=\"h-2 w-2 shrink-0 rounded-full\"\n style={{ backgroundColor: resolveCssColor(it.color) }}\n aria-hidden\n />\n <span className=\"min-w-0 flex-1 truncate font-mono text-muted-foreground\">\n {it.label}\n </span>\n <span className=\"font-mono tabular-nums text-foreground\">\n {formatUnit(it.value, it.unit ?? unit)}\n </span>\n <span className=\"w-10 text-right font-mono tabular-nums text-muted-foreground\">\n {it.percent.toFixed(0)}%\n </span>\n </li>\n ))}\n </ul>\n ) : null}\n </div>\n );\n}\n\ntype ChartVariant = \"area\" | \"line\" | \"stacked\";\n\nfunction Chart({\n rows,\n series,\n variant,\n unit,\n showZeroLine,\n}: {\n rows: MergedRow[];\n series: ResolvedSeries[];\n variant: ChartVariant;\n unit: string | undefined;\n showZeroLine: boolean;\n}) {\n const stacked = variant === \"stacked\";\n // Mirrored series carry negative values; show magnitudes on axis and tooltip.\n const seriesByKey = new Map(series.map((s) => [s.key, s]));\n // recharts needs a CSS color value; a series may carry a Tailwind class.\n const strokeOf = (s: ResolvedSeries) => resolveCssColor(s.color);\n // Distinct units map to a left and (when present) right axis; each series draws\n // against the axis for its unit so mixed-unit panels keep independent scales.\n const { axes: yAxes, axisOfKey } = useMemo(() => assignAxes(series), [series]);\n const axisOf = (key: string): \"left\" | \"right\" => axisOfKey.get(key) ?? \"left\";\n const axes = (\n <>\n <CartesianGrid strokeOpacity={0.12} vertical={false} />\n <XAxis\n dataKey=\"at\"\n tickFormatter={formatClock}\n tick={{ fontSize: 10 }}\n stroke=\"currentColor\"\n strokeOpacity={0.35}\n minTickGap={32}\n />\n {yAxes.map((ax) => (\n <YAxis\n key={ax.id}\n yAxisId={ax.id}\n orientation={ax.orientation}\n tick={{ fontSize: 10 }}\n stroke=\"currentColor\"\n strokeOpacity={0.35}\n width={40}\n tickFormatter={(v: number) => formatUnit(Math.abs(v), ax.unit)}\n />\n ))}\n <Tooltip\n labelFormatter={(label) => formatClock(String(label))}\n formatter={(value, name) => {\n const s = seriesByKey.get(String(name));\n return [formatUnit(Math.abs(Number(value) || 0), s?.unit ?? unit), s?.label ?? String(name)];\n }}\n contentStyle={{ fontSize: 12 }}\n />\n {showZeroLine ? (\n <ReferenceLine yAxisId=\"left\" y={0} stroke=\"currentColor\" strokeOpacity={0.35} />\n ) : null}\n </>\n );\n\n return (\n <ResponsiveContainer width=\"100%\" height=\"100%\">\n {variant === \"line\" ? (\n <LineChart data={rows} margin={{ top: 4, right: 8, bottom: 0, left: 0 }}>\n {axes}\n {series.map((s) => (\n <Line\n key={s.key}\n yAxisId={axisOf(s.key)}\n type=\"monotone\"\n dataKey={s.key}\n name={s.key}\n stroke={strokeOf(s)}\n strokeWidth={2}\n dot={false}\n connectNulls\n isAnimationActive={false}\n />\n ))}\n </LineChart>\n ) : (\n <AreaChart data={rows} margin={{ top: 4, right: 8, bottom: 0, left: 0 }}>\n {axes}\n {series.map((s) => (\n <Area\n key={s.key}\n yAxisId={axisOf(s.key)}\n type=\"monotone\"\n dataKey={s.key}\n name={s.key}\n stroke={strokeOf(s)}\n fill={strokeOf(s)}\n {...(stacked ? { stackId: `stack-${axisOf(s.key)}` } : {})}\n fillOpacity={stacked ? 0.7 : 0.12}\n strokeWidth={stacked ? 0 : 2}\n connectNulls\n isAnimationActive={false}\n />\n ))}\n </AreaChart>\n )}\n </ResponsiveContainer>\n );\n}\n\nfunction PanelMessage({ children }: { children: React.ReactNode }) {\n return (\n <div className=\"flex h-full items-center justify-center text-xs text-muted-foreground\">\n {children}\n </div>\n );\n}\n\nfunction PanelIcon({ icon }: { icon: string | StaticIconComponent }) {\n if (typeof icon === \"string\") {\n return <Icon name={icon} width={16} height={16} className=\"text-muted-foreground\" />;\n }\n return <Icon icon={icon} width={16} height={16} className=\"text-muted-foreground\" />;\n}\n"],"names":["_a"],"mappings":"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"}
|
package/dist/data/Tree.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Tree.cjs","sources":["../../src/data/Tree.tsx"],"sourcesContent":["import { useMemo, useState, type ReactNode } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { Icon } from \"./Icon\";\nimport { UiClose, UiCollapseAll, UiExpandAll, UiSearch } from \"../icons\";\nimport { TreeNode, type TreeNodeProps } from \"./TreeNode\";\n\nexport type TreeProps<T> = Omit<\n TreeNodeProps<T>,\n \"node\" | \"depth\" | \"expandAll\" | \"forcedOpenKeys\"\n> & {\n /** Root nodes to render. */\n roots: T[];\n /** Empty-state content when no roots are provided. */\n empty?: ReactNode;\n /** Classes applied to the tree root. */\n className?: string;\n /**\n * Render the Expand all / Collapse all toolbar above the tree. Enabled by\n * default. Set to false to suppress when the tree is tiny or the controls\n * are owned externally.\n */\n showControls?: boolean;\n /**\n * Externally controlled expand-all state. When provided together with\n * `onExpandAllChange`, the tree is controlled; otherwise it manages the\n * toolbar state internally.\n */\n expandAll?: boolean | null;\n /** Called when the toolbar expand-all state changes. */\n onExpandAllChange?: (next: boolean | null) => void;\n /** Classes applied to the search/control toolbar. */\n toolbarClassName?: string;\n /**\n * Override the text the filter matches against for a given node.\n * Defaults to a recursive walk of the node's own fields (excluding\n * its `children` and any secondary children), which works for\n * plain-shape trees but leaks text from nested domain objects —\n * e.g. a SQL Table's Field rows are marked secondary, yet the\n * parent record still carries `record.fields[].field` strings that\n * the default walker would collect. Callers that need precise\n * control over search behaviour should return only the text they\n * want surfaced (the node's own display label / id / type).\n */\n getSearchText?: (node: T) => string;\n};\n\nfunction countTreeEdges<T>(roots: T[], getChildren: TreeProps<T>[\"getChildren\"]) {\n let total = 0;\n const stack = [...roots];\n\n while (stack.length > 0) {\n const node = stack.pop()!;\n const children = getChildren(node) ?? [];\n total += children.length;\n stack.push(...children);\n }\n\n return total;\n}\n\nfunction collectTreeSearchText(\n value: unknown,\n excluded = new Set<unknown>(),\n seen = new Set<unknown>(),\n): string {\n if (value == null || excluded.has(value)) return \"\";\n\n if (\n typeof value === \"string\" ||\n typeof value === \"number\" ||\n typeof value === \"boolean\" ||\n typeof value === \"bigint\"\n ) {\n return String(value);\n }\n\n if (typeof value !== \"object\") return \"\";\n if (seen.has(value)) return \"\";\n seen.add(value);\n\n if (Array.isArray(value)) {\n return value\n .map((entry) => collectTreeSearchText(entry, excluded, seen))\n .filter(Boolean)\n .join(\" \");\n }\n\n const preferredKeys = [\"label\", \"name\", \"title\", \"text\", \"plain\", \"id\", \"content\"];\n const record = value as Record<string, unknown>;\n const chunks: string[] = [];\n\n for (const key of preferredKeys) {\n if (!(key in record)) continue;\n const text = collectTreeSearchText(record[key], excluded, seen);\n if (text) chunks.push(text);\n }\n\n for (const [key, entry] of Object.entries(record)) {\n if (key === \"children\" || preferredKeys.includes(key)) continue;\n const text = collectTreeSearchText(entry, excluded, seen);\n if (text) chunks.push(text);\n }\n\n return chunks.join(\" \");\n}\n\nfunction nodeSearchText<T>(node: T, children: ReturnType<TreeProps<T>[\"getChildren\"]>) {\n const excluded = new Set<unknown>();\n if (children) excluded.add(children);\n return collectTreeSearchText(node, excluded).trim().toLowerCase();\n}\n\nfunction filterTreeRoots<T>(\n roots: T[],\n getChildren: TreeProps<T>[\"getChildren\"],\n getKey: TreeProps<T>[\"getKey\"],\n query: string,\n isSecondary?: (node: T) => boolean,\n getSearchText?: (node: T) => string,\n) {\n const normalizedQuery = query.trim().toLowerCase();\n if (!normalizedQuery) {\n return {\n roots,\n filteredChildren: null as Map<string | number, T[]> | null,\n forcedOpenKeys: null as Set<string | number> | null,\n };\n }\n\n const filteredChildren = new Map<string | number, T[]>();\n const forcedOpenKeys = new Set<string | number>();\n\n function matchText(node: T, children: T[]): string {\n // Honour the caller-supplied text extractor first — it's the\n // only way to get precise control over what the filter sees when\n // the node carries domain data whose fields would otherwise leak\n // into the default walker.\n if (getSearchText) return getSearchText(node).trim().toLowerCase();\n // Match-text skips secondary children so e.g. a Table's search\n // text only draws from the Table itself, not from its Field\n // rows. The Table is primary, so its own text still counts.\n const searchChildren = children.filter((c) => !isSecondary?.(c));\n return nodeSearchText(node, searchChildren);\n }\n\n function visit(node: T): boolean {\n // A secondary node never drives a filter match — it's\n // effectively invisible to search. Still rendered when the\n // caller opens the parent manually. This is what lets a 28k-row\n // field tree stay searchable at the record level without \"field\n // name contains 'x'\" matches spamming the results.\n if (isSecondary?.(node)) return false;\n\n const key = getKey(node);\n const children = getChildren(node) ?? [];\n const childrenAllSecondary =\n children.length > 0 && children.every((child) => isSecondary?.(child));\n const matches = matchText(node, children).includes(normalizedQuery);\n const visibleChildren: T[] = [];\n\n for (const child of children) {\n if (visit(child)) visibleChildren.push(child);\n }\n\n if (matches) {\n if (children.length > 0) {\n filteredChildren.set(key, children);\n if (!childrenAllSecondary) forcedOpenKeys.add(key);\n }\n return true;\n }\n\n if (visibleChildren.length > 0) {\n filteredChildren.set(key, visibleChildren);\n forcedOpenKeys.add(key);\n return true;\n }\n\n return false;\n }\n\n return {\n roots: roots.filter((root) => visit(root)),\n filteredChildren,\n forcedOpenKeys,\n };\n}\n\nexport function Tree<T>({\n roots,\n empty,\n className,\n showControls = true,\n expandAll: controlledExpandAll,\n onExpandAllChange,\n toolbarClassName,\n getSearchText,\n ...nodeProps\n}: TreeProps<T>) {\n const [internalExpandAll, setInternalExpandAll] = useState<boolean | null>(null);\n const [filterQuery, setFilterQuery] = useState(\"\");\n const basePaddingPx = nodeProps.basePaddingPx ?? 8;\n const isControlled = onExpandAllChange !== undefined;\n const expandAll = isControlled ? (controlledExpandAll ?? null) : internalExpandAll;\n const totalEdges = useMemo(\n () => countTreeEdges(roots, nodeProps.getChildren),\n [roots, nodeProps.getChildren],\n );\n const showFilter = totalEdges > 20;\n const activeFilter = showFilter ? filterQuery : \"\";\n const filteredTree = useMemo(\n () =>\n filterTreeRoots(\n roots,\n nodeProps.getChildren,\n nodeProps.getKey,\n activeFilter,\n nodeProps.isSecondary,\n getSearchText,\n ),\n [\n roots,\n nodeProps.getChildren,\n nodeProps.getKey,\n activeFilter,\n nodeProps.isSecondary,\n getSearchText,\n ],\n );\n const treeRoots = filteredTree.roots;\n const filteredChildren = filteredTree.filteredChildren;\n const forcedOpenKeys = filteredTree.forcedOpenKeys;\n const effectiveGetChildren = filteredChildren\n ? (node: T) => filteredChildren.get(nodeProps.getKey(node)) ?? []\n : nodeProps.getChildren;\n const effectiveExpandAll = expandAll;\n const effectiveEmpty = activeFilter.trim() ? (\n <div className=\"px-3 py-4 text-sm text-muted-foreground\">No matching tree nodes.</div>\n ) : (\n empty\n );\n const showControlButtons = showControls && roots.length > 0;\n const showVirtualRow = showFilter || showControlButtons;\n const setExpandAll = (next: boolean | null) => {\n if (isControlled) onExpandAllChange?.(next);\n else setInternalExpandAll(next);\n };\n\n if (treeRoots.length === 0 && !showVirtualRow) return <>{effectiveEmpty ?? null}</>;\n\n return (\n <div className={cn(\"flex flex-col min-h-0\", className)}>\n <div role=\"tree\" className=\"min-h-0 flex-1 overflow-auto\">\n {showVirtualRow && (\n <div\n role=\"presentation\"\n className={cn(\n \"sticky top-0 z-10 flex items-center gap-1.5 border-b border-border/70 bg-background/95 py-1 pr-2 text-sm backdrop-blur supports-[backdrop-filter]:bg-background/80\",\n toolbarClassName,\n )}\n style={{ paddingLeft: `${basePaddingPx}px` }}\n >\n {showFilter ? (\n <Icon icon={UiSearch} className=\"w-3 shrink-0 text-xs text-muted-foreground\" />\n ) : (\n <span className=\"w-3 shrink-0\" aria-hidden />\n )}\n\n {showFilter ? (\n <label className=\"flex min-w-0 flex-1 items-center gap-2 rounded-md border border-border bg-background px-2 py-1 text-xs text-foreground shadow-sm\">\n <input\n type=\"search\"\n value={filterQuery}\n onChange={(event) => setFilterQuery(event.target.value)}\n placeholder=\"Filter tree\"\n className=\"h-5 w-full border-0 bg-transparent p-0 text-xs outline-none placeholder:text-muted-foreground\"\n aria-label=\"Filter tree nodes\"\n />\n {filterQuery && (\n <button\n type=\"button\"\n onClick={() => setFilterQuery(\"\")}\n className=\"inline-flex items-center rounded p-0.5 text-muted-foreground hover:bg-accent hover:text-accent-foreground\"\n aria-label=\"Clear tree filter\"\n title=\"Clear tree filter\"\n >\n <Icon icon={UiClose} className=\"text-xs\" />\n </button>\n )}\n </label>\n ) : (\n <span className=\"flex-1\" />\n )}\n\n {showControlButtons && (\n <div className=\"ml-auto flex items-center gap-1\">\n <button\n type=\"button\"\n onClick={() => setExpandAll(true)}\n aria-pressed={effectiveExpandAll === true}\n aria-label=\"Expand all\"\n title=\"Expand all\"\n disabled={activeFilter.trim().length > 0}\n className={cn(\n \"inline-flex h-6 w-6 items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-accent-foreground disabled:cursor-not-allowed disabled:opacity-50\",\n effectiveExpandAll === true && \"bg-accent text-accent-foreground\",\n )}\n >\n <Icon icon={UiExpandAll} className=\"text-sm\" />\n </button>\n <button\n type=\"button\"\n onClick={() => setExpandAll(false)}\n aria-pressed={effectiveExpandAll === false}\n aria-label=\"Collapse all\"\n title=\"Collapse all\"\n disabled={activeFilter.trim().length > 0}\n className={cn(\n \"inline-flex h-6 w-6 items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-accent-foreground disabled:cursor-not-allowed disabled:opacity-50\",\n effectiveExpandAll === false && \"bg-accent text-accent-foreground\",\n )}\n >\n <Icon icon={UiCollapseAll} className=\"text-sm\" />\n </button>\n </div>\n )}\n </div>\n )}\n\n {treeRoots.length === 0 ? (\n (effectiveEmpty ?? null)\n ) : (\n <>\n {treeRoots.map((root) => (\n <TreeNode<T>\n {...nodeProps}\n key={nodeProps.getKey(root)}\n node={root}\n expandAll={effectiveExpandAll}\n forcedOpenKeys={forcedOpenKeys}\n getChildren={effectiveGetChildren}\n />\n ))}\n </>\n )}\n </div>\n </div>\n );\n}\n"],"names":["useState","useMemo","jsx","Fragment","cn","jsxs","Icon","UiSearch","UiClose","UiExpandAll","UiCollapseAll","createElement","TreeNode"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Tree.cjs","sources":["../../src/data/Tree.tsx"],"sourcesContent":["import { useMemo, useState, type ReactNode } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { Icon } from \"./Icon\";\nimport { UiClose, UiCollapseAll, UiExpandAll, UiSearch } from \"../icons\";\nimport { TreeNode, type TreeNodeProps } from \"./TreeNode\";\n\nexport type TreeProps<T> = Omit<\n TreeNodeProps<T>,\n \"node\" | \"depth\" | \"expandAll\" | \"forcedOpenKeys\"\n> & {\n /** Root nodes to render. */\n roots: T[];\n /** Empty-state content when no roots are provided. */\n empty?: ReactNode;\n /** Classes applied to the tree root. */\n className?: string;\n /**\n * Render the Expand all / Collapse all toolbar above the tree. Enabled by\n * default. Set to false to suppress when the tree is tiny or the controls\n * are owned externally.\n */\n showControls?: boolean;\n /**\n * Externally controlled expand-all state. When provided together with\n * `onExpandAllChange`, the tree is controlled; otherwise it manages the\n * toolbar state internally.\n */\n expandAll?: boolean | null;\n /** Called when the toolbar expand-all state changes. */\n onExpandAllChange?: (next: boolean | null) => void;\n /** Classes applied to the search/control toolbar. */\n toolbarClassName?: string;\n /**\n * Override the text the filter matches against for a given node.\n * Defaults to a recursive walk of the node's own fields (excluding\n * its `children` and any secondary children), which works for\n * plain-shape trees but leaks text from nested domain objects —\n * e.g. a SQL Table's Field rows are marked secondary, yet the\n * parent record still carries `record.fields[].field` strings that\n * the default walker would collect. Callers that need precise\n * control over search behaviour should return only the text they\n * want surfaced (the node's own display label / id / type).\n */\n getSearchText?: (node: T) => string;\n};\n\n// Edge counting, search-text collection and filtering all walk the tree through\n// the synchronous `getChildren` only. A lazy node (one declared via\n// `hasMoreChildren` whose children have not been fetched yet) therefore\n// contributes no edges and no search text until the operator expands it — so it\n// is invisible to \"expand all\" and to the filter while still collapsed. This is\n// intentional: we never trigger network loads from bulk/search operations.\nfunction countTreeEdges<T>(roots: T[], getChildren: TreeProps<T>[\"getChildren\"]) {\n let total = 0;\n const stack = [...roots];\n\n while (stack.length > 0) {\n const node = stack.pop()!;\n const children = getChildren(node) ?? [];\n total += children.length;\n stack.push(...children);\n }\n\n return total;\n}\n\nfunction collectTreeSearchText(\n value: unknown,\n excluded = new Set<unknown>(),\n seen = new Set<unknown>(),\n): string {\n if (value == null || excluded.has(value)) return \"\";\n\n if (\n typeof value === \"string\" ||\n typeof value === \"number\" ||\n typeof value === \"boolean\" ||\n typeof value === \"bigint\"\n ) {\n return String(value);\n }\n\n if (typeof value !== \"object\") return \"\";\n if (seen.has(value)) return \"\";\n seen.add(value);\n\n if (Array.isArray(value)) {\n return value\n .map((entry) => collectTreeSearchText(entry, excluded, seen))\n .filter(Boolean)\n .join(\" \");\n }\n\n const preferredKeys = [\"label\", \"name\", \"title\", \"text\", \"plain\", \"id\", \"content\"];\n const record = value as Record<string, unknown>;\n const chunks: string[] = [];\n\n for (const key of preferredKeys) {\n if (!(key in record)) continue;\n const text = collectTreeSearchText(record[key], excluded, seen);\n if (text) chunks.push(text);\n }\n\n for (const [key, entry] of Object.entries(record)) {\n if (key === \"children\" || preferredKeys.includes(key)) continue;\n const text = collectTreeSearchText(entry, excluded, seen);\n if (text) chunks.push(text);\n }\n\n return chunks.join(\" \");\n}\n\nfunction nodeSearchText<T>(node: T, children: ReturnType<TreeProps<T>[\"getChildren\"]>) {\n const excluded = new Set<unknown>();\n if (children) excluded.add(children);\n return collectTreeSearchText(node, excluded).trim().toLowerCase();\n}\n\nfunction filterTreeRoots<T>(\n roots: T[],\n getChildren: TreeProps<T>[\"getChildren\"],\n getKey: TreeProps<T>[\"getKey\"],\n query: string,\n isSecondary?: (node: T) => boolean,\n getSearchText?: (node: T) => string,\n) {\n const normalizedQuery = query.trim().toLowerCase();\n if (!normalizedQuery) {\n return {\n roots,\n filteredChildren: null as Map<string | number, T[]> | null,\n forcedOpenKeys: null as Set<string | number> | null,\n };\n }\n\n const filteredChildren = new Map<string | number, T[]>();\n const forcedOpenKeys = new Set<string | number>();\n\n function matchText(node: T, children: T[]): string {\n // Honour the caller-supplied text extractor first — it's the\n // only way to get precise control over what the filter sees when\n // the node carries domain data whose fields would otherwise leak\n // into the default walker.\n if (getSearchText) return getSearchText(node).trim().toLowerCase();\n // Match-text skips secondary children so e.g. a Table's search\n // text only draws from the Table itself, not from its Field\n // rows. The Table is primary, so its own text still counts.\n const searchChildren = children.filter((c) => !isSecondary?.(c));\n return nodeSearchText(node, searchChildren);\n }\n\n function visit(node: T): boolean {\n // A secondary node never drives a filter match — it's\n // effectively invisible to search. Still rendered when the\n // caller opens the parent manually. This is what lets a 28k-row\n // field tree stay searchable at the record level without \"field\n // name contains 'x'\" matches spamming the results.\n if (isSecondary?.(node)) return false;\n\n const key = getKey(node);\n const children = getChildren(node) ?? [];\n const childrenAllSecondary =\n children.length > 0 && children.every((child) => isSecondary?.(child));\n const matches = matchText(node, children).includes(normalizedQuery);\n const visibleChildren: T[] = [];\n\n for (const child of children) {\n if (visit(child)) visibleChildren.push(child);\n }\n\n if (matches) {\n if (children.length > 0) {\n filteredChildren.set(key, children);\n if (!childrenAllSecondary) forcedOpenKeys.add(key);\n }\n return true;\n }\n\n if (visibleChildren.length > 0) {\n filteredChildren.set(key, visibleChildren);\n forcedOpenKeys.add(key);\n return true;\n }\n\n return false;\n }\n\n return {\n roots: roots.filter((root) => visit(root)),\n filteredChildren,\n forcedOpenKeys,\n };\n}\n\nexport function Tree<T>({\n roots,\n empty,\n className,\n showControls = true,\n expandAll: controlledExpandAll,\n onExpandAllChange,\n toolbarClassName,\n getSearchText,\n ...nodeProps\n}: TreeProps<T>) {\n const [internalExpandAll, setInternalExpandAll] = useState<boolean | null>(null);\n const [filterQuery, setFilterQuery] = useState(\"\");\n const basePaddingPx = nodeProps.basePaddingPx ?? 8;\n const isControlled = onExpandAllChange !== undefined;\n const expandAll = isControlled ? (controlledExpandAll ?? null) : internalExpandAll;\n const totalEdges = useMemo(\n () => countTreeEdges(roots, nodeProps.getChildren),\n [roots, nodeProps.getChildren],\n );\n const showFilter = totalEdges > 20;\n const activeFilter = showFilter ? filterQuery : \"\";\n const filteredTree = useMemo(\n () =>\n filterTreeRoots(\n roots,\n nodeProps.getChildren,\n nodeProps.getKey,\n activeFilter,\n nodeProps.isSecondary,\n getSearchText,\n ),\n [\n roots,\n nodeProps.getChildren,\n nodeProps.getKey,\n activeFilter,\n nodeProps.isSecondary,\n getSearchText,\n ],\n );\n const treeRoots = filteredTree.roots;\n const filteredChildren = filteredTree.filteredChildren;\n const forcedOpenKeys = filteredTree.forcedOpenKeys;\n const effectiveGetChildren = filteredChildren\n ? (node: T) => filteredChildren.get(nodeProps.getKey(node)) ?? []\n : nodeProps.getChildren;\n const effectiveExpandAll = expandAll;\n const effectiveEmpty = activeFilter.trim() ? (\n <div className=\"px-3 py-4 text-sm text-muted-foreground\">No matching tree nodes.</div>\n ) : (\n empty\n );\n const showControlButtons = showControls && roots.length > 0;\n const showVirtualRow = showFilter || showControlButtons;\n const setExpandAll = (next: boolean | null) => {\n if (isControlled) onExpandAllChange?.(next);\n else setInternalExpandAll(next);\n };\n\n if (treeRoots.length === 0 && !showVirtualRow) return <>{effectiveEmpty ?? null}</>;\n\n return (\n <div className={cn(\"flex flex-col min-h-0\", className)}>\n <div role=\"tree\" className=\"min-h-0 flex-1 overflow-auto\">\n {showVirtualRow && (\n <div\n role=\"presentation\"\n className={cn(\n \"sticky top-0 z-10 flex items-center gap-1.5 border-b border-border/70 bg-background/95 py-1 pr-2 text-sm backdrop-blur supports-[backdrop-filter]:bg-background/80\",\n toolbarClassName,\n )}\n style={{ paddingLeft: `${basePaddingPx}px` }}\n >\n {showFilter ? (\n <Icon icon={UiSearch} className=\"w-3 shrink-0 text-xs text-muted-foreground\" />\n ) : (\n <span className=\"w-3 shrink-0\" aria-hidden />\n )}\n\n {showFilter ? (\n <label className=\"flex min-w-0 flex-1 items-center gap-2 rounded-md border border-border bg-background px-2 py-1 text-xs text-foreground shadow-sm\">\n <input\n type=\"search\"\n value={filterQuery}\n onChange={(event) => setFilterQuery(event.target.value)}\n placeholder=\"Filter tree\"\n className=\"h-5 w-full border-0 bg-transparent p-0 text-xs outline-none placeholder:text-muted-foreground\"\n aria-label=\"Filter tree nodes\"\n />\n {filterQuery && (\n <button\n type=\"button\"\n onClick={() => setFilterQuery(\"\")}\n className=\"inline-flex items-center rounded p-0.5 text-muted-foreground hover:bg-accent hover:text-accent-foreground\"\n aria-label=\"Clear tree filter\"\n title=\"Clear tree filter\"\n >\n <Icon icon={UiClose} className=\"text-xs\" />\n </button>\n )}\n </label>\n ) : (\n <span className=\"flex-1\" />\n )}\n\n {showControlButtons && (\n <div className=\"ml-auto flex items-center gap-1\">\n <button\n type=\"button\"\n onClick={() => setExpandAll(true)}\n aria-pressed={effectiveExpandAll === true}\n aria-label=\"Expand all\"\n title=\"Expand all\"\n disabled={activeFilter.trim().length > 0}\n className={cn(\n \"inline-flex h-6 w-6 items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-accent-foreground disabled:cursor-not-allowed disabled:opacity-50\",\n effectiveExpandAll === true && \"bg-accent text-accent-foreground\",\n )}\n >\n <Icon icon={UiExpandAll} className=\"text-sm\" />\n </button>\n <button\n type=\"button\"\n onClick={() => setExpandAll(false)}\n aria-pressed={effectiveExpandAll === false}\n aria-label=\"Collapse all\"\n title=\"Collapse all\"\n disabled={activeFilter.trim().length > 0}\n className={cn(\n \"inline-flex h-6 w-6 items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-accent-foreground disabled:cursor-not-allowed disabled:opacity-50\",\n effectiveExpandAll === false && \"bg-accent text-accent-foreground\",\n )}\n >\n <Icon icon={UiCollapseAll} className=\"text-sm\" />\n </button>\n </div>\n )}\n </div>\n )}\n\n {treeRoots.length === 0 ? (\n (effectiveEmpty ?? null)\n ) : (\n <>\n {treeRoots.map((root) => (\n <TreeNode<T>\n {...nodeProps}\n key={nodeProps.getKey(root)}\n node={root}\n expandAll={effectiveExpandAll}\n forcedOpenKeys={forcedOpenKeys}\n getChildren={effectiveGetChildren}\n />\n ))}\n </>\n )}\n </div>\n </div>\n );\n}\n"],"names":["useState","useMemo","jsx","Fragment","cn","jsxs","Icon","UiSearch","UiClose","UiExpandAll","UiCollapseAll","createElement","TreeNode"],"mappings":"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"}
|
package/dist/data/Tree.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Tree.d.ts","sourceRoot":"","sources":["../../src/data/Tree.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAqB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAI1D,OAAO,EAAY,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AAE1D,MAAM,MAAM,SAAS,CAAC,CAAC,IAAI,IAAI,CAC7B,aAAa,CAAC,CAAC,CAAC,EAChB,MAAM,GAAG,OAAO,GAAG,WAAW,GAAG,gBAAgB,CAClD,GAAG;IACF,4BAA4B;IAC5B,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,sDAAsD;IACtD,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IAC3B,wDAAwD;IACxD,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,KAAK,IAAI,CAAC;IACnD,qDAAqD;IACrD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;;;;;;OAUG;IACH,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,CAAC;CACrC,CAAC;
|
|
1
|
+
{"version":3,"file":"Tree.d.ts","sourceRoot":"","sources":["../../src/data/Tree.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAqB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAI1D,OAAO,EAAY,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AAE1D,MAAM,MAAM,SAAS,CAAC,CAAC,IAAI,IAAI,CAC7B,aAAa,CAAC,CAAC,CAAC,EAChB,MAAM,GAAG,OAAO,GAAG,WAAW,GAAG,gBAAgB,CAClD,GAAG;IACF,4BAA4B;IAC5B,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,sDAAsD;IACtD,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IAC3B,wDAAwD;IACxD,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,KAAK,IAAI,CAAC;IACnD,qDAAqD;IACrD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;;;;;;OAUG;IACH,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,CAAC;CACrC,CAAC;AAsJF,wBAAgB,IAAI,CAAC,CAAC,EAAE,EACtB,KAAK,EACL,KAAK,EACL,SAAS,EACT,YAAmB,EACnB,SAAS,EAAE,mBAAmB,EAC9B,iBAAiB,EACjB,gBAAgB,EAChB,aAAa,EACb,GAAG,SAAS,EACb,EAAE,SAAS,CAAC,CAAC,CAAC,2CAsJd"}
|
package/dist/data/Tree.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Tree.js","sources":["../../src/data/Tree.tsx"],"sourcesContent":["import { useMemo, useState, type ReactNode } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { Icon } from \"./Icon\";\nimport { UiClose, UiCollapseAll, UiExpandAll, UiSearch } from \"../icons\";\nimport { TreeNode, type TreeNodeProps } from \"./TreeNode\";\n\nexport type TreeProps<T> = Omit<\n TreeNodeProps<T>,\n \"node\" | \"depth\" | \"expandAll\" | \"forcedOpenKeys\"\n> & {\n /** Root nodes to render. */\n roots: T[];\n /** Empty-state content when no roots are provided. */\n empty?: ReactNode;\n /** Classes applied to the tree root. */\n className?: string;\n /**\n * Render the Expand all / Collapse all toolbar above the tree. Enabled by\n * default. Set to false to suppress when the tree is tiny or the controls\n * are owned externally.\n */\n showControls?: boolean;\n /**\n * Externally controlled expand-all state. When provided together with\n * `onExpandAllChange`, the tree is controlled; otherwise it manages the\n * toolbar state internally.\n */\n expandAll?: boolean | null;\n /** Called when the toolbar expand-all state changes. */\n onExpandAllChange?: (next: boolean | null) => void;\n /** Classes applied to the search/control toolbar. */\n toolbarClassName?: string;\n /**\n * Override the text the filter matches against for a given node.\n * Defaults to a recursive walk of the node's own fields (excluding\n * its `children` and any secondary children), which works for\n * plain-shape trees but leaks text from nested domain objects —\n * e.g. a SQL Table's Field rows are marked secondary, yet the\n * parent record still carries `record.fields[].field` strings that\n * the default walker would collect. Callers that need precise\n * control over search behaviour should return only the text they\n * want surfaced (the node's own display label / id / type).\n */\n getSearchText?: (node: T) => string;\n};\n\nfunction countTreeEdges<T>(roots: T[], getChildren: TreeProps<T>[\"getChildren\"]) {\n let total = 0;\n const stack = [...roots];\n\n while (stack.length > 0) {\n const node = stack.pop()!;\n const children = getChildren(node) ?? [];\n total += children.length;\n stack.push(...children);\n }\n\n return total;\n}\n\nfunction collectTreeSearchText(\n value: unknown,\n excluded = new Set<unknown>(),\n seen = new Set<unknown>(),\n): string {\n if (value == null || excluded.has(value)) return \"\";\n\n if (\n typeof value === \"string\" ||\n typeof value === \"number\" ||\n typeof value === \"boolean\" ||\n typeof value === \"bigint\"\n ) {\n return String(value);\n }\n\n if (typeof value !== \"object\") return \"\";\n if (seen.has(value)) return \"\";\n seen.add(value);\n\n if (Array.isArray(value)) {\n return value\n .map((entry) => collectTreeSearchText(entry, excluded, seen))\n .filter(Boolean)\n .join(\" \");\n }\n\n const preferredKeys = [\"label\", \"name\", \"title\", \"text\", \"plain\", \"id\", \"content\"];\n const record = value as Record<string, unknown>;\n const chunks: string[] = [];\n\n for (const key of preferredKeys) {\n if (!(key in record)) continue;\n const text = collectTreeSearchText(record[key], excluded, seen);\n if (text) chunks.push(text);\n }\n\n for (const [key, entry] of Object.entries(record)) {\n if (key === \"children\" || preferredKeys.includes(key)) continue;\n const text = collectTreeSearchText(entry, excluded, seen);\n if (text) chunks.push(text);\n }\n\n return chunks.join(\" \");\n}\n\nfunction nodeSearchText<T>(node: T, children: ReturnType<TreeProps<T>[\"getChildren\"]>) {\n const excluded = new Set<unknown>();\n if (children) excluded.add(children);\n return collectTreeSearchText(node, excluded).trim().toLowerCase();\n}\n\nfunction filterTreeRoots<T>(\n roots: T[],\n getChildren: TreeProps<T>[\"getChildren\"],\n getKey: TreeProps<T>[\"getKey\"],\n query: string,\n isSecondary?: (node: T) => boolean,\n getSearchText?: (node: T) => string,\n) {\n const normalizedQuery = query.trim().toLowerCase();\n if (!normalizedQuery) {\n return {\n roots,\n filteredChildren: null as Map<string | number, T[]> | null,\n forcedOpenKeys: null as Set<string | number> | null,\n };\n }\n\n const filteredChildren = new Map<string | number, T[]>();\n const forcedOpenKeys = new Set<string | number>();\n\n function matchText(node: T, children: T[]): string {\n // Honour the caller-supplied text extractor first — it's the\n // only way to get precise control over what the filter sees when\n // the node carries domain data whose fields would otherwise leak\n // into the default walker.\n if (getSearchText) return getSearchText(node).trim().toLowerCase();\n // Match-text skips secondary children so e.g. a Table's search\n // text only draws from the Table itself, not from its Field\n // rows. The Table is primary, so its own text still counts.\n const searchChildren = children.filter((c) => !isSecondary?.(c));\n return nodeSearchText(node, searchChildren);\n }\n\n function visit(node: T): boolean {\n // A secondary node never drives a filter match — it's\n // effectively invisible to search. Still rendered when the\n // caller opens the parent manually. This is what lets a 28k-row\n // field tree stay searchable at the record level without \"field\n // name contains 'x'\" matches spamming the results.\n if (isSecondary?.(node)) return false;\n\n const key = getKey(node);\n const children = getChildren(node) ?? [];\n const childrenAllSecondary =\n children.length > 0 && children.every((child) => isSecondary?.(child));\n const matches = matchText(node, children).includes(normalizedQuery);\n const visibleChildren: T[] = [];\n\n for (const child of children) {\n if (visit(child)) visibleChildren.push(child);\n }\n\n if (matches) {\n if (children.length > 0) {\n filteredChildren.set(key, children);\n if (!childrenAllSecondary) forcedOpenKeys.add(key);\n }\n return true;\n }\n\n if (visibleChildren.length > 0) {\n filteredChildren.set(key, visibleChildren);\n forcedOpenKeys.add(key);\n return true;\n }\n\n return false;\n }\n\n return {\n roots: roots.filter((root) => visit(root)),\n filteredChildren,\n forcedOpenKeys,\n };\n}\n\nexport function Tree<T>({\n roots,\n empty,\n className,\n showControls = true,\n expandAll: controlledExpandAll,\n onExpandAllChange,\n toolbarClassName,\n getSearchText,\n ...nodeProps\n}: TreeProps<T>) {\n const [internalExpandAll, setInternalExpandAll] = useState<boolean | null>(null);\n const [filterQuery, setFilterQuery] = useState(\"\");\n const basePaddingPx = nodeProps.basePaddingPx ?? 8;\n const isControlled = onExpandAllChange !== undefined;\n const expandAll = isControlled ? (controlledExpandAll ?? null) : internalExpandAll;\n const totalEdges = useMemo(\n () => countTreeEdges(roots, nodeProps.getChildren),\n [roots, nodeProps.getChildren],\n );\n const showFilter = totalEdges > 20;\n const activeFilter = showFilter ? filterQuery : \"\";\n const filteredTree = useMemo(\n () =>\n filterTreeRoots(\n roots,\n nodeProps.getChildren,\n nodeProps.getKey,\n activeFilter,\n nodeProps.isSecondary,\n getSearchText,\n ),\n [\n roots,\n nodeProps.getChildren,\n nodeProps.getKey,\n activeFilter,\n nodeProps.isSecondary,\n getSearchText,\n ],\n );\n const treeRoots = filteredTree.roots;\n const filteredChildren = filteredTree.filteredChildren;\n const forcedOpenKeys = filteredTree.forcedOpenKeys;\n const effectiveGetChildren = filteredChildren\n ? (node: T) => filteredChildren.get(nodeProps.getKey(node)) ?? []\n : nodeProps.getChildren;\n const effectiveExpandAll = expandAll;\n const effectiveEmpty = activeFilter.trim() ? (\n <div className=\"px-3 py-4 text-sm text-muted-foreground\">No matching tree nodes.</div>\n ) : (\n empty\n );\n const showControlButtons = showControls && roots.length > 0;\n const showVirtualRow = showFilter || showControlButtons;\n const setExpandAll = (next: boolean | null) => {\n if (isControlled) onExpandAllChange?.(next);\n else setInternalExpandAll(next);\n };\n\n if (treeRoots.length === 0 && !showVirtualRow) return <>{effectiveEmpty ?? null}</>;\n\n return (\n <div className={cn(\"flex flex-col min-h-0\", className)}>\n <div role=\"tree\" className=\"min-h-0 flex-1 overflow-auto\">\n {showVirtualRow && (\n <div\n role=\"presentation\"\n className={cn(\n \"sticky top-0 z-10 flex items-center gap-1.5 border-b border-border/70 bg-background/95 py-1 pr-2 text-sm backdrop-blur supports-[backdrop-filter]:bg-background/80\",\n toolbarClassName,\n )}\n style={{ paddingLeft: `${basePaddingPx}px` }}\n >\n {showFilter ? (\n <Icon icon={UiSearch} className=\"w-3 shrink-0 text-xs text-muted-foreground\" />\n ) : (\n <span className=\"w-3 shrink-0\" aria-hidden />\n )}\n\n {showFilter ? (\n <label className=\"flex min-w-0 flex-1 items-center gap-2 rounded-md border border-border bg-background px-2 py-1 text-xs text-foreground shadow-sm\">\n <input\n type=\"search\"\n value={filterQuery}\n onChange={(event) => setFilterQuery(event.target.value)}\n placeholder=\"Filter tree\"\n className=\"h-5 w-full border-0 bg-transparent p-0 text-xs outline-none placeholder:text-muted-foreground\"\n aria-label=\"Filter tree nodes\"\n />\n {filterQuery && (\n <button\n type=\"button\"\n onClick={() => setFilterQuery(\"\")}\n className=\"inline-flex items-center rounded p-0.5 text-muted-foreground hover:bg-accent hover:text-accent-foreground\"\n aria-label=\"Clear tree filter\"\n title=\"Clear tree filter\"\n >\n <Icon icon={UiClose} className=\"text-xs\" />\n </button>\n )}\n </label>\n ) : (\n <span className=\"flex-1\" />\n )}\n\n {showControlButtons && (\n <div className=\"ml-auto flex items-center gap-1\">\n <button\n type=\"button\"\n onClick={() => setExpandAll(true)}\n aria-pressed={effectiveExpandAll === true}\n aria-label=\"Expand all\"\n title=\"Expand all\"\n disabled={activeFilter.trim().length > 0}\n className={cn(\n \"inline-flex h-6 w-6 items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-accent-foreground disabled:cursor-not-allowed disabled:opacity-50\",\n effectiveExpandAll === true && \"bg-accent text-accent-foreground\",\n )}\n >\n <Icon icon={UiExpandAll} className=\"text-sm\" />\n </button>\n <button\n type=\"button\"\n onClick={() => setExpandAll(false)}\n aria-pressed={effectiveExpandAll === false}\n aria-label=\"Collapse all\"\n title=\"Collapse all\"\n disabled={activeFilter.trim().length > 0}\n className={cn(\n \"inline-flex h-6 w-6 items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-accent-foreground disabled:cursor-not-allowed disabled:opacity-50\",\n effectiveExpandAll === false && \"bg-accent text-accent-foreground\",\n )}\n >\n <Icon icon={UiCollapseAll} className=\"text-sm\" />\n </button>\n </div>\n )}\n </div>\n )}\n\n {treeRoots.length === 0 ? (\n (effectiveEmpty ?? null)\n ) : (\n <>\n {treeRoots.map((root) => (\n <TreeNode<T>\n {...nodeProps}\n key={nodeProps.getKey(root)}\n node={root}\n expandAll={effectiveExpandAll}\n forcedOpenKeys={forcedOpenKeys}\n getChildren={effectiveGetChildren}\n />\n ))}\n </>\n )}\n </div>\n </div>\n );\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Tree.js","sources":["../../src/data/Tree.tsx"],"sourcesContent":["import { useMemo, useState, type ReactNode } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { Icon } from \"./Icon\";\nimport { UiClose, UiCollapseAll, UiExpandAll, UiSearch } from \"../icons\";\nimport { TreeNode, type TreeNodeProps } from \"./TreeNode\";\n\nexport type TreeProps<T> = Omit<\n TreeNodeProps<T>,\n \"node\" | \"depth\" | \"expandAll\" | \"forcedOpenKeys\"\n> & {\n /** Root nodes to render. */\n roots: T[];\n /** Empty-state content when no roots are provided. */\n empty?: ReactNode;\n /** Classes applied to the tree root. */\n className?: string;\n /**\n * Render the Expand all / Collapse all toolbar above the tree. Enabled by\n * default. Set to false to suppress when the tree is tiny or the controls\n * are owned externally.\n */\n showControls?: boolean;\n /**\n * Externally controlled expand-all state. When provided together with\n * `onExpandAllChange`, the tree is controlled; otherwise it manages the\n * toolbar state internally.\n */\n expandAll?: boolean | null;\n /** Called when the toolbar expand-all state changes. */\n onExpandAllChange?: (next: boolean | null) => void;\n /** Classes applied to the search/control toolbar. */\n toolbarClassName?: string;\n /**\n * Override the text the filter matches against for a given node.\n * Defaults to a recursive walk of the node's own fields (excluding\n * its `children` and any secondary children), which works for\n * plain-shape trees but leaks text from nested domain objects —\n * e.g. a SQL Table's Field rows are marked secondary, yet the\n * parent record still carries `record.fields[].field` strings that\n * the default walker would collect. Callers that need precise\n * control over search behaviour should return only the text they\n * want surfaced (the node's own display label / id / type).\n */\n getSearchText?: (node: T) => string;\n};\n\n// Edge counting, search-text collection and filtering all walk the tree through\n// the synchronous `getChildren` only. A lazy node (one declared via\n// `hasMoreChildren` whose children have not been fetched yet) therefore\n// contributes no edges and no search text until the operator expands it — so it\n// is invisible to \"expand all\" and to the filter while still collapsed. This is\n// intentional: we never trigger network loads from bulk/search operations.\nfunction countTreeEdges<T>(roots: T[], getChildren: TreeProps<T>[\"getChildren\"]) {\n let total = 0;\n const stack = [...roots];\n\n while (stack.length > 0) {\n const node = stack.pop()!;\n const children = getChildren(node) ?? [];\n total += children.length;\n stack.push(...children);\n }\n\n return total;\n}\n\nfunction collectTreeSearchText(\n value: unknown,\n excluded = new Set<unknown>(),\n seen = new Set<unknown>(),\n): string {\n if (value == null || excluded.has(value)) return \"\";\n\n if (\n typeof value === \"string\" ||\n typeof value === \"number\" ||\n typeof value === \"boolean\" ||\n typeof value === \"bigint\"\n ) {\n return String(value);\n }\n\n if (typeof value !== \"object\") return \"\";\n if (seen.has(value)) return \"\";\n seen.add(value);\n\n if (Array.isArray(value)) {\n return value\n .map((entry) => collectTreeSearchText(entry, excluded, seen))\n .filter(Boolean)\n .join(\" \");\n }\n\n const preferredKeys = [\"label\", \"name\", \"title\", \"text\", \"plain\", \"id\", \"content\"];\n const record = value as Record<string, unknown>;\n const chunks: string[] = [];\n\n for (const key of preferredKeys) {\n if (!(key in record)) continue;\n const text = collectTreeSearchText(record[key], excluded, seen);\n if (text) chunks.push(text);\n }\n\n for (const [key, entry] of Object.entries(record)) {\n if (key === \"children\" || preferredKeys.includes(key)) continue;\n const text = collectTreeSearchText(entry, excluded, seen);\n if (text) chunks.push(text);\n }\n\n return chunks.join(\" \");\n}\n\nfunction nodeSearchText<T>(node: T, children: ReturnType<TreeProps<T>[\"getChildren\"]>) {\n const excluded = new Set<unknown>();\n if (children) excluded.add(children);\n return collectTreeSearchText(node, excluded).trim().toLowerCase();\n}\n\nfunction filterTreeRoots<T>(\n roots: T[],\n getChildren: TreeProps<T>[\"getChildren\"],\n getKey: TreeProps<T>[\"getKey\"],\n query: string,\n isSecondary?: (node: T) => boolean,\n getSearchText?: (node: T) => string,\n) {\n const normalizedQuery = query.trim().toLowerCase();\n if (!normalizedQuery) {\n return {\n roots,\n filteredChildren: null as Map<string | number, T[]> | null,\n forcedOpenKeys: null as Set<string | number> | null,\n };\n }\n\n const filteredChildren = new Map<string | number, T[]>();\n const forcedOpenKeys = new Set<string | number>();\n\n function matchText(node: T, children: T[]): string {\n // Honour the caller-supplied text extractor first — it's the\n // only way to get precise control over what the filter sees when\n // the node carries domain data whose fields would otherwise leak\n // into the default walker.\n if (getSearchText) return getSearchText(node).trim().toLowerCase();\n // Match-text skips secondary children so e.g. a Table's search\n // text only draws from the Table itself, not from its Field\n // rows. The Table is primary, so its own text still counts.\n const searchChildren = children.filter((c) => !isSecondary?.(c));\n return nodeSearchText(node, searchChildren);\n }\n\n function visit(node: T): boolean {\n // A secondary node never drives a filter match — it's\n // effectively invisible to search. Still rendered when the\n // caller opens the parent manually. This is what lets a 28k-row\n // field tree stay searchable at the record level without \"field\n // name contains 'x'\" matches spamming the results.\n if (isSecondary?.(node)) return false;\n\n const key = getKey(node);\n const children = getChildren(node) ?? [];\n const childrenAllSecondary =\n children.length > 0 && children.every((child) => isSecondary?.(child));\n const matches = matchText(node, children).includes(normalizedQuery);\n const visibleChildren: T[] = [];\n\n for (const child of children) {\n if (visit(child)) visibleChildren.push(child);\n }\n\n if (matches) {\n if (children.length > 0) {\n filteredChildren.set(key, children);\n if (!childrenAllSecondary) forcedOpenKeys.add(key);\n }\n return true;\n }\n\n if (visibleChildren.length > 0) {\n filteredChildren.set(key, visibleChildren);\n forcedOpenKeys.add(key);\n return true;\n }\n\n return false;\n }\n\n return {\n roots: roots.filter((root) => visit(root)),\n filteredChildren,\n forcedOpenKeys,\n };\n}\n\nexport function Tree<T>({\n roots,\n empty,\n className,\n showControls = true,\n expandAll: controlledExpandAll,\n onExpandAllChange,\n toolbarClassName,\n getSearchText,\n ...nodeProps\n}: TreeProps<T>) {\n const [internalExpandAll, setInternalExpandAll] = useState<boolean | null>(null);\n const [filterQuery, setFilterQuery] = useState(\"\");\n const basePaddingPx = nodeProps.basePaddingPx ?? 8;\n const isControlled = onExpandAllChange !== undefined;\n const expandAll = isControlled ? (controlledExpandAll ?? null) : internalExpandAll;\n const totalEdges = useMemo(\n () => countTreeEdges(roots, nodeProps.getChildren),\n [roots, nodeProps.getChildren],\n );\n const showFilter = totalEdges > 20;\n const activeFilter = showFilter ? filterQuery : \"\";\n const filteredTree = useMemo(\n () =>\n filterTreeRoots(\n roots,\n nodeProps.getChildren,\n nodeProps.getKey,\n activeFilter,\n nodeProps.isSecondary,\n getSearchText,\n ),\n [\n roots,\n nodeProps.getChildren,\n nodeProps.getKey,\n activeFilter,\n nodeProps.isSecondary,\n getSearchText,\n ],\n );\n const treeRoots = filteredTree.roots;\n const filteredChildren = filteredTree.filteredChildren;\n const forcedOpenKeys = filteredTree.forcedOpenKeys;\n const effectiveGetChildren = filteredChildren\n ? (node: T) => filteredChildren.get(nodeProps.getKey(node)) ?? []\n : nodeProps.getChildren;\n const effectiveExpandAll = expandAll;\n const effectiveEmpty = activeFilter.trim() ? (\n <div className=\"px-3 py-4 text-sm text-muted-foreground\">No matching tree nodes.</div>\n ) : (\n empty\n );\n const showControlButtons = showControls && roots.length > 0;\n const showVirtualRow = showFilter || showControlButtons;\n const setExpandAll = (next: boolean | null) => {\n if (isControlled) onExpandAllChange?.(next);\n else setInternalExpandAll(next);\n };\n\n if (treeRoots.length === 0 && !showVirtualRow) return <>{effectiveEmpty ?? null}</>;\n\n return (\n <div className={cn(\"flex flex-col min-h-0\", className)}>\n <div role=\"tree\" className=\"min-h-0 flex-1 overflow-auto\">\n {showVirtualRow && (\n <div\n role=\"presentation\"\n className={cn(\n \"sticky top-0 z-10 flex items-center gap-1.5 border-b border-border/70 bg-background/95 py-1 pr-2 text-sm backdrop-blur supports-[backdrop-filter]:bg-background/80\",\n toolbarClassName,\n )}\n style={{ paddingLeft: `${basePaddingPx}px` }}\n >\n {showFilter ? (\n <Icon icon={UiSearch} className=\"w-3 shrink-0 text-xs text-muted-foreground\" />\n ) : (\n <span className=\"w-3 shrink-0\" aria-hidden />\n )}\n\n {showFilter ? (\n <label className=\"flex min-w-0 flex-1 items-center gap-2 rounded-md border border-border bg-background px-2 py-1 text-xs text-foreground shadow-sm\">\n <input\n type=\"search\"\n value={filterQuery}\n onChange={(event) => setFilterQuery(event.target.value)}\n placeholder=\"Filter tree\"\n className=\"h-5 w-full border-0 bg-transparent p-0 text-xs outline-none placeholder:text-muted-foreground\"\n aria-label=\"Filter tree nodes\"\n />\n {filterQuery && (\n <button\n type=\"button\"\n onClick={() => setFilterQuery(\"\")}\n className=\"inline-flex items-center rounded p-0.5 text-muted-foreground hover:bg-accent hover:text-accent-foreground\"\n aria-label=\"Clear tree filter\"\n title=\"Clear tree filter\"\n >\n <Icon icon={UiClose} className=\"text-xs\" />\n </button>\n )}\n </label>\n ) : (\n <span className=\"flex-1\" />\n )}\n\n {showControlButtons && (\n <div className=\"ml-auto flex items-center gap-1\">\n <button\n type=\"button\"\n onClick={() => setExpandAll(true)}\n aria-pressed={effectiveExpandAll === true}\n aria-label=\"Expand all\"\n title=\"Expand all\"\n disabled={activeFilter.trim().length > 0}\n className={cn(\n \"inline-flex h-6 w-6 items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-accent-foreground disabled:cursor-not-allowed disabled:opacity-50\",\n effectiveExpandAll === true && \"bg-accent text-accent-foreground\",\n )}\n >\n <Icon icon={UiExpandAll} className=\"text-sm\" />\n </button>\n <button\n type=\"button\"\n onClick={() => setExpandAll(false)}\n aria-pressed={effectiveExpandAll === false}\n aria-label=\"Collapse all\"\n title=\"Collapse all\"\n disabled={activeFilter.trim().length > 0}\n className={cn(\n \"inline-flex h-6 w-6 items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-accent-foreground disabled:cursor-not-allowed disabled:opacity-50\",\n effectiveExpandAll === false && \"bg-accent text-accent-foreground\",\n )}\n >\n <Icon icon={UiCollapseAll} className=\"text-sm\" />\n </button>\n </div>\n )}\n </div>\n )}\n\n {treeRoots.length === 0 ? (\n (effectiveEmpty ?? null)\n ) : (\n <>\n {treeRoots.map((root) => (\n <TreeNode<T>\n {...nodeProps}\n key={nodeProps.getKey(root)}\n node={root}\n expandAll={effectiveExpandAll}\n forcedOpenKeys={forcedOpenKeys}\n getChildren={effectiveGetChildren}\n />\n ))}\n </>\n )}\n </div>\n </div>\n );\n}\n"],"names":[],"mappings":"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"}
|