@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
JavaScript
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