@hienlh/ppm 0.18.9 → 0.18.10
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/.spike/group-protected-probe.ts +21 -0
- package/.spike/tunnel.err +22 -42
- package/.spike/w.err +26 -94
- package/CHANGELOG.md +12 -0
- package/assets/skills/ppm/SKILL.md +1 -1
- package/assets/skills/ppm/references/http-api.md +1 -1
- package/dist/web/assets/{ai-resource-editor-DFCuNe-P.js → ai-resource-editor-mdDKG-rw.js} +1 -1
- package/dist/web/assets/{chat-tab-flMPLoKb.js → chat-tab-C060A94y.js} +3 -3
- package/dist/web/assets/{code-editor-C2VQbXzw.js → code-editor-WELegzdp.js} +2 -2
- package/dist/web/assets/{conflict-editor-CdH_h0vD.js → conflict-editor-fRTU3f1o.js} +1 -1
- package/dist/web/assets/{csv-preview-CY_454vd.js → csv-preview-D1d0cxOJ.js} +1 -1
- package/dist/web/assets/{database-viewer-BCZOTrPT.js → database-viewer-DevnWTV4.js} +1 -1
- package/dist/web/assets/{diff-viewer-BwfMN-nW.js → diff-viewer-DGxVg1pD.js} +1 -1
- package/dist/web/assets/{docx-preview-D3Sdh_aT.js → docx-preview-Cx-zLwAw.js} +1 -1
- package/dist/web/assets/{explorer-body-BKOMM566.js → explorer-body-j_4hVc34.js} +1 -1
- package/dist/web/assets/{explorer-window-content-Bbzijk8t.js → explorer-window-content-DdrGwk3w.js} +1 -1
- package/dist/web/assets/{extension-webview-CuOQrHIS.js → extension-webview-BH_mWYMe.js} +1 -1
- package/dist/web/assets/format-bytes-BE_W3UBg.js +1 -0
- package/dist/web/assets/{git-log-panel-DcV7PhqW.js → git-log-panel-CfpXRJcP.js} +1 -1
- package/dist/web/assets/{glide-data-grid-Cehfgrzc.js → glide-data-grid-n4ox_uev.js} +1 -1
- package/dist/web/assets/{group-chat-tab-BVPYxjz-.js → group-chat-tab-dYuuknLN.js} +1 -1
- package/dist/web/assets/{image-preview-D-Ok10eZ.js → image-preview-Ss38ex8g.js} +1 -1
- package/dist/web/assets/index-BiTiXgBZ.css +2 -0
- package/dist/web/assets/{index-CerSxGdf.js → index-Ch8l9de-.js} +5 -5
- package/dist/web/assets/{keybindings-store-aIY40k2o.js → keybindings-store-CDfTjwZX.js} +1 -1
- package/dist/web/assets/{markdown-renderer-pVzBdkpl.js → markdown-renderer-B317vEys.js} +1 -1
- package/dist/web/assets/{markdown-renderer-I89kyQhe.js → markdown-renderer-CpRlpLHw.js} +1 -1
- package/dist/web/assets/{mobile-explorer-sheet-5Uj9ZMM9.js → mobile-explorer-sheet-D1Bv9UCT.js} +1 -1
- package/dist/web/assets/{notification-store-DJ-OtAG3.js → notification-store-KwTAZT8b.js} +1 -1
- package/dist/web/assets/overview-panel-BxDohIEa.js +1 -0
- package/dist/web/assets/{pdf-preview-Di5xHFyT.js → pdf-preview-dwRe54ht.js} +1 -1
- package/dist/web/assets/{postgres-viewer-CR6VpTrX.js → postgres-viewer-BWZLW7kI.js} +1 -1
- package/dist/web/assets/process-table-CI-fnVsy.js +1 -0
- package/dist/web/assets/resolve-theme-B9ABAH_3.js +1 -0
- package/dist/web/assets/{settings-tab-jbiQwozX.js → settings-tab-mPxrKkfm.js} +1 -1
- package/dist/web/assets/{sql-query-editor-BPYsJDsn.js → sql-query-editor-BwPjEaP9.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-Bq9ZJA2y.js → sqlite-viewer-Cs-_D942.js} +1 -1
- package/dist/web/assets/{system-monitor-body-CKEYWyN9.js → system-monitor-body-CV7-0hZG.js} +2 -2
- package/dist/web/assets/{system-monitor-tab-WjhFFHyM.js → system-monitor-tab-Ce-8Axhh.js} +1 -1
- package/dist/web/assets/{system-monitor-window-content-CMiISeoC.js → system-monitor-window-content-CetwYOQy.js} +1 -1
- package/dist/web/assets/{team-member-sheet-oY5EkH53.js → team-member-sheet-CjgaQjsZ.js} +2 -2
- package/dist/web/assets/{team-member-window-content-yCBeI_5X.js → team-member-window-content-DNJiYWS7.js} +1 -1
- package/dist/web/assets/{terminal-tab-BxHtMR7h.js → terminal-tab-DF7521sT.js} +1 -1
- package/dist/web/assets/{tool-cards-BcGrk1zk.js → tool-cards-DCAXGDJI.js} +2 -2
- package/dist/web/assets/{use-monaco-theme-CGucu3fb.js → use-monaco-theme-DSCdlrjO.js} +1 -1
- package/dist/web/assets/{use-open-team-member-DdrRXj-M.js → use-open-team-member-H4k74HnV.js} +1 -1
- package/dist/web/assets/{use-websocket-C-Rxv8JN.js → use-websocket-iJfc7EbK.js} +1 -1
- package/dist/web/assets/{video-preview-DYAbbmd0.js → video-preview-B0ZWpX0F.js} +1 -1
- package/dist/web/index.html +3 -3
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
- package/src/web/components/system/build-kill-request.ts +28 -1
- package/src/web/components/system/chart-scale.ts +3 -3
- package/src/web/components/system/kill-confirm-dialog.tsx +44 -28
- package/src/web/components/system/metric-chart-canvas.tsx +1 -2
- package/src/web/components/system/process-columns-grid.ts +39 -29
- package/src/web/components/system/process-group-row.tsx +31 -8
- package/src/web/components/system/process-row.tsx +6 -7
- package/src/web/components/system/process-table-model.ts +4 -22
- package/src/web/components/system/process-table-toolbar.tsx +12 -43
- package/src/web/components/system/process-table.tsx +22 -63
- package/src/web/components/system/sortable-header.tsx +33 -8
- package/src/web/components/system/system-monitor-body.tsx +1 -1
- package/src/web/components/system/use-column-widths.ts +91 -0
- package/src/web/components/system/use-process-kill.ts +100 -0
- package/src/web/styles/globals.css +5 -3
- package/src/web/theme/builtin/aurora.ts +8 -5
- package/dist/web/assets/format-bytes-Dk7B73B7.js +0 -1
- package/dist/web/assets/index-DRA_OpWY.css +0 -2
- package/dist/web/assets/overview-panel-Cgi3UPY1.js +0 -1
- package/dist/web/assets/process-table-DL6shbzt.js +0 -1
- package/dist/web/assets/resolve-theme-IdhSR-gx.js +0 -1
- package/src/web/components/system/sparkline-canvas.tsx +0 -63
|
@@ -5,44 +5,60 @@ import { Button } from "@/components/ui/button";
|
|
|
5
5
|
import { Switch } from "@/components/ui/switch";
|
|
6
6
|
import { useIsMobile } from "@/hooks/use-is-mobile";
|
|
7
7
|
import { formatRam } from "@/lib/format-bytes";
|
|
8
|
-
import type {
|
|
8
|
+
import type { KillTarget } from "./build-kill-request";
|
|
9
9
|
|
|
10
10
|
export interface KillConfirmDialogProps {
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
target: KillTarget | null;
|
|
12
|
+
/** `tree` is only meaningful for a single process; a group is always ended whole. */
|
|
13
|
+
onConfirm: (target: KillTarget, tree: boolean) => void;
|
|
13
14
|
onCancel: () => void;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
/** Confirm dialog for ending a process. Bottom sheet below
|
|
17
|
-
* `md`+. The "end child processes" toggle defaults off and
|
|
18
|
-
* never the default focus — Escape/backdrop cancels either
|
|
19
|
-
|
|
17
|
+
/** Confirm dialog for ending a process or a whole app group. Bottom sheet below
|
|
18
|
+
* `md`, centered dialog at `md`+. The "end child processes" toggle defaults off and
|
|
19
|
+
* the destructive action is never the default focus — Escape/backdrop cancels either
|
|
20
|
+
* way. */
|
|
21
|
+
export function KillConfirmDialog({ target, onConfirm, onCancel }: KillConfirmDialogProps) {
|
|
20
22
|
const isMobile = useIsMobile();
|
|
21
23
|
const [tree, setTree] = useState(false);
|
|
22
24
|
|
|
23
25
|
useEffect(() => {
|
|
24
|
-
if (!
|
|
25
|
-
}, [
|
|
26
|
+
if (!target) setTree(false);
|
|
27
|
+
}, [target]);
|
|
26
28
|
|
|
27
|
-
if (!
|
|
29
|
+
if (!target) return null;
|
|
30
|
+
|
|
31
|
+
const isGroup = target.kind === "group";
|
|
32
|
+
const title = isGroup ? "End app" : "End process";
|
|
33
|
+
const cpu = isGroup ? target.group.cpu : target.proc.cpu;
|
|
34
|
+
const ramMB = isGroup ? target.group.ramMB : target.proc.ramMB;
|
|
28
35
|
|
|
29
36
|
const body = (
|
|
30
|
-
<div className="space-y-4" data-testid="sysmon-kill-confirm">
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
37
|
+
<div className="space-y-4" data-testid="sysmon-kill-confirm" data-kill-kind={target.kind}>
|
|
38
|
+
{isGroup ? (
|
|
39
|
+
<p className="text-sm">
|
|
40
|
+
End <span className="font-medium">{target.group.label}</span> and all its{" "}
|
|
41
|
+
<span className="font-medium">{target.members.length}</span> processes?
|
|
42
|
+
</p>
|
|
43
|
+
) : (
|
|
44
|
+
<p className="text-sm">
|
|
45
|
+
End <span className="font-medium">{target.proc.name}</span> (pid {target.proc.pid})?
|
|
46
|
+
</p>
|
|
47
|
+
)}
|
|
34
48
|
<p className="text-xs text-text-subtle">
|
|
35
|
-
CPU {
|
|
49
|
+
CPU {cpu.toFixed(1)}% · RAM {formatRam(ramMB)}
|
|
36
50
|
</p>
|
|
37
|
-
|
|
38
|
-
<
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
51
|
+
{!isGroup && (
|
|
52
|
+
<label className="flex items-center justify-between gap-3 min-h-11 py-1">
|
|
53
|
+
<span className="text-sm">Also end child processes</span>
|
|
54
|
+
<Switch
|
|
55
|
+
checked={tree}
|
|
56
|
+
onCheckedChange={setTree}
|
|
57
|
+
data-testid="sysmon-kill-tree"
|
|
58
|
+
aria-label="Also end child processes"
|
|
59
|
+
/>
|
|
60
|
+
</label>
|
|
61
|
+
)}
|
|
46
62
|
<div className="flex flex-col-reverse md:flex-row gap-2 md:justify-end pt-2">
|
|
47
63
|
<Button
|
|
48
64
|
variant="outline"
|
|
@@ -55,11 +71,11 @@ export function KillConfirmDialog({ process, onConfirm, onCancel }: KillConfirmD
|
|
|
55
71
|
<Button
|
|
56
72
|
variant="destructive"
|
|
57
73
|
autoFocus={false}
|
|
58
|
-
onClick={() => onConfirm(
|
|
74
|
+
onClick={() => onConfirm(target, tree)}
|
|
59
75
|
data-testid="sysmon-kill-confirm-ok"
|
|
60
76
|
className="min-h-11"
|
|
61
77
|
>
|
|
62
|
-
End process
|
|
78
|
+
{isGroup ? `End ${target.members.length} processes` : "End process"}
|
|
63
79
|
</Button>
|
|
64
80
|
</div>
|
|
65
81
|
</div>
|
|
@@ -69,7 +85,7 @@ export function KillConfirmDialog({ process, onConfirm, onCancel }: KillConfirmD
|
|
|
69
85
|
return (
|
|
70
86
|
<BottomSheet open onClose={onCancel}>
|
|
71
87
|
<div className="px-4 pb-4">
|
|
72
|
-
<h2 className="text-base font-semibold mb-3">
|
|
88
|
+
<h2 className="text-base font-semibold mb-3">{title}</h2>
|
|
73
89
|
{body}
|
|
74
90
|
</div>
|
|
75
91
|
</BottomSheet>
|
|
@@ -80,7 +96,7 @@ export function KillConfirmDialog({ process, onConfirm, onCancel }: KillConfirmD
|
|
|
80
96
|
<Dialog open onOpenChange={(open) => !open && onCancel()}>
|
|
81
97
|
<DialogContent>
|
|
82
98
|
<DialogHeader>
|
|
83
|
-
<DialogTitle>
|
|
99
|
+
<DialogTitle>{title}</DialogTitle>
|
|
84
100
|
</DialogHeader>
|
|
85
101
|
{body}
|
|
86
102
|
</DialogContent>
|
|
@@ -20,8 +20,7 @@ interface MetricChartCanvasProps {
|
|
|
20
20
|
* width of its parent — the cards live in a resizable floating window, so a fixed
|
|
21
21
|
* pixel width overflows the card as soon as the window is narrower than designed.
|
|
22
22
|
* No redraw throttle — one draw per prop change, which at a 2s tick cadence is
|
|
23
|
-
* nowhere near a frame budget.
|
|
24
|
-
* used by process rows. */
|
|
23
|
+
* nowhere near a frame budget. */
|
|
25
24
|
export const MetricChartCanvas = memo(function MetricChartCanvas({
|
|
26
25
|
series,
|
|
27
26
|
height,
|
|
@@ -1,45 +1,50 @@
|
|
|
1
1
|
/** Pure grid-template builder for the process table. No React, so the visibility
|
|
2
|
-
* rules (which optional columns exist, which one survives below `@lg`)
|
|
3
|
-
* unit-testable without mounting a component.
|
|
2
|
+
* rules (which optional columns exist, which one survives below `@lg`) and the
|
|
3
|
+
* user-resized widths are unit-testable without mounting a component.
|
|
4
4
|
*
|
|
5
5
|
* Two responsive templates are computed up front and switched via CSS custom
|
|
6
6
|
* properties + a `@lg:` Tailwind class (see `PROCESS_ROW_GRID_CLASS` below) —
|
|
7
7
|
* Tailwind's arbitrary-value scan only needs the class NAME to be a static
|
|
8
|
-
* string; the var() value itself can vary at runtime.
|
|
9
|
-
* two-template approach the process table already used for the trend/age
|
|
10
|
-
* track, just parameterised by which optional columns the host can measure. */
|
|
8
|
+
* string; the var() value itself can vary at runtime. */
|
|
11
9
|
import type { CSSProperties } from "react";
|
|
12
10
|
import type { ProcessColumnAvailability, SortKey } from "../../../types/system-metrics";
|
|
13
11
|
|
|
14
12
|
export type OptionalColumnKey = "disk" | "gpu" | "net";
|
|
13
|
+
/** Every column whose width the user can drag. The name column takes the rest. */
|
|
14
|
+
export type ResizableColumnKey = "cpu" | "ram" | OptionalColumnKey;
|
|
15
15
|
|
|
16
16
|
/** Alias kept local so call sites don't need to know the exact server-contract
|
|
17
17
|
* type name — this module only cares about the three boolean flags. */
|
|
18
18
|
export type ProcessColumnsFlags = ProcessColumnAvailability;
|
|
19
19
|
|
|
20
|
+
/** px overrides from a drag; missing keys fall back to `DEFAULT_COLUMN_WIDTH`. */
|
|
21
|
+
export type ColumnWidths = Partial<Record<ResizableColumnKey, number>>;
|
|
22
|
+
|
|
20
23
|
const OPTIONAL_COLUMN_ORDER: OptionalColumnKey[] = ["disk", "gpu", "net"];
|
|
21
24
|
|
|
22
25
|
/** Disk/Net cells show two values ("↓ read ↑ write"); GPU shows one composite
|
|
23
26
|
* ("12% · 1.1 GB") and fits narrower. */
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
export const DEFAULT_COLUMN_WIDTH: Record<ResizableColumnKey, number> = {
|
|
28
|
+
cpu: 64,
|
|
29
|
+
ram: 80,
|
|
30
|
+
disk: 120,
|
|
31
|
+
gpu: 90,
|
|
32
|
+
net: 120,
|
|
28
33
|
};
|
|
34
|
+
/** Drag bounds: below 48px a "1.2 MB/s" no longer fits; above 400px the name
|
|
35
|
+
* column starves in a default-size window. */
|
|
36
|
+
export const MIN_COLUMN_WIDTH = 48;
|
|
37
|
+
export const MAX_COLUMN_WIDTH = 400;
|
|
29
38
|
|
|
30
|
-
|
|
31
|
-
* CPU, RAM at the start; the trend/age track (wide only) and the kill button at
|
|
32
|
-
* the end. */
|
|
33
|
-
const NAME_CPU_RAM = ["minmax(0,1fr)", "64px", "80px"];
|
|
34
|
-
const TREND_TRACK = "130px";
|
|
39
|
+
const NAME_TRACK = "minmax(0,1fr)";
|
|
35
40
|
const KILL_TRACK = "44px";
|
|
36
41
|
|
|
37
42
|
export interface ProcessGridResult {
|
|
38
43
|
/** Grid template for `@lg` and up: Name/CPU/RAM + every enabled optional column
|
|
39
|
-
* in a fixed order +
|
|
44
|
+
* in a fixed order + kill button. */
|
|
40
45
|
wideTemplate: string;
|
|
41
46
|
/** Grid template below `@lg`: Name/CPU/RAM + AT MOST the one optional column
|
|
42
|
-
* currently sorted by (if enabled) + kill button.
|
|
47
|
+
* currently sorted by (if enabled) + kill button. */
|
|
43
48
|
narrowTemplate: string;
|
|
44
49
|
/** Which optional columns are enabled at all (host can measure them). */
|
|
45
50
|
columns: ProcessColumnsFlags;
|
|
@@ -52,22 +57,27 @@ function isOptionalColumnKey(key: SortKey): key is OptionalColumnKey {
|
|
|
52
57
|
return key === "disk" || key === "gpu" || key === "net";
|
|
53
58
|
}
|
|
54
59
|
|
|
55
|
-
export function
|
|
60
|
+
export function clampColumnWidth(px: number): number {
|
|
61
|
+
return Math.round(Math.min(MAX_COLUMN_WIDTH, Math.max(MIN_COLUMN_WIDTH, px)));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function columnWidthPx(key: ResizableColumnKey, widths: ColumnWidths): number {
|
|
65
|
+
const w = widths[key];
|
|
66
|
+
return typeof w === "number" && Number.isFinite(w) ? clampColumnWidth(w) : DEFAULT_COLUMN_WIDTH[key];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function buildProcessGrid(
|
|
70
|
+
columns: ProcessColumnsFlags,
|
|
71
|
+
sortKey: SortKey,
|
|
72
|
+
widths: ColumnWidths = {},
|
|
73
|
+
): ProcessGridResult {
|
|
56
74
|
const enabled = OPTIONAL_COLUMN_ORDER.filter((k) => columns[k]);
|
|
57
75
|
const narrowExtra = isOptionalColumnKey(sortKey) && columns[sortKey] ? sortKey : null;
|
|
76
|
+
const track = (k: ResizableColumnKey) => `${columnWidthPx(k, widths)}px`;
|
|
77
|
+
const base = [NAME_TRACK, track("cpu"), track("ram")];
|
|
58
78
|
|
|
59
|
-
const wideTemplate = [
|
|
60
|
-
|
|
61
|
-
...enabled.map((k) => OPTIONAL_COLUMN_WIDTH[k]),
|
|
62
|
-
TREND_TRACK,
|
|
63
|
-
KILL_TRACK,
|
|
64
|
-
].join(" ");
|
|
65
|
-
|
|
66
|
-
const narrowTemplate = [
|
|
67
|
-
...NAME_CPU_RAM,
|
|
68
|
-
...(narrowExtra ? [OPTIONAL_COLUMN_WIDTH[narrowExtra]] : []),
|
|
69
|
-
KILL_TRACK,
|
|
70
|
-
].join(" ");
|
|
79
|
+
const wideTemplate = [...base, ...enabled.map(track), KILL_TRACK].join(" ");
|
|
80
|
+
const narrowTemplate = [...base, ...(narrowExtra ? [track(narrowExtra)] : []), KILL_TRACK].join(" ");
|
|
71
81
|
|
|
72
82
|
return { wideTemplate, narrowTemplate, columns, narrowExtra };
|
|
73
83
|
}
|
|
@@ -1,25 +1,28 @@
|
|
|
1
|
-
import { ChevronRight, ChevronDown } from "lucide-react";
|
|
1
|
+
import { ChevronRight, ChevronDown, X } from "lucide-react";
|
|
2
2
|
import { cn } from "@/lib/utils";
|
|
3
3
|
import { formatRam } from "@/lib/format-bytes";
|
|
4
4
|
import { cpuColor, formatDiskCell, formatGpuCell, formatNetCell, sumOptionalBps } from "./process-row-format";
|
|
5
5
|
import { optionalCellClassName, type ProcessGridResult } from "./process-columns-grid";
|
|
6
|
-
import { SparklineCanvas } from "./sparkline-canvas";
|
|
7
6
|
import type { ProcessGroup } from "../../../types/system-metrics";
|
|
8
7
|
|
|
8
|
+
const PROTECTED_GROUP_TOOLTIP = "Protected: this app contains a PPM or OS-critical process — cannot be ended as a whole.";
|
|
9
|
+
|
|
9
10
|
export interface ProcessGroupRowProps {
|
|
10
11
|
group: ProcessGroup;
|
|
11
12
|
expanded: boolean;
|
|
12
|
-
spark: number[];
|
|
13
13
|
grid: ProcessGridResult;
|
|
14
|
+
/** True when any member is refused by the kill guard — the whole-app button is disabled. */
|
|
15
|
+
killProtected: boolean;
|
|
14
16
|
onToggle: (key: string) => void;
|
|
17
|
+
onKillClick: (group: ProcessGroup) => void;
|
|
15
18
|
}
|
|
16
19
|
|
|
17
|
-
export function ProcessGroupRow({ group, expanded,
|
|
20
|
+
export function ProcessGroupRow({ group, expanded, grid, killProtected, onToggle, onKillClick }: ProcessGroupRowProps) {
|
|
18
21
|
const Chevron = expanded ? ChevronDown : ChevronRight;
|
|
19
22
|
|
|
20
23
|
return (
|
|
21
24
|
<div
|
|
22
|
-
className="grid items-center gap-1 min-h-11 md:min-h-7 px-3 text-xs cursor-pointer hover:bg-surface-hover transition-colors grid-cols-[var(--sysmon-grid-n)] @lg:grid-cols-[var(--sysmon-grid-w)]"
|
|
25
|
+
className="grid items-center gap-1 min-h-11 md:min-h-7 px-3 text-xs cursor-pointer hover:bg-surface-hover transition-colors group/proc grid-cols-[var(--sysmon-grid-n)] @lg:grid-cols-[var(--sysmon-grid-w)]"
|
|
23
26
|
onClick={() => onToggle(group.key)}
|
|
24
27
|
data-testid="sysmon-group-row"
|
|
25
28
|
data-group-key={group.key}
|
|
@@ -62,10 +65,30 @@ export function ProcessGroupRow({ group, expanded, spark, grid, onToggle }: Proc
|
|
|
62
65
|
{formatNetCell(group.netInBps, group.netOutBps)}
|
|
63
66
|
</span>
|
|
64
67
|
)}
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
{/* Same 44px track as the leaf rows' kill button, so ending a whole app (Notion
|
|
69
|
+
and its 7 helpers) is one click instead of seven. */}
|
|
70
|
+
<div className="flex justify-end">
|
|
71
|
+
<button
|
|
72
|
+
type="button"
|
|
73
|
+
onClick={(e) => {
|
|
74
|
+
e.stopPropagation();
|
|
75
|
+
onKillClick(group);
|
|
76
|
+
}}
|
|
77
|
+
disabled={killProtected}
|
|
78
|
+
aria-label={`End ${group.label} and its ${group.count} processes`}
|
|
79
|
+
title={killProtected ? PROTECTED_GROUP_TOOLTIP : `End ${group.label} (${group.count} processes)`}
|
|
80
|
+
data-testid="sysmon-group-kill-btn"
|
|
81
|
+
className={cn(
|
|
82
|
+
"flex items-center justify-center size-11 md:size-7 rounded transition-all",
|
|
83
|
+
"can-hover:opacity-0 can-hover:group-hover/proc:opacity-100",
|
|
84
|
+
killProtected
|
|
85
|
+
? "opacity-40 cursor-not-allowed"
|
|
86
|
+
: "hover:bg-error/20 hover:text-error active:bg-error/30",
|
|
87
|
+
)}
|
|
88
|
+
>
|
|
89
|
+
<X className="size-3.5" />
|
|
90
|
+
</button>
|
|
67
91
|
</div>
|
|
68
|
-
<div className="hidden @lg:block" />
|
|
69
92
|
</div>
|
|
70
93
|
);
|
|
71
94
|
}
|
|
@@ -26,9 +26,14 @@ export function ProcessRow({ proc, indent, grid, onKillClick }: ProcessRowProps)
|
|
|
26
26
|
data-gpu-pct={proc.gpuPct}
|
|
27
27
|
data-net-bps={sumOptionalBps(proc.netInBps, proc.netOutBps)}
|
|
28
28
|
>
|
|
29
|
+
{/* Uptime lives in the tooltip: a dedicated column cost 130px for a figure
|
|
30
|
+
that matters only when you are already looking at one row. */}
|
|
29
31
|
<div className={cn("flex items-start gap-1.5 min-w-0", indent && "pl-6")}>
|
|
30
32
|
<span className="text-text-subtle shrink-0">{proc.pid}</span>
|
|
31
|
-
<span
|
|
33
|
+
<span
|
|
34
|
+
className="truncate text-text-secondary"
|
|
35
|
+
title={proc.startedAt ? `${proc.command}\nRunning ${formatAge(proc.startedAt)}` : proc.command}
|
|
36
|
+
>
|
|
32
37
|
{proc.command}
|
|
33
38
|
</span>
|
|
34
39
|
</div>
|
|
@@ -49,12 +54,6 @@ export function ProcessRow({ proc, indent, grid, onKillClick }: ProcessRowProps)
|
|
|
49
54
|
{formatNetCell(proc.netInBps, proc.netOutBps)}
|
|
50
55
|
</span>
|
|
51
56
|
)}
|
|
52
|
-
<span
|
|
53
|
-
className="hidden @lg:block text-text-subtle text-right"
|
|
54
|
-
title={proc.startedAt ? new Date(proc.startedAt).toLocaleString() : ""}
|
|
55
|
-
>
|
|
56
|
-
{formatAge(proc.startedAt || undefined)}
|
|
57
|
-
</span>
|
|
58
57
|
<div className="flex justify-end">
|
|
59
58
|
<button
|
|
60
59
|
type="button"
|
|
@@ -2,26 +2,19 @@
|
|
|
2
2
|
* filter/sort/expand/rollup rules are unit-testable without mounting a component.
|
|
3
3
|
* Sort comparator lives in `process-table-sort.ts`, totals accumulator in
|
|
4
4
|
* `process-table-totals.ts` — both split out to stay under the file-size guideline. */
|
|
5
|
-
import type {
|
|
6
|
-
MetricsHistoryPoint,
|
|
7
|
-
ProcessGroup,
|
|
8
|
-
ProcessInfo,
|
|
9
|
-
SortDir,
|
|
10
|
-
SortKey,
|
|
11
|
-
} from "../../../types/system-metrics";
|
|
5
|
+
import type { ProcessGroup, ProcessInfo, SortDir, SortKey } from "../../../types/system-metrics";
|
|
12
6
|
import { sortByKey, sortFields } from "./process-table-sort";
|
|
13
7
|
import { accumulateTotals, EMPTY_TOTALS, type Totals } from "./process-table-totals";
|
|
14
8
|
|
|
15
9
|
export type { Totals } from "./process-table-totals";
|
|
16
10
|
|
|
17
11
|
export type TableRow =
|
|
18
|
-
| { kind: "group"; group: ProcessGroup; expanded: boolean
|
|
12
|
+
| { kind: "group"; group: ProcessGroup; expanded: boolean }
|
|
19
13
|
| { kind: "process"; proc: ProcessInfo; indent: boolean };
|
|
20
14
|
|
|
21
15
|
export interface BuildRowsInput {
|
|
22
16
|
processes: ProcessInfo[];
|
|
23
17
|
groups: ProcessGroup[];
|
|
24
|
-
history: MetricsHistoryPoint[];
|
|
25
18
|
mode: "grouped" | "flat";
|
|
26
19
|
ppmOnly: boolean;
|
|
27
20
|
query: string;
|
|
@@ -37,8 +30,6 @@ export interface BuildRowsResult {
|
|
|
37
30
|
autoExpanded: Set<string>;
|
|
38
31
|
}
|
|
39
32
|
|
|
40
|
-
const SPARK_POINTS = 60;
|
|
41
|
-
|
|
42
33
|
function matches(text: string, query: string): boolean {
|
|
43
34
|
return text.toLowerCase().includes(query);
|
|
44
35
|
}
|
|
@@ -47,13 +38,9 @@ function processMatches(proc: ProcessInfo, query: string): boolean {
|
|
|
47
38
|
return matches(proc.name, query) || matches(proc.command, query);
|
|
48
39
|
}
|
|
49
40
|
|
|
50
|
-
function sparkFor(history: MetricsHistoryPoint[], groupKey: string): number[] {
|
|
51
|
-
return history.slice(-SPARK_POINTS).map((h) => h.groups[groupKey]?.cpu ?? 0);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
41
|
/** Order fixed: filter (ppmOnly) -> filter (query) -> sort -> flatten (expand). */
|
|
55
42
|
export function buildRows(input: BuildRowsInput): BuildRowsResult {
|
|
56
|
-
const { processes, groups,
|
|
43
|
+
const { processes, groups, mode, ppmOnly, query, sortKey, sortDir, expanded } = input;
|
|
57
44
|
const q = query.trim().toLowerCase();
|
|
58
45
|
const byPid = new Map(processes.map((p) => [p.pid, p]));
|
|
59
46
|
|
|
@@ -93,12 +80,7 @@ export function buildRows(input: BuildRowsInput): BuildRowsResult {
|
|
|
93
80
|
const rows: TableRow[] = [];
|
|
94
81
|
for (const group of filteredGroups) {
|
|
95
82
|
const isExpanded = q ? autoExpanded.has(group.key) || expanded.has(group.key) : expanded.has(group.key);
|
|
96
|
-
rows.push({
|
|
97
|
-
kind: "group",
|
|
98
|
-
group,
|
|
99
|
-
expanded: isExpanded,
|
|
100
|
-
spark: sparkFor(history, group.key),
|
|
101
|
-
});
|
|
83
|
+
rows.push({ kind: "group", group, expanded: isExpanded });
|
|
102
84
|
if (!isExpanded) continue;
|
|
103
85
|
|
|
104
86
|
// Skip pids the current snapshot no longer has — a process can exit between the
|
|
@@ -5,7 +5,7 @@ import { formatRam } from "@/lib/format-bytes";
|
|
|
5
5
|
import { cpuColor, formatDiskCell, formatGpuCell, formatNetCell } from "./process-row-format";
|
|
6
6
|
import { optionalCellClassName, PROCESS_ROW_GRID_CLASS, type ProcessGridResult } from "./process-columns-grid";
|
|
7
7
|
import type { Totals } from "./process-table-totals";
|
|
8
|
-
import { SortableHeader } from "./sortable-header";
|
|
8
|
+
import { SortableHeader, type ColumnResizeHandlers } from "./sortable-header";
|
|
9
9
|
import type { SortDir, SortKey } from "../../../types/system-metrics";
|
|
10
10
|
|
|
11
11
|
const SEARCH_DEBOUNCE_MS = 150;
|
|
@@ -83,12 +83,15 @@ export interface ProcessTableHeaderProps {
|
|
|
83
83
|
sortDir: SortDir;
|
|
84
84
|
grid: ProcessGridResult;
|
|
85
85
|
onSort: (field: Exclude<SortKey, null>) => void;
|
|
86
|
+
resize: ColumnResizeHandlers;
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
/** Sticky column header row, grid-aligned with the rows below it. Disk/GPU/Net
|
|
89
90
|
* headers only render when the host reported that column as measurable
|
|
90
|
-
* (`grid.columns.<x>`); below `@lg` only the one currently sorted survives.
|
|
91
|
-
|
|
91
|
+
* (`grid.columns.<x>`); below `@lg` only the one currently sorted survives. Every
|
|
92
|
+
* fixed-width column carries a drag handle on its right edge. */
|
|
93
|
+
export function ProcessTableHeader({ sortKey, sortDir, grid, onSort, resize }: ProcessTableHeaderProps) {
|
|
94
|
+
const common = { activeKey: sortKey, activeDir: sortDir, onClick: onSort, resize };
|
|
92
95
|
return (
|
|
93
96
|
<div
|
|
94
97
|
role="row"
|
|
@@ -97,51 +100,18 @@ export function ProcessTableHeader({ sortKey, sortDir, grid, onSort }: ProcessTa
|
|
|
97
100
|
PROCESS_ROW_GRID_CLASS,
|
|
98
101
|
)}
|
|
99
102
|
>
|
|
100
|
-
<SortableHeader
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
activeKey={sortKey}
|
|
104
|
-
activeDir={sortDir}
|
|
105
|
-
onClick={onSort}
|
|
106
|
-
align="left"
|
|
107
|
-
testId="sysmon-sort-name"
|
|
108
|
-
/>
|
|
109
|
-
<SortableHeader label="CPU" field="cpu" activeKey={sortKey} activeDir={sortDir} onClick={onSort} testId="sysmon-sort-cpu" />
|
|
110
|
-
<SortableHeader label="RAM" field="ram" activeKey={sortKey} activeDir={sortDir} onClick={onSort} testId="sysmon-sort-ram" />
|
|
103
|
+
<SortableHeader label="Process" field="name" {...common} resize={undefined} align="left" testId="sysmon-sort-name" />
|
|
104
|
+
<SortableHeader label="CPU" field="cpu" {...common} resizeKey="cpu" testId="sysmon-sort-cpu" />
|
|
105
|
+
<SortableHeader label="RAM" field="ram" {...common} resizeKey="ram" testId="sysmon-sort-ram" />
|
|
111
106
|
{grid.columns.disk && (
|
|
112
|
-
<SortableHeader
|
|
113
|
-
label="Disk"
|
|
114
|
-
field="disk"
|
|
115
|
-
activeKey={sortKey}
|
|
116
|
-
activeDir={sortDir}
|
|
117
|
-
onClick={onSort}
|
|
118
|
-
testId="sysmon-col-disk"
|
|
119
|
-
className={optionalCellClassName(grid, "disk")}
|
|
120
|
-
/>
|
|
107
|
+
<SortableHeader label="Disk" field="disk" {...common} resizeKey="disk" testId="sysmon-col-disk" className={optionalCellClassName(grid, "disk")} />
|
|
121
108
|
)}
|
|
122
109
|
{grid.columns.gpu && (
|
|
123
|
-
<SortableHeader
|
|
124
|
-
label="GPU"
|
|
125
|
-
field="gpu"
|
|
126
|
-
activeKey={sortKey}
|
|
127
|
-
activeDir={sortDir}
|
|
128
|
-
onClick={onSort}
|
|
129
|
-
testId="sysmon-col-gpu"
|
|
130
|
-
className={optionalCellClassName(grid, "gpu")}
|
|
131
|
-
/>
|
|
110
|
+
<SortableHeader label="GPU" field="gpu" {...common} resizeKey="gpu" testId="sysmon-col-gpu" className={optionalCellClassName(grid, "gpu")} />
|
|
132
111
|
)}
|
|
133
112
|
{grid.columns.net && (
|
|
134
|
-
<SortableHeader
|
|
135
|
-
label="Net"
|
|
136
|
-
field="net"
|
|
137
|
-
activeKey={sortKey}
|
|
138
|
-
activeDir={sortDir}
|
|
139
|
-
onClick={onSort}
|
|
140
|
-
testId="sysmon-col-net"
|
|
141
|
-
className={optionalCellClassName(grid, "net")}
|
|
142
|
-
/>
|
|
113
|
+
<SortableHeader label="Net" field="net" {...common} resizeKey="net" testId="sysmon-col-net" className={optionalCellClassName(grid, "net")} />
|
|
143
114
|
)}
|
|
144
|
-
<span className="text-right hidden @lg:block">Trend</span>
|
|
145
115
|
<span />
|
|
146
116
|
</div>
|
|
147
117
|
);
|
|
@@ -183,7 +153,6 @@ export function ProcessTableFooter({ totals, grid, gpuUtilPercent }: ProcessTabl
|
|
|
183
153
|
{formatNetCell(totals.netInBps, totals.netOutBps)}
|
|
184
154
|
</span>
|
|
185
155
|
)}
|
|
186
|
-
<span className="hidden @lg:block" />
|
|
187
156
|
<span />
|
|
188
157
|
</div>
|
|
189
158
|
);
|
|
@@ -1,24 +1,15 @@
|
|
|
1
1
|
import { useCallback, useMemo, useRef, useState } from "react";
|
|
2
2
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
3
|
-
import { toast } from "sonner";
|
|
4
|
-
import { getAuthToken } from "@/lib/api-client";
|
|
5
3
|
import { useIsMobile } from "@/hooks/use-is-mobile";
|
|
6
4
|
import { buildRows, toggleSort } from "./process-table-model";
|
|
7
|
-
import { buildKillRequest } from "./build-kill-request";
|
|
8
5
|
import { buildProcessGrid, gridCssVars } from "./process-columns-grid";
|
|
9
6
|
import { ProcessTableToolbar, ProcessTableHeader, ProcessTableFooter } from "./process-table-toolbar";
|
|
10
7
|
import { ProcessGroupRow } from "./process-group-row";
|
|
11
8
|
import { ProcessRow } from "./process-row";
|
|
12
9
|
import { KillConfirmDialog } from "./kill-confirm-dialog";
|
|
13
|
-
import
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
MetricsHistoryPoint,
|
|
17
|
-
MetricsSnapshot,
|
|
18
|
-
ProcessInfo,
|
|
19
|
-
SortDir,
|
|
20
|
-
SortKey,
|
|
21
|
-
} from "../../../types/system-metrics";
|
|
10
|
+
import { useProcessKill } from "./use-process-kill";
|
|
11
|
+
import { useColumnWidths } from "./use-column-widths";
|
|
12
|
+
import type { MetricsSnapshot, SortDir, SortKey } from "../../../types/system-metrics";
|
|
22
13
|
|
|
23
14
|
/** Older cached bundles/snapshots (mid-rollout) predate `processColumns` — default
|
|
24
15
|
* every optional column off rather than let a missing field throw. */
|
|
@@ -26,39 +17,9 @@ const NO_OPTIONAL_COLUMNS = { disk: false, gpu: false, net: false };
|
|
|
26
17
|
|
|
27
18
|
export interface ProcessTableProps {
|
|
28
19
|
snapshot: MetricsSnapshot;
|
|
29
|
-
history: MetricsHistoryPoint[];
|
|
30
20
|
}
|
|
31
21
|
|
|
32
|
-
|
|
33
|
-
* kill route requires `X-PPM-Request: 1` — CSRF hardening, since a cross-origin HTML
|
|
34
|
-
* form cannot set a custom header, so a preflight becomes mandatory. A raw fetch here
|
|
35
|
-
* mirrors `handleResponse`'s single-Error-on-any-failure contract exactly, so the
|
|
36
|
-
* caller still has exactly one catch path. */
|
|
37
|
-
async function killProcess(request: KillProcessRequest): Promise<KillProcessResult> {
|
|
38
|
-
const token = getAuthToken();
|
|
39
|
-
const headers: Record<string, string> = {
|
|
40
|
-
"Content-Type": "application/json",
|
|
41
|
-
"X-PPM-Request": "1",
|
|
42
|
-
};
|
|
43
|
-
if (token) headers.Authorization = `Bearer ${token}`;
|
|
44
|
-
|
|
45
|
-
const res = await fetch("/api/system/resources/kill", {
|
|
46
|
-
method: "POST",
|
|
47
|
-
headers,
|
|
48
|
-
body: JSON.stringify(request),
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
let json: { ok: boolean; data?: KillProcessResult; error?: string };
|
|
52
|
-
try {
|
|
53
|
-
json = await res.json();
|
|
54
|
-
} catch {
|
|
55
|
-
throw new Error(res.ok ? "Empty response from server" : `Server error (HTTP ${res.status})`);
|
|
56
|
-
}
|
|
57
|
-
if (json.ok === false) throw new Error(json.error ?? `HTTP ${res.status}`);
|
|
58
|
-
return json.data as KillProcessResult;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function ProcessTable({ snapshot, history }: ProcessTableProps) {
|
|
22
|
+
export function ProcessTable({ snapshot }: ProcessTableProps) {
|
|
62
23
|
const isMobile = useIsMobile();
|
|
63
24
|
const parentRef = useRef<HTMLDivElement>(null);
|
|
64
25
|
|
|
@@ -68,14 +29,15 @@ export function ProcessTable({ snapshot, history }: ProcessTableProps) {
|
|
|
68
29
|
const [sortKey, setSortKey] = useState<SortKey>(null);
|
|
69
30
|
const [sortDir, setSortDir] = useState<SortDir>("desc");
|
|
70
31
|
const [expanded, setExpanded] = useState<Set<string>>(() => new Set());
|
|
71
|
-
const
|
|
32
|
+
const { pendingKill, groupProtected, requestKillProcess, requestKillGroup, confirmKill, cancelKill } =
|
|
33
|
+
useProcessKill(snapshot);
|
|
34
|
+
const { widths, begin: onResizeStart, reset: onResizeReset } = useColumnWidths();
|
|
72
35
|
|
|
73
36
|
const { rows, totals } = useMemo(
|
|
74
37
|
() =>
|
|
75
38
|
buildRows({
|
|
76
39
|
processes: snapshot.processes,
|
|
77
40
|
groups: snapshot.groups,
|
|
78
|
-
history,
|
|
79
41
|
mode,
|
|
80
42
|
ppmOnly,
|
|
81
43
|
query,
|
|
@@ -83,11 +45,11 @@ export function ProcessTable({ snapshot, history }: ProcessTableProps) {
|
|
|
83
45
|
sortDir,
|
|
84
46
|
expanded,
|
|
85
47
|
}),
|
|
86
|
-
[snapshot.processes, snapshot.groups,
|
|
48
|
+
[snapshot.processes, snapshot.groups, mode, ppmOnly, query, sortKey, sortDir, expanded],
|
|
87
49
|
);
|
|
88
50
|
|
|
89
51
|
const columns = snapshot.processColumns ?? NO_OPTIONAL_COLUMNS;
|
|
90
|
-
const grid = useMemo(() => buildProcessGrid(columns, sortKey), [columns, sortKey]);
|
|
52
|
+
const grid = useMemo(() => buildProcessGrid(columns, sortKey, widths), [columns, sortKey, widths]);
|
|
91
53
|
const gpuUtilPercent = snapshot.system.gpus[0]?.utilPercent;
|
|
92
54
|
|
|
93
55
|
const virtualizer = useVirtualizer({
|
|
@@ -115,16 +77,6 @@ export function ProcessTable({ snapshot, history }: ProcessTableProps) {
|
|
|
115
77
|
});
|
|
116
78
|
}, []);
|
|
117
79
|
|
|
118
|
-
const handleConfirmKill = useCallback(async (proc: ProcessInfo, tree: boolean) => {
|
|
119
|
-
setPendingKill(null);
|
|
120
|
-
try {
|
|
121
|
-
await killProcess(buildKillRequest(proc, tree));
|
|
122
|
-
toast.success(`Ended ${proc.name} (${proc.pid})`);
|
|
123
|
-
} catch (e) {
|
|
124
|
-
toast.error(e instanceof Error ? e.message : `Failed to end ${proc.name}`);
|
|
125
|
-
}
|
|
126
|
-
}, []);
|
|
127
|
-
|
|
128
80
|
// `@container`: the trend/age track is toggled on the table's own width, not the
|
|
129
81
|
// viewport — a floating window is routinely narrower than the screen it sits on.
|
|
130
82
|
// `gridCssVars` sets `--sysmon-grid-n`/`--sysmon-grid-w` once here; CSS custom
|
|
@@ -143,7 +95,13 @@ export function ProcessTable({ snapshot, history }: ProcessTableProps) {
|
|
|
143
95
|
onPpmOnlyChange={setPpmOnly}
|
|
144
96
|
onQueryChange={setQuery}
|
|
145
97
|
/>
|
|
146
|
-
<ProcessTableHeader
|
|
98
|
+
<ProcessTableHeader
|
|
99
|
+
sortKey={sortKey}
|
|
100
|
+
sortDir={sortDir}
|
|
101
|
+
grid={grid}
|
|
102
|
+
onSort={handleSort}
|
|
103
|
+
resize={{ onResizeStart, onResizeReset }}
|
|
104
|
+
/>
|
|
147
105
|
<div ref={parentRef} className="flex-1 min-h-0 overflow-y-auto">
|
|
148
106
|
<div style={{ height: virtualizer.getTotalSize(), position: "relative" }}>
|
|
149
107
|
{virtualizer.getVirtualItems().map((item) => {
|
|
@@ -164,12 +122,13 @@ export function ProcessTable({ snapshot, history }: ProcessTableProps) {
|
|
|
164
122
|
<ProcessGroupRow
|
|
165
123
|
group={row.group}
|
|
166
124
|
expanded={row.expanded}
|
|
167
|
-
spark={row.spark}
|
|
168
125
|
grid={grid}
|
|
126
|
+
killProtected={groupProtected.get(row.group.key) ?? true}
|
|
169
127
|
onToggle={handleToggleGroup}
|
|
128
|
+
onKillClick={requestKillGroup}
|
|
170
129
|
/>
|
|
171
130
|
) : (
|
|
172
|
-
<ProcessRow proc={row.proc} indent={row.indent} grid={grid} onKillClick={
|
|
131
|
+
<ProcessRow proc={row.proc} indent={row.indent} grid={grid} onKillClick={requestKillProcess} />
|
|
173
132
|
)}
|
|
174
133
|
</div>
|
|
175
134
|
);
|
|
@@ -178,9 +137,9 @@ export function ProcessTable({ snapshot, history }: ProcessTableProps) {
|
|
|
178
137
|
</div>
|
|
179
138
|
<ProcessTableFooter totals={totals} grid={grid} gpuUtilPercent={gpuUtilPercent} />
|
|
180
139
|
<KillConfirmDialog
|
|
181
|
-
|
|
182
|
-
onConfirm={
|
|
183
|
-
onCancel={
|
|
140
|
+
target={pendingKill}
|
|
141
|
+
onConfirm={confirmKill}
|
|
142
|
+
onCancel={cancelKill}
|
|
184
143
|
/>
|
|
185
144
|
</div>
|
|
186
145
|
);
|