UNPKG

@flanksource/clicky-ui

Version:

Flanksource Clicky UI — React component library built on shadcn/ui with light/dark and density theming.

446 lines (445 loc) 18.3 kB
import { jsx, jsxs, Fragment } from "react/jsx-runtime"; import { useState, useMemo, useEffect } from "react"; import { Icon } from "../Icon.js"; import { processLabel, processStateIcon, processStateColor, formatBytes } from "./utils.js"; import { parseStackDump, countStackByState } from "./stacktrace.js"; import { GoroutineCard, goroutineStateDot } from "./GoroutineCard.js"; import { ThreadCard, threadStateDot } from "./ThreadCard.js"; const STACK_MIN_LINES = 10; const STACK_LINE_HEIGHT_REM = 1; const STACK_MIN_HEIGHT = `${STACK_MIN_LINES * STACK_LINE_HEIGHT_REM}rem`; function cpuTone(pct) { if (pct >= 90) return "danger"; if (pct >= 60) return "warning"; if (pct > 0) return "success"; return "neutral"; } function memoryTone(rss, vms) { if (!rss || !vms) return "neutral"; const ratio = rss / vms; if (ratio >= 0.9) return "danger"; if (ratio >= 0.6) return "warning"; return "info"; } function DiagnosticsDetailPanel({ process, collectBusy, onCollectStack, runMeta }) { const [search, setSearch] = useState(""); const [selectedStates, setSelectedStates] = useState(/* @__PURE__ */ new Set()); const [hideRuntimeOnly, setHideRuntimeOnly] = useState(true); const stack = process == null ? void 0 : process.stack_capture; const parsed = useMemo(() => parseStackDump((stack == null ? void 0 : stack.text) || ""), [stack == null ? void 0 : stack.text]); const stateCounts = useMemo(() => countStackByState(parsed), [parsed]); const filtered = useMemo(() => { const needle = search.trim().toLowerCase(); const items = parsed.format === "jvm" ? parsed.threads : parsed.format === "go" ? parsed.goroutines : []; return items.filter((item) => { if (selectedStates.size > 0 && !selectedStates.has(item.state)) return false; if (hideRuntimeOnly && item.userFrameCount === 0) return false; if (needle && !item.searchText.includes(needle)) return false; return true; }); }, [parsed, search, selectedStates, hideRuntimeOnly]); useEffect(() => { setSearch(""); setSelectedStates(/* @__PURE__ */ new Set()); setHideRuntimeOnly(true); }, [stack == null ? void 0 : stack.text, process == null ? void 0 : process.pid]); if (!process) { return /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center h-full text-muted-foreground text-sm", children: /* @__PURE__ */ jsxs("div", { className: "text-center", children: [ /* @__PURE__ */ jsx(Icon, { name: "codicon:server-process", className: "text-4xl mb-density-2" }), /* @__PURE__ */ jsx("div", { children: "Select a process to view diagnostics" }) ] }) }); } return /* @__PURE__ */ jsxs("div", { className: "h-full min-h-0 flex flex-col gap-density-3 p-density-4", children: [ /* @__PURE__ */ jsx(Header, { process }), runMeta && /* @__PURE__ */ jsx(RunSection, { runMeta }), process.command && /* @__PURE__ */ jsx(PanelSection, { title: "Command", children: /* @__PURE__ */ jsx("pre", { className: "text-xs text-foreground whitespace-pre-wrap font-mono bg-blue-50 dark:bg-blue-500/10 rounded p-2 break-all", children: process.command }) }), /* @__PURE__ */ jsx(PanelSection, { title: "Metrics", children: /* @__PURE__ */ jsx(ProcessMetrics, { process }) }), /* @__PURE__ */ jsx(PanelSection, { title: "Stack", grow: true, children: /* @__PURE__ */ jsx( StackBlock, { process, parsed, stateCounts, filtered, search, setSearch, selectedStates, setSelectedStates, hideRuntimeOnly, setHideRuntimeOnly, collectBusy: collectBusy ?? false, ...onCollectStack ? { onCollectStack } : {} } ) }) ] }); } function Header({ process }) { return /* @__PURE__ */ jsx("div", { className: "flex items-start justify-between gap-density-3", children: /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-density-2", children: [ /* @__PURE__ */ jsx( Icon, { name: process.is_root ? "codicon:server-process" : "codicon:debug-alt", className: "text-2xl text-blue-600" } ), /* @__PURE__ */ jsx("h2", { className: "text-lg font-bold text-foreground break-words", children: processLabel(process) }) ] }), /* @__PURE__ */ jsxs("div", { className: "mt-1 flex items-center gap-density-2 flex-wrap text-xs text-muted-foreground", children: [ /* @__PURE__ */ jsxs("span", { className: "font-mono", children: [ "pid ", process.pid ] }), process.ppid ? /* @__PURE__ */ jsxs("span", { className: "font-mono", children: [ "ppid ", process.ppid ] }) : null, process.status ? /* @__PURE__ */ jsxs( "span", { className: `inline-flex items-center gap-1 rounded-full bg-muted px-2 py-0.5 ${processStateColor( process.status )}`, children: [ /* @__PURE__ */ jsx(Icon, { name: processStateIcon(process.status) }), process.status ] } ) : null ] }) ] }) }); } function RunSection({ runMeta }) { return /* @__PURE__ */ jsx(PanelSection, { title: "Run", children: /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-density-2 text-sm", children: [ /* @__PURE__ */ jsx( InfoTile, { label: runMeta.kind === "rerun" ? `Rerun #${runMeta.sequence}` : "Initial run", value: runMeta.started ? new Date(runMeta.started).toLocaleString() : "Unavailable" } ), /* @__PURE__ */ jsx( InfoTile, { label: "Finished", value: runMeta.ended ? new Date(runMeta.ended).toLocaleString() : "In progress" } ) ] }) }); } function stackItemCount(parsed) { if (parsed.format === "jvm") return parsed.threads.length; if (parsed.format === "go") return parsed.goroutines.length; return 0; } function stackItemLabel(parsed) { if (parsed.format === "jvm") return "threads"; if (parsed.format === "go") return "goroutines"; return "frames"; } function stackStateDot(parsed, state) { return parsed.format === "jvm" ? threadStateDot(state) : goroutineStateDot(state); } function StackBlock(props) { const { process, parsed, stateCounts, filtered, search, setSearch, selectedStates, setSelectedStates, hideRuntimeOnly, setHideRuntimeOnly, collectBusy, onCollectStack } = props; const stack = process.stack_capture; if (!(stack == null ? void 0 : stack.text)) { return /* @__PURE__ */ jsxs("div", { className: "h-full min-h-[14rem] flex flex-col justify-center gap-density-3 p-density-3", children: [ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [ /* @__PURE__ */ jsx( "div", { className: `h-3 w-40 rounded ${collectBusy ? "animate-pulse bg-muted" : "bg-muted/70"}` } ), /* @__PURE__ */ jsx( "div", { className: `h-3 w-full rounded ${collectBusy ? "animate-pulse bg-muted" : "bg-muted/70"}` } ), /* @__PURE__ */ jsx( "div", { className: `h-3 w-5/6 rounded ${collectBusy ? "animate-pulse bg-muted" : "bg-muted/70"}` } ) ] }), /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-density-3", children: [ /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: (stack == null ? void 0 : stack.error) ? stack.error : collectBusy ? "Collecting stack trace..." : "No stack trace collected yet." }), onCollectStack && /* @__PURE__ */ jsx( CollectButton, { pid: process.pid, busy: collectBusy ?? false, onClick: onCollectStack, primary: true } ) ] }) ] }); } return /* @__PURE__ */ jsxs("div", { className: "h-full min-h-0 flex flex-col", children: [ /* @__PURE__ */ jsxs("div", { className: "px-1 py-1.5 space-y-density-2", children: [ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-density-2 text-[11px] text-muted-foreground flex-wrap", children: [ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-density-2 flex-wrap", children: [ /* @__PURE__ */ jsx(StackStatusBadge, { status: stack.status }), stack.collected_at && /* @__PURE__ */ jsx("span", { children: new Date(stack.collected_at).toLocaleString() }), stackItemCount(parsed) > 0 && /* @__PURE__ */ jsxs("span", { children: [ filtered.length, " / ", stackItemCount(parsed), " ", stackItemLabel(parsed) ] }) ] }), onCollectStack && /* @__PURE__ */ jsx(CollectButton, { pid: process.pid, busy: collectBusy ?? false, onClick: onCollectStack }) ] }), stackItemCount(parsed) > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 flex-wrap", children: [ /* @__PURE__ */ jsxs("div", { className: "relative min-w-[14rem] flex-1", children: [ /* @__PURE__ */ jsx( Icon, { name: "codicon:search", className: "absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground text-xs" } ), /* @__PURE__ */ jsx( "input", { className: "w-full rounded-md border border-border bg-muted/50 py-1 pl-7 pr-2 text-xs outline-none focus:border-primary focus:bg-background", placeholder: parsed.format === "jvm" ? "Filter by thread name, function, or file" : "Filter by goroutine id, function, or file", value: search, onChange: (e) => setSearch(e.target.value) } ) ] }), /* @__PURE__ */ jsxs("label", { className: "inline-flex items-center gap-1.5 rounded-md border border-border bg-muted/50 px-2 py-1 text-[11px] text-muted-foreground", children: [ /* @__PURE__ */ jsx( "input", { type: "checkbox", checked: hideRuntimeOnly, onChange: (e) => setHideRuntimeOnly(e.target.checked) } ), "Hide runtime-only" ] }), (search || selectedStates.size > 0 || !hideRuntimeOnly) && /* @__PURE__ */ jsx( "button", { className: "text-[11px] text-muted-foreground hover:text-foreground", onClick: () => { setSearch(""); setSelectedStates(() => /* @__PURE__ */ new Set()); setHideRuntimeOnly(true); }, children: "Clear" } ) ] }), /* @__PURE__ */ jsx("div", { className: "flex items-center gap-1 flex-wrap", children: Array.from(stateCounts.entries()).sort((a, b) => b[1] - a[1]).map(([state, count]) => { const active = selectedStates.has(state); return /* @__PURE__ */ jsxs( "button", { className: `inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[11px] transition-colors ${active ? "border-primary bg-primary/10 text-primary" : "border-border bg-muted/50 text-muted-foreground hover:bg-background"}`, onClick: () => { setSelectedStates((prev) => { const next = new Set(prev); if (next.has(state)) next.delete(state); else next.add(state); return next; }); }, children: [ /* @__PURE__ */ jsx("span", { className: `h-2 w-2 rounded-full ${stackStateDot(parsed, state)}` }), state, /* @__PURE__ */ jsx("span", { className: "text-[10px] opacity-70", children: count }) ] }, state ); }) }) ] }) ] }), stack.error && /* @__PURE__ */ jsx("div", { className: "mt-density-2 text-xs text-red-600 whitespace-pre-wrap", children: stack.error }), stackItemCount(parsed) === 0 ? /* @__PURE__ */ jsx( "pre", { className: "flex-1 min-h-0 overflow-auto py-1 text-[11px] text-foreground whitespace-pre-wrap font-mono leading-4", style: { minHeight: STACK_MIN_HEIGHT }, children: stack.text } ) : /* @__PURE__ */ jsxs( "div", { className: "flex-1 min-h-0 overflow-auto py-1 space-y-1", style: { minHeight: STACK_MIN_HEIGHT }, children: [ filtered.length === 0 && /* @__PURE__ */ jsxs("div", { className: "py-density-3 text-center text-xs text-muted-foreground", children: [ "No ", stackItemLabel(parsed), " match the current filters." ] }), parsed.format === "jvm" && parsed.threads.filter((t) => filtered.includes(t)).map((t) => /* @__PURE__ */ jsx( ThreadCard, { thread: t, search, hideRuntimeOnly }, t.id )), parsed.format === "go" && parsed.goroutines.filter((g) => filtered.includes(g)).map((goroutine) => /* @__PURE__ */ jsx( GoroutineCard, { goroutine, search, hideRuntimeOnly }, goroutine.id )) ] } ) ] }); } function StackStatusBadge({ status }) { const cls = status === "ready" ? "bg-green-100 text-green-700 dark:bg-green-500/20 dark:text-green-300" : status === "unsupported" ? "bg-yellow-100 text-yellow-700 dark:bg-yellow-500/20 dark:text-yellow-300" : "bg-red-100 text-red-700 dark:bg-red-500/20 dark:text-red-300"; return /* @__PURE__ */ jsx("span", { className: `px-2 py-0.5 rounded-full ${cls}`, children: status }); } function CollectButton({ pid, busy, onClick, primary }) { const base = "shrink-0 text-[11px] px-2 py-1 rounded disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1"; const style = primary ? "bg-primary text-primary-foreground hover:bg-primary/90" : "border border-border text-muted-foreground hover:bg-accent"; return /* @__PURE__ */ jsxs( "button", { className: `${base} ${style}`, onClick: () => onClick(pid), disabled: busy, title: primary ? "Collect the latest stack trace" : "Refresh stack trace", children: [ /* @__PURE__ */ jsx( Icon, { name: busy ? "svg-spinners:ring-resize" : primary ? "codicon:debug-alt-small" : "codicon:refresh" } ), busy ? "Collecting..." : primary ? "Collect stack trace" : "Refresh" ] } ); } function PanelSection({ title, grow, children }) { return /* @__PURE__ */ jsxs("section", { className: grow ? "flex min-h-0 flex-1 flex-col" : "", children: [ /* @__PURE__ */ jsx("div", { className: "text-[11px] font-semibold uppercase tracking-wide text-muted-foreground mb-1.5", children: title }), grow ? /* @__PURE__ */ jsx("div", { className: "flex-1 min-h-0 overflow-hidden", children }) : children ] }); } function InfoTile({ label, value }) { return /* @__PURE__ */ jsxs("div", { className: "border border-border rounded-lg bg-muted/50 px-2.5 py-density-2", children: [ /* @__PURE__ */ jsx("div", { className: "text-[10px] uppercase tracking-wide text-muted-foreground", children: label }), /* @__PURE__ */ jsx("div", { className: "text-xs font-medium text-foreground mt-0.5", children: value }) ] }); } function ProcessMetrics({ process }) { const cpu = process.cpu_percent || 0; const rss = process.rss; const vms = process.vms; const memRatioPct = rss && vms && vms > 0 ? Math.min(100, Math.round(rss / vms * 100)) : 0; return /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-4 gap-density-1", children: [ /* @__PURE__ */ jsx( MetricTile, { label: "CPU", value: `${cpu.toFixed(1)}%`, barPct: Math.min(100, cpu), tone: cpuTone(cpu) } ), /* @__PURE__ */ jsx( MetricTile, { label: "RSS", value: rss !== void 0 ? formatBytes(rss) : "n/a", barPct: memRatioPct, tone: memoryTone(rss, vms) } ), /* @__PURE__ */ jsx(MetricTile, { label: "VMS", value: vms !== void 0 ? formatBytes(vms) : "n/a", tone: "neutral" }), /* @__PURE__ */ jsx( MetricTile, { label: "Files", value: process.open_files !== void 0 ? String(process.open_files) : "n/a", barPct: process.open_files ? Math.min(100, process.open_files / 1024 * 100) : 0, tone: "info" } ) ] }); } function MetricTile({ label, value, barPct, tone = "neutral" }) { const toneBar = { neutral: "bg-muted-foreground/40", success: "bg-green-500", warning: "bg-yellow-500", danger: "bg-red-500", info: "bg-blue-500" }; const toneText = { neutral: "text-foreground", success: "text-green-600 dark:text-green-400", warning: "text-yellow-600 dark:text-yellow-400", danger: "text-red-600 dark:text-red-400", info: "text-blue-600 dark:text-blue-400" }; return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-0.5 rounded-md border border-border bg-background px-density-2 py-1 min-w-0", children: [ /* @__PURE__ */ jsx("span", { className: "text-[9px] uppercase tracking-wide text-muted-foreground leading-none", children: label }), /* @__PURE__ */ jsx("span", { className: `text-xs font-semibold tabular-nums truncate ${toneText[tone]}`, children: value }), barPct !== void 0 && /* @__PURE__ */ jsx("div", { className: "h-0.5 w-full rounded-full bg-muted overflow-hidden", children: /* @__PURE__ */ jsx( "div", { className: `h-full ${toneBar[tone]} transition-all duration-300`, style: { width: `${barPct}%` } } ) }) ] }); } export { DiagnosticsDetailPanel }; //# sourceMappingURL=DiagnosticsDetailPanel.js.map