@skyhook-io/radar-app 1.9.0 → 1.9.2

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.
Files changed (52) hide show
  1. package/package.json +7 -7
  2. package/src/App.tsx +69 -16
  3. package/src/api/apiResources.test.ts +11 -0
  4. package/src/api/apiResources.ts +51 -12
  5. package/src/api/client.capacity.test.ts +92 -0
  6. package/src/api/client.ts +2905 -2081
  7. package/src/api/config.test.ts +47 -0
  8. package/src/api/config.ts +15 -0
  9. package/src/api/diagnose.ts +15 -15
  10. package/src/components/ConnectionErrorView.test.tsx +88 -0
  11. package/src/components/ConnectionErrorView.tsx +128 -22
  12. package/src/components/capacity/CapacityActivity.tsx +787 -0
  13. package/src/components/capacity/CapacityDemand.tsx +961 -0
  14. package/src/components/capacity/CapacityOverview.tsx +1529 -0
  15. package/src/components/capacity/CapacityPoolDetail.tsx +1626 -0
  16. package/src/components/capacity/CapacityView.test.tsx +2287 -0
  17. package/src/components/capacity/CapacityView.tsx +85 -0
  18. package/src/components/capacity/ClusterSchedulingCard.tsx +603 -0
  19. package/src/components/capacity/DemandNomination.test.tsx +151 -0
  20. package/src/components/capacity/certaintyGlyph.test.tsx +191 -0
  21. package/src/components/capacity/coverageCertainty.test.ts +162 -0
  22. package/src/components/capacity/podDemandGate.test.ts +47 -0
  23. package/src/components/capacity/podDemandGate.ts +22 -0
  24. package/src/components/capacity/schedulingBar.test.ts +244 -0
  25. package/src/components/capacity/shared.tsx +1841 -0
  26. package/src/components/diagnose/AISettings.tsx +21 -7
  27. package/src/components/diagnose/AgentSetupNotice.tsx +117 -0
  28. package/src/components/diagnose/DiagnoseContext.tsx +127 -57
  29. package/src/components/diagnose/DiagnoseSurface.tsx +33 -15
  30. package/src/components/diagnose/LocalDiagnoseAction.tsx +50 -27
  31. package/src/components/diagnose/agentCatalog.ts +30 -0
  32. package/src/components/diagnose/parts.test.tsx +125 -0
  33. package/src/components/diagnose/parts.tsx +166 -75
  34. package/src/components/home/CapacityCard.test.tsx +150 -0
  35. package/src/components/home/CapacityCard.tsx +125 -0
  36. package/src/components/home/HomeView.tsx +15 -1
  37. package/src/components/issues/IssuesPane.test.ts +142 -0
  38. package/src/components/issues/IssuesPane.tsx +142 -38
  39. package/src/components/nav/PrimaryNavRail.test.tsx +20 -0
  40. package/src/components/nav/PrimaryNavRail.tsx +191 -103
  41. package/src/components/resources/ResourcesView.tsx +9 -8
  42. package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +29 -1
  43. package/src/components/resources/renderers/PodRenderer.tsx +32 -3
  44. package/src/components/settings/SettingsDialog.tsx +31 -19
  45. package/src/components/timeline/TimelineView.tsx +17 -3
  46. package/src/components/ui/command-items.ts +222 -98
  47. package/src/components/workload/WorkloadView.tsx +16 -83
  48. package/src/context/ConnectionContext.test.ts +39 -0
  49. package/src/context/ConnectionContext.tsx +155 -51
  50. package/src/context/DiagnoseCustomization.tsx +1 -1
  51. package/src/utils/shell-safe.test.ts +55 -0
  52. package/src/utils/shell-safe.ts +21 -0
@@ -0,0 +1,85 @@
1
+ import { useLocation, useNavigate } from "react-router-dom";
2
+ import { useCapacityOverview } from "../../api/client";
3
+ import { useConnection } from "../../context/ConnectionContext";
4
+ import type { SelectedResource } from "../../types";
5
+ import { CapacityActivity } from "./CapacityActivity";
6
+ import { CapacityDemand } from "./CapacityDemand";
7
+ import { CapacityOverview } from "./CapacityOverview";
8
+ import { CapacityPoolDetail } from "./CapacityPoolDetail";
9
+ import { capacityPoolPath, parseCapacityRoute } from "./shared";
10
+
11
+ interface CapacityViewProps {
12
+ onOpenResource: (resource: SelectedResource) => void;
13
+ }
14
+
15
+ /**
16
+ * Capacity is a hub-and-spoke surface: Overview is the landing hub; Demand,
17
+ * Activity and NodePool detail are spokes reached from the signals, counts and
18
+ * pools that reference them (each carries a breadcrumb back). No persistent tab
19
+ * chrome — the destination you're on is the page.
20
+ */
21
+ /**
22
+ * Capacity is deliberately cluster-wide: supply (pools/nodes/claims) is
23
+ * cluster-scoped and unfilterable, so scoping only the pod-derived numbers
24
+ * would show "my namespace's demand" against "everyone's supply" — the wrong
25
+ * mental model for capacity questions. RBAC (involuntary, honestly labeled in
26
+ * coverage) is the only scoper; the header's namespace view filter does not
27
+ * apply here and is not forwarded.
28
+ */
29
+ export function CapacityView({ onOpenResource }: CapacityViewProps) {
30
+ const location = useLocation();
31
+ const navigate = useNavigate();
32
+ const route = parseCapacityRoute(location.pathname);
33
+ const { connection } = useConnection();
34
+ const overview = useCapacityOverview({
35
+ enabled: !route.poolName && route.topTab === "overview",
36
+ });
37
+
38
+ const navigateCapacity = (target: string) => {
39
+ const [pathname, rawSearch = ""] = target.split("?", 2);
40
+ const params = new URLSearchParams(rawSearch);
41
+ navigate({
42
+ pathname,
43
+ search: params.toString() ? `?${params.toString()}` : "",
44
+ });
45
+ };
46
+
47
+ const openPool = (name: string) => navigateCapacity(capacityPoolPath(name));
48
+
49
+ return (
50
+ <div className="flex min-h-0 min-w-0 flex-1 flex-col bg-theme-base">
51
+ {route.poolName ? (
52
+ <CapacityPoolDetail
53
+ key={route.poolName}
54
+ name={route.poolName}
55
+ section={route.poolSection}
56
+ connectionState={connection.state}
57
+ onNavigate={navigateCapacity}
58
+ onOpenResource={onOpenResource}
59
+ />
60
+ ) : route.topTab === "overview" ? (
61
+ <CapacityOverview
62
+ query={overview}
63
+ connectionState={connection.state}
64
+ onOpenPool={openPool}
65
+ onOpenResource={onOpenResource}
66
+ onNavigate={navigateCapacity}
67
+ />
68
+ ) : route.topTab === "demand" ? (
69
+ <CapacityDemand
70
+ connectionState={connection.state}
71
+ onOpenPool={openPool}
72
+ onOpenResource={onOpenResource}
73
+ onNavigate={navigateCapacity}
74
+ />
75
+ ) : (
76
+ <CapacityActivity
77
+ connectionState={connection.state}
78
+ onOpenPool={openPool}
79
+ onOpenResource={onOpenResource}
80
+ onNavigate={navigateCapacity}
81
+ />
82
+ )}
83
+ </div>
84
+ );
85
+ }
@@ -0,0 +1,603 @@
1
+ import { useMemo, useState } from "react";
2
+ import {
3
+ type CapacityCertainty,
4
+ type CapacityQuantityObservation,
5
+ type CapacitySchedulingCapacity,
6
+ } from "@skyhook-io/k8s-ui";
7
+ import { Badge } from "@skyhook-io/k8s-ui/components/ui/Badge";
8
+ import {
9
+ parseCPUToNanocores,
10
+ parseMemoryToBytes,
11
+ } from "@skyhook-io/k8s-ui/utils/format";
12
+ import {
13
+ CertaintyGlyph,
14
+ LinkButton,
15
+ SectionCard,
16
+ certaintyGlyph,
17
+ certaintyValueLabel,
18
+ formatQuantity,
19
+ quantityResourceRank,
20
+ resourceLabel,
21
+ } from "./shared";
22
+
23
+ // Karpenter scheduling capacity — the fleet-level "how full is Karpenter, and
24
+ // how much is knocking at the door" bar. Karpenter-pooled nodes only: on mixed
25
+ // clusters, static capacity is deliberately outside this bar's ledger. Scale
26
+ // semantics are deliberate and asymmetric:
27
+ // · scheduled requests fill the allocatable track proportionally;
28
+ // · in-flight claim capacity extends BEYOND the allocatable edge, to scale —
29
+ // it is capacity that will exist once claims register, never current
30
+ // capacity, so it must not sit inside the track;
31
+ // · pending demand is a COUNT chip after a `//` discontinuity — at 10×
32
+ // overflow no bounded bar can draw it proportionally without lying, so
33
+ // nothing after the `//` is to scale.
34
+ // The bar shows scheduling capacity (requests vs allocatable), never usage,
35
+ // and never acquires health colors.
36
+
37
+ type RowComponent = "requested" | "allocatable" | "inFlight" | "pending";
38
+
39
+ const COMPONENT_LABEL: Record<RowComponent, string> = {
40
+ requested: "requested",
41
+ allocatable: "allocatable",
42
+ inFlight: "in flight",
43
+ pending: "pending demand",
44
+ };
45
+
46
+ const CERTAINTY_BADNESS: Record<CapacityCertainty, number> = {
47
+ exact: 0,
48
+ lower_bound: 1,
49
+ upper_bound: 2,
50
+ unknown: 3,
51
+ };
52
+
53
+ export interface SchedulingBarRow {
54
+ resource: string;
55
+ /** Raw quantity per component; null = that source was not observed
56
+ * (unavailable ≠ zero). A present observation missing this resource is a
57
+ * structural "0". In-flight is the exception: null just means no claims
58
+ * are in flight. */
59
+ requested: string | null;
60
+ allocatable: string | null;
61
+ inFlight: string | null;
62
+ pending: string | null;
63
+ /** Worst certainty across the components that determine the row; unknown
64
+ * when a load-bearing source (requested/allocatable/pending) is absent. */
65
+ certainty: CapacityCertainty;
66
+ /** Per-component certainty of present values, for glyph decomposition. */
67
+ components: { component: RowComponent; certainty: CapacityCertainty }[];
68
+ /** requested/allocatable in [0,1], null when not computable. */
69
+ requestedFraction: number | null;
70
+ /** in-flight relative to allocatable (same scale as the track), null when
71
+ * not computable or no in-flight. */
72
+ inFlightFraction: number | null;
73
+ /** Requests from bound pods with priority < 0 — the SUBSET of `requested`
74
+ * naming potential preemption victims. null = the field was absent (none
75
+ * observed or the source was unobserved); a present-but-missing resource is
76
+ * a structural "0". Never additive with `requested`. */
77
+ negativePriority: string | null;
78
+ /** Certainty of the negative-priority observation, for the annotation glyph;
79
+ * null when the field is absent. */
80
+ negativePriorityCertainty: CapacityCertainty | null;
81
+ /** negativePriority/allocatable in [0,1], populated ONLY when both the
82
+ * negative-priority and allocatable observations are exact — two lower
83
+ * bounds cannot establish a share. null otherwise (the row falls back to a
84
+ * text annotation). */
85
+ negativePriorityFraction: number | null;
86
+ }
87
+
88
+ export function quantityToNumber(
89
+ resource: string,
90
+ value: string,
91
+ ): number | null {
92
+ if (resource === "cpu") {
93
+ const parsed = parseCPUToNanocores(value);
94
+ if (Number.isFinite(parsed) && (parsed !== 0 || /^[0.]+\D*$/.test(value))) {
95
+ return parsed;
96
+ }
97
+ return null;
98
+ }
99
+ // Milli quantities before the byte parser — it ignores the `m` suffix and
100
+ // would read 3000m as 3000, a 1000× distortion.
101
+ const milli = /^(-?\d+(?:\.\d+)?)m$/.exec(value);
102
+ if (milli) return Number(milli[1]) / 1000;
103
+ const parsed = parseMemoryToBytes(value);
104
+ if (Number.isFinite(parsed) && (parsed !== 0 || /^[0.]+\D*$/.test(value))) {
105
+ return parsed;
106
+ }
107
+ const plain = Number(value);
108
+ return Number.isFinite(plain) ? plain : null;
109
+ }
110
+
111
+ function worstCertainty(
112
+ values: (CapacityCertainty | null)[],
113
+ ): CapacityCertainty {
114
+ let worst: CapacityCertainty = "exact";
115
+ for (const value of values) {
116
+ const certainty = value ?? "unknown";
117
+ if (CERTAINTY_BADNESS[certainty] > CERTAINTY_BADNESS[worst]) {
118
+ worst = certainty;
119
+ }
120
+ }
121
+ return worst;
122
+ }
123
+
124
+ export function deriveSchedulingRows(
125
+ scheduling: CapacitySchedulingCapacity | undefined,
126
+ pending: CapacityQuantityObservation | undefined,
127
+ ): SchedulingBarRow[] {
128
+ const observations: Record<
129
+ RowComponent,
130
+ CapacityQuantityObservation | undefined
131
+ > = {
132
+ requested: scheduling?.scheduledRequests,
133
+ allocatable: scheduling?.allocatable,
134
+ inFlight: scheduling?.inFlightCapacity,
135
+ pending,
136
+ };
137
+ const names = new Set<string>(["cpu", "memory"]);
138
+ for (const observation of Object.values(observations)) {
139
+ for (const name of Object.keys(observation?.resources ?? {})) {
140
+ names.add(name);
141
+ }
142
+ }
143
+ // Pod slots stay a chip in the pending-requests row — a bar row for a
144
+ // resource nobody reasons about in units would be noise.
145
+ names.delete("pods");
146
+
147
+ const negativeObservation = scheduling?.negativePriorityRequests;
148
+
149
+ const value = (component: RowComponent, resource: string): string | null => {
150
+ const observation = observations[component];
151
+ if (!observation) return null;
152
+ return observation.resources[resource] ?? "0";
153
+ };
154
+ const numeric = (raw: string | null, resource: string): number | null =>
155
+ raw === null ? null : quantityToNumber(resource, raw);
156
+
157
+ const rows: SchedulingBarRow[] = [];
158
+ for (const resource of names) {
159
+ const requested = value("requested", resource);
160
+ const allocatable = value("allocatable", resource);
161
+ const pendingValue = value("pending", resource);
162
+ let inFlight = value("inFlight", resource);
163
+ const inFlightNumber = numeric(inFlight, resource);
164
+ if (inFlightNumber !== null && inFlightNumber <= 0) inFlight = null;
165
+
166
+ const allocatableNumber = numeric(allocatable, resource);
167
+ const requestedNumber = numeric(requested, resource);
168
+ const pendingNumber = numeric(pendingValue, resource);
169
+ const negativePriority = negativeObservation
170
+ ? (negativeObservation.resources[resource] ?? "0")
171
+ : null;
172
+ const negativePriorityNumber = numeric(negativePriority, resource);
173
+ // A proportional split is only honest when the share's numerator and
174
+ // denominator are both exact — two lower bounds cannot establish a fraction.
175
+ const negativePrioritySplittable =
176
+ negativeObservation?.certainty === "exact" &&
177
+ observations.allocatable?.certainty === "exact";
178
+ // A row earns its place with a signal: capacity that exists, requests or
179
+ // demand that exist, in-flight claims, or an unobserved source worth an
180
+ // honest "not zero". A fully-zero row (empty fleet, nothing pending) is
181
+ // noise — the card-level empty state covers that story.
182
+ const hasSignal =
183
+ allocatable === null ||
184
+ (allocatableNumber ?? 0) > 0 ||
185
+ (requestedNumber ?? 0) > 0 ||
186
+ (pendingNumber ?? 0) > 0 ||
187
+ inFlight !== null;
188
+ if (!hasSignal) continue;
189
+
190
+ const components: SchedulingBarRow["components"] = [];
191
+ for (const component of [
192
+ "requested",
193
+ "allocatable",
194
+ "inFlight",
195
+ "pending",
196
+ ] as const) {
197
+ const observation = observations[component];
198
+ if (observation) {
199
+ components.push({ component, certainty: observation.certainty });
200
+ }
201
+ }
202
+ rows.push({
203
+ resource,
204
+ requested,
205
+ allocatable,
206
+ inFlight,
207
+ pending: pendingValue,
208
+ certainty: worstCertainty([
209
+ observations.requested?.certainty ?? null,
210
+ observations.allocatable?.certainty ?? null,
211
+ observations.pending?.certainty ?? null,
212
+ // Absent in-flight means "none", not "unobserved" — it never
213
+ // degrades the row.
214
+ inFlight === null
215
+ ? "exact"
216
+ : (observations.inFlight?.certainty ?? null),
217
+ ]),
218
+ components,
219
+ requestedFraction:
220
+ requestedNumber !== null &&
221
+ allocatableNumber !== null &&
222
+ allocatableNumber > 0
223
+ ? Math.min(1, requestedNumber / allocatableNumber)
224
+ : null,
225
+ inFlightFraction:
226
+ inFlightNumber !== null &&
227
+ inFlightNumber > 0 &&
228
+ allocatableNumber !== null &&
229
+ allocatableNumber > 0
230
+ ? inFlightNumber / allocatableNumber
231
+ : null,
232
+ negativePriority,
233
+ negativePriorityCertainty: negativeObservation?.certainty ?? null,
234
+ negativePriorityFraction:
235
+ negativePrioritySplittable &&
236
+ negativePriorityNumber !== null &&
237
+ negativePriorityNumber > 0 &&
238
+ allocatableNumber !== null &&
239
+ allocatableNumber > 0
240
+ ? Math.min(1, negativePriorityNumber / allocatableNumber)
241
+ : null,
242
+ });
243
+ }
244
+ rows.sort(
245
+ (left, right) =>
246
+ quantityResourceRank(left.resource) -
247
+ quantityResourceRank(right.resource) ||
248
+ left.resource.localeCompare(right.resource),
249
+ );
250
+ return rows;
251
+ }
252
+
253
+ const VISIBLE_ROW_LIMIT = 3;
254
+
255
+ // The bar has one explicit scope. Cluster scope draws scheduled requests vs
256
+ // allocatable across EVERY observed node (all managers); Karpenter scope keeps
257
+ // the original Karpenter-pooled-only ledger. In-flight capacity is Karpenter-
258
+ // only in both — no other manager reports claim capacity before it registers —
259
+ // so the cluster legend spells that out.
260
+ type SchedulingScope = "cluster" | "karpenter";
261
+
262
+ const SCOPE_COPY: Record<
263
+ SchedulingScope,
264
+ { title: string; subtitle: string; inFlightLegend: string; empty: string }
265
+ > = {
266
+ cluster: {
267
+ title: "Cluster scheduling capacity",
268
+ subtitle:
269
+ "scheduled requests vs node allocatable, all observed nodes · not usage, not a health score",
270
+ inFlightLegend: "in flight (Karpenter, beyond the edge)",
271
+ empty:
272
+ "No scheduling capacity observed yet — no node reports allocatable, and nothing is pending. Bars appear as soon as either side of the ledger exists.",
273
+ },
274
+ karpenter: {
275
+ title: "Karpenter scheduling capacity",
276
+ subtitle:
277
+ "scheduled requests vs node allocatable, Karpenter-pooled nodes only · not usage, not a health score",
278
+ inFlightLegend: "in flight (beyond the edge)",
279
+ empty:
280
+ "No Karpenter-provisioned capacity yet — the fleet has no pooled nodes or in-flight claims, and no pending demand. Bars appear as soon as either side of the ledger exists.",
281
+ },
282
+ };
283
+
284
+ export function ClusterSchedulingCard({
285
+ scheduling,
286
+ pending,
287
+ onExplain,
288
+ scope = "karpenter",
289
+ }: {
290
+ scheduling?: CapacitySchedulingCapacity;
291
+ pending?: CapacityQuantityObservation;
292
+ onExplain: () => void;
293
+ scope?: SchedulingScope;
294
+ }) {
295
+ const rows = useMemo(
296
+ () => deriveSchedulingRows(scheduling, pending),
297
+ [scheduling, pending],
298
+ );
299
+ const [expanded, setExpanded] = useState(false);
300
+ if (!scheduling) return null;
301
+ const copy = SCOPE_COPY[scope];
302
+ const visible = expanded ? rows : rows.slice(0, VISIBLE_ROW_LIMIT);
303
+ const hidden = rows.length - visible.length;
304
+ // The hatch legend swatch describes a drawn sub-band; under non-exact
305
+ // certainty the negative-priority value is a text annotation instead, which is
306
+ // self-describing, so the swatch would describe nothing.
307
+ const hasNegativePriorityBand = rows.some(
308
+ (row) => row.negativePriorityFraction !== null,
309
+ );
310
+
311
+ if (rows.length === 0) {
312
+ return (
313
+ <SectionCard
314
+ title={copy.title}
315
+ subtitle={copy.subtitle}
316
+ actions={<LinkButton onClick={onExplain}>How to read →</LinkButton>}
317
+ bodyClassName="px-4 py-3"
318
+ >
319
+ <p className="text-xs text-theme-text-tertiary">{copy.empty}</p>
320
+ </SectionCard>
321
+ );
322
+ }
323
+
324
+ return (
325
+ <SectionCard
326
+ title={copy.title}
327
+ subtitle={copy.subtitle}
328
+ actions={<LinkButton onClick={onExplain}>How to read →</LinkButton>}
329
+ bodyClassName="flex flex-col gap-3 px-4 py-3"
330
+ >
331
+ {visible.map((row) => (
332
+ <SchedulingBarRowView key={row.resource} row={row} />
333
+ ))}
334
+ {hidden > 0 && (
335
+ <LinkButton onClick={() => setExpanded(true)}>
336
+ +{hidden} more {hidden === 1 ? "resource" : "resources"} ▾
337
+ </LinkButton>
338
+ )}
339
+ {expanded && rows.length > VISIBLE_ROW_LIMIT && (
340
+ <LinkButton onClick={() => setExpanded(false)}>Show less ▴</LinkButton>
341
+ )}
342
+ <div className="flex flex-wrap items-center gap-x-4 gap-y-1 border-t border-theme-border-subtle pt-2 text-[11px] text-theme-text-tertiary">
343
+ <span className="flex items-center gap-1.5">
344
+ <span aria-hidden className="h-2 w-4 rounded-sm bg-blue-500/70" />
345
+ scheduled requests
346
+ </span>
347
+ <span className="flex items-center gap-1.5">
348
+ <span
349
+ aria-hidden
350
+ className="h-2 w-4 rounded-sm border border-theme-border"
351
+ style={HATCH_STYLE}
352
+ />
353
+ {copy.inFlightLegend}
354
+ </span>
355
+ <span className="flex items-center gap-1.5">
356
+ <span
357
+ aria-hidden
358
+ className="h-2 w-4 rounded-sm border border-dashed border-orange-400"
359
+ />
360
+ pending demand — count, not to scale
361
+ </span>
362
+ {hasNegativePriorityBand && (
363
+ <span className="flex items-center gap-1.5">
364
+ <span
365
+ aria-hidden
366
+ className="h-2 w-4 rounded-sm bg-blue-500/70 text-theme-text-primary"
367
+ style={NEGATIVE_PRIORITY_HATCH_STYLE}
368
+ />
369
+ negative-priority (potential preemption victims)
370
+ </span>
371
+ )}
372
+ </div>
373
+ </SectionCard>
374
+ );
375
+ }
376
+
377
+ // currentColor hatch keeps the pattern legible on both themes without
378
+ // hand-picking a color pair.
379
+ const HATCH_STYLE = {
380
+ backgroundImage:
381
+ "repeating-linear-gradient(135deg, currentColor 0 1px, transparent 1px 5px)",
382
+ } as const;
383
+
384
+ // A denser, opposite-angle hatch distinguishes the negative-priority sub-band
385
+ // from the in-flight hatch and the solid requested fill it sits inside. Same
386
+ // currentColor approach — no hand-picked color pair.
387
+ const NEGATIVE_PRIORITY_HATCH_STYLE = {
388
+ backgroundImage:
389
+ "repeating-linear-gradient(45deg, currentColor 0 1px, transparent 1px 4px)",
390
+ } as const;
391
+
392
+ function rowGlyphTitle(row: SchedulingBarRow): string {
393
+ const parts = row.components.map(
394
+ ({ component, certainty }) =>
395
+ `${COMPONENT_LABEL[component]} ${certaintyGlyph(certainty)} ${certaintyValueLabel(certainty).toLowerCase()}`,
396
+ );
397
+ return `Worst-case certainty across this row. ${parts.join(" · ")}.`;
398
+ }
399
+
400
+ function SchedulingBarRowView({ row }: { row: SchedulingBarRow }) {
401
+ const pendingNumber =
402
+ row.pending === null ? null : quantityToNumber(row.resource, row.pending);
403
+ const showPendingChip = pendingNumber !== null && pendingNumber > 0;
404
+ const negativePriorityNumber =
405
+ row.negativePriority === null
406
+ ? null
407
+ : quantityToNumber(row.resource, row.negativePriority);
408
+ // Band XOR annotation: the exact case draws the proportional sub-band; any
409
+ // present-but-unsplittable value falls back to an honest text annotation whose
410
+ // glyph carries the certainty (≥ under a lower bound).
411
+ const showNegativeAnnotation =
412
+ row.negativePriorityFraction === null &&
413
+ negativePriorityNumber !== null &&
414
+ negativePriorityNumber > 0;
415
+ const ariaLabel = [
416
+ resourceLabel(row.resource),
417
+ row.requested !== null
418
+ ? `${formatQuantity(row.resource, row.requested)} requested`
419
+ : "requested unknown",
420
+ row.allocatable !== null
421
+ ? `of ${formatQuantity(row.resource, row.allocatable)} allocatable`
422
+ : "allocatable unknown",
423
+ row.inFlight !== null
424
+ ? `${formatQuantity(row.resource, row.inFlight)} in flight`
425
+ : null,
426
+ row.pending !== null
427
+ ? `${formatQuantity(row.resource, row.pending)} pending demand`
428
+ : "pending demand unknown",
429
+ row.negativePriority !== null &&
430
+ negativePriorityNumber !== null &&
431
+ negativePriorityNumber > 0
432
+ ? `${formatQuantity(row.resource, row.negativePriority)} negative-priority`
433
+ : null,
434
+ ]
435
+ .filter(Boolean)
436
+ .join(", ");
437
+
438
+ return (
439
+ <div role="group" aria-label={ariaLabel}>
440
+ <div className="flex items-center gap-3">
441
+ <span className="flex w-44 shrink-0 items-center gap-1.5">
442
+ <span className="truncate font-mono text-xs text-theme-text-primary">
443
+ {row.resource === "cpu" || row.resource === "memory"
444
+ ? resourceLabel(row.resource)
445
+ : row.resource}
446
+ </span>
447
+ {row.certainty !== "exact" && (
448
+ <CertaintyGlyph
449
+ certainty={row.certainty}
450
+ title={rowGlyphTitle(row)}
451
+ />
452
+ )}
453
+ </span>
454
+ {row.allocatable === null ? (
455
+ <span className="text-xs text-theme-text-tertiary">
456
+ Node inventory not observed — capacity unknown, not zero.
457
+ </span>
458
+ ) : (
459
+ <div className="flex min-w-0 flex-1 items-center">
460
+ <div
461
+ className="relative h-2.5 min-w-3 overflow-hidden rounded-l-full rounded-r-none border border-theme-border bg-theme-elevated"
462
+ style={{
463
+ flexGrow: Math.max(
464
+ quantityToNumber(row.resource, row.allocatable) ?? 0,
465
+ 0,
466
+ ),
467
+ flexBasis: 12,
468
+ borderTopRightRadius: row.inFlightFraction ? 0 : undefined,
469
+ borderBottomRightRadius: row.inFlightFraction ? 0 : undefined,
470
+ }}
471
+ >
472
+ {row.requestedFraction !== null && (
473
+ <div
474
+ className="flex h-full justify-end bg-blue-500/70"
475
+ style={{ width: `${row.requestedFraction * 100}%` }}
476
+ >
477
+ {row.negativePriorityFraction !== null && (
478
+ <div
479
+ aria-hidden
480
+ className="h-full border-l border-theme-base text-theme-text-primary"
481
+ style={{
482
+ ...NEGATIVE_PRIORITY_HATCH_STYLE,
483
+ width: `${
484
+ Math.min(
485
+ 1,
486
+ row.negativePriorityFraction /
487
+ row.requestedFraction,
488
+ ) * 100
489
+ }%`,
490
+ }}
491
+ />
492
+ )}
493
+ </div>
494
+ )}
495
+ </div>
496
+ {row.inFlightFraction !== null && (
497
+ <div
498
+ aria-hidden
499
+ className="h-2.5 rounded-r-full border border-l-2 border-theme-border border-l-theme-text-tertiary text-theme-text-tertiary"
500
+ style={{
501
+ ...HATCH_STYLE,
502
+ flexGrow:
503
+ (quantityToNumber(row.resource, row.allocatable) ?? 0) *
504
+ row.inFlightFraction,
505
+ flexBasis: 8,
506
+ }}
507
+ />
508
+ )}
509
+ {showPendingChip && (
510
+ <>
511
+ <span
512
+ aria-hidden
513
+ className="shrink-0 px-1.5 font-mono text-xs text-theme-text-tertiary"
514
+ >
515
+ {"//"}
516
+ </span>
517
+ <Badge
518
+ severity="alert"
519
+ size="sm"
520
+ className="shrink-0 border-dashed font-mono"
521
+ >
522
+ +{formatQuantity(row.resource, row.pending ?? "")} pending
523
+ </Badge>
524
+ </>
525
+ )}
526
+ </div>
527
+ )}
528
+ </div>
529
+ <div className="ml-[11.75rem] mt-1 flex flex-wrap items-baseline gap-x-3 gap-y-0.5 text-xs">
530
+ <NumberPart
531
+ value={
532
+ row.requested !== null
533
+ ? formatQuantity(row.resource, row.requested)
534
+ : null
535
+ }
536
+ label="requested"
537
+ missing="requested unavailable — not zero"
538
+ />
539
+ <NumberPart
540
+ value={
541
+ row.allocatable !== null
542
+ ? formatQuantity(row.resource, row.allocatable)
543
+ : null
544
+ }
545
+ label="allocatable"
546
+ prefix="of"
547
+ missing="allocatable unavailable — not zero"
548
+ />
549
+ {row.inFlight !== null && (
550
+ <NumberPart
551
+ value={`+${formatQuantity(row.resource, row.inFlight)}`}
552
+ label="in flight"
553
+ />
554
+ )}
555
+ {row.pending !== null &&
556
+ pendingNumber !== null &&
557
+ pendingNumber > 0 && (
558
+ <span className="text-orange-700 dark:text-orange-400">
559
+ <span className="font-mono">
560
+ {formatQuantity(row.resource, row.pending)}
561
+ </span>{" "}
562
+ pending demand
563
+ </span>
564
+ )}
565
+ {showNegativeAnnotation && row.negativePriority !== null && (
566
+ <span className="text-theme-text-tertiary">
567
+ <span className="font-mono text-theme-text-secondary">
568
+ {`${certaintyGlyph(
569
+ row.negativePriorityCertainty ?? "unknown",
570
+ )}${formatQuantity(row.resource, row.negativePriority)}`}
571
+ </span>{" "}
572
+ negative-priority
573
+ </span>
574
+ )}
575
+ </div>
576
+ </div>
577
+ );
578
+ }
579
+
580
+ function NumberPart({
581
+ value,
582
+ label,
583
+ prefix,
584
+ missing,
585
+ }: {
586
+ value: string | null;
587
+ label: string;
588
+ prefix?: string;
589
+ missing?: string;
590
+ }) {
591
+ if (value === null) {
592
+ return missing ? (
593
+ <span className="text-theme-text-tertiary">{missing}</span>
594
+ ) : null;
595
+ }
596
+ return (
597
+ <span className="text-theme-text-secondary">
598
+ {prefix ? `${prefix} ` : ""}
599
+ <span className="font-mono text-theme-text-primary">{value}</span>{" "}
600
+ <span className="text-theme-text-tertiary">{label}</span>
601
+ </span>
602
+ );
603
+ }