@skyhook-io/radar-app 1.9.1 → 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.
- package/package.json +1 -1
- package/src/App.tsx +42 -7
- package/src/api/apiResources.test.ts +11 -0
- package/src/api/apiResources.ts +51 -12
- package/src/api/client.capacity.test.ts +92 -0
- package/src/api/client.ts +2884 -2075
- package/src/api/diagnose.ts +5 -0
- package/src/components/ConnectionErrorView.test.tsx +88 -0
- package/src/components/ConnectionErrorView.tsx +128 -22
- package/src/components/capacity/CapacityActivity.tsx +787 -0
- package/src/components/capacity/CapacityDemand.tsx +961 -0
- package/src/components/capacity/CapacityOverview.tsx +1529 -0
- package/src/components/capacity/CapacityPoolDetail.tsx +1626 -0
- package/src/components/capacity/CapacityView.test.tsx +2287 -0
- package/src/components/capacity/CapacityView.tsx +85 -0
- package/src/components/capacity/ClusterSchedulingCard.tsx +603 -0
- package/src/components/capacity/DemandNomination.test.tsx +151 -0
- package/src/components/capacity/certaintyGlyph.test.tsx +191 -0
- package/src/components/capacity/coverageCertainty.test.ts +162 -0
- package/src/components/capacity/podDemandGate.test.ts +47 -0
- package/src/components/capacity/podDemandGate.ts +22 -0
- package/src/components/capacity/schedulingBar.test.ts +244 -0
- package/src/components/capacity/shared.tsx +1841 -0
- package/src/components/diagnose/AgentSetupNotice.tsx +117 -0
- package/src/components/diagnose/DiagnoseContext.tsx +51 -10
- package/src/components/diagnose/DiagnoseSurface.tsx +20 -7
- package/src/components/diagnose/LocalDiagnoseAction.tsx +50 -27
- package/src/components/diagnose/agentCatalog.ts +30 -0
- package/src/components/home/CapacityCard.test.tsx +150 -0
- package/src/components/home/CapacityCard.tsx +125 -0
- package/src/components/home/HomeView.tsx +15 -1
- package/src/components/issues/IssuesPane.test.ts +142 -0
- package/src/components/issues/IssuesPane.tsx +142 -38
- package/src/components/nav/PrimaryNavRail.test.tsx +20 -0
- package/src/components/nav/PrimaryNavRail.tsx +191 -103
- package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +29 -1
- package/src/components/resources/renderers/PodRenderer.tsx +32 -3
- package/src/components/ui/command-items.ts +222 -98
- package/src/components/workload/WorkloadView.tsx +16 -83
- package/src/context/ConnectionContext.test.ts +39 -0
- package/src/context/ConnectionContext.tsx +155 -51
- package/src/utils/shell-safe.test.ts +55 -0
- package/src/utils/shell-safe.ts +21 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { CapacityQuantityObservation } from "@skyhook-io/k8s-ui";
|
|
3
|
+
import {
|
|
4
|
+
deriveSchedulingRows,
|
|
5
|
+
quantityToNumber,
|
|
6
|
+
} from "./ClusterSchedulingCard";
|
|
7
|
+
import { claimStagesDetail } from "./CapacityOverview";
|
|
8
|
+
|
|
9
|
+
function observation(
|
|
10
|
+
resources: Record<string, string>,
|
|
11
|
+
certainty: CapacityQuantityObservation["certainty"] = "exact",
|
|
12
|
+
): CapacityQuantityObservation {
|
|
13
|
+
return {
|
|
14
|
+
resources,
|
|
15
|
+
certainty,
|
|
16
|
+
sources: ["test"],
|
|
17
|
+
asOf: "2026-07-16T00:00:00Z",
|
|
18
|
+
granularity: "aggregate",
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe("deriveSchedulingRows", () => {
|
|
23
|
+
it("builds proportional rows and keeps pod slots out", () => {
|
|
24
|
+
const rows = deriveSchedulingRows(
|
|
25
|
+
{
|
|
26
|
+
scheduledRequests: observation({
|
|
27
|
+
cpu: "21900m",
|
|
28
|
+
memory: "79Gi",
|
|
29
|
+
pods: "27",
|
|
30
|
+
}),
|
|
31
|
+
allocatable: observation({
|
|
32
|
+
cpu: "31200m",
|
|
33
|
+
memory: "102Gi",
|
|
34
|
+
pods: "220",
|
|
35
|
+
}),
|
|
36
|
+
inFlightCapacity: observation({ cpu: "4" }),
|
|
37
|
+
},
|
|
38
|
+
observation({ cpu: "48750m", memory: "193Gi", "nvidia.com/gpu": "8" }),
|
|
39
|
+
);
|
|
40
|
+
expect(rows.map((row) => row.resource)).toEqual([
|
|
41
|
+
"cpu",
|
|
42
|
+
"memory",
|
|
43
|
+
"nvidia.com/gpu",
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
const cpu = rows[0];
|
|
47
|
+
expect(cpu.requested).toBe("21900m");
|
|
48
|
+
expect(cpu.allocatable).toBe("31200m");
|
|
49
|
+
expect(cpu.inFlight).toBe("4");
|
|
50
|
+
expect(cpu.pending).toBe("48750m");
|
|
51
|
+
expect(cpu.requestedFraction).toBeCloseTo(21.9 / 31.2, 5);
|
|
52
|
+
expect(cpu.inFlightFraction).toBeCloseTo(4 / 31.2, 5);
|
|
53
|
+
expect(cpu.certainty).toBe("exact");
|
|
54
|
+
|
|
55
|
+
// GPU: present allocatable observation without the key is a structural
|
|
56
|
+
// zero, and the row exists because pending demand names the resource.
|
|
57
|
+
const gpu = rows[2];
|
|
58
|
+
expect(gpu.allocatable).toBe("0");
|
|
59
|
+
expect(gpu.requested).toBe("0");
|
|
60
|
+
expect(gpu.pending).toBe("8");
|
|
61
|
+
expect(gpu.requestedFraction).toBeNull();
|
|
62
|
+
expect(gpu.inFlight).toBeNull();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("degrades row certainty to the worst present component", () => {
|
|
66
|
+
const rows = deriveSchedulingRows(
|
|
67
|
+
{
|
|
68
|
+
scheduledRequests: observation({ cpu: "1" }),
|
|
69
|
+
allocatable: observation({ cpu: "4" }, "lower_bound"),
|
|
70
|
+
},
|
|
71
|
+
observation({ cpu: "0" }),
|
|
72
|
+
);
|
|
73
|
+
expect(rows[0].certainty).toBe("lower_bound");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("treats an unobserved source as unknown, never zero", () => {
|
|
77
|
+
const rows = deriveSchedulingRows(
|
|
78
|
+
{ scheduledRequests: observation({ cpu: "1" }) },
|
|
79
|
+
undefined,
|
|
80
|
+
);
|
|
81
|
+
const cpu = rows.find((row) => row.resource === "cpu");
|
|
82
|
+
expect(cpu?.allocatable).toBeNull();
|
|
83
|
+
expect(cpu?.pending).toBeNull();
|
|
84
|
+
expect(cpu?.certainty).toBe("unknown");
|
|
85
|
+
expect(cpu?.requestedFraction).toBeNull();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("never lets absent in-flight degrade the row", () => {
|
|
89
|
+
const rows = deriveSchedulingRows(
|
|
90
|
+
{
|
|
91
|
+
scheduledRequests: observation({ cpu: "1" }),
|
|
92
|
+
allocatable: observation({ cpu: "4" }),
|
|
93
|
+
},
|
|
94
|
+
observation({}),
|
|
95
|
+
);
|
|
96
|
+
expect(rows[0].inFlight).toBeNull();
|
|
97
|
+
expect(rows[0].certainty).toBe("exact");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("drops rows carrying no signal at all", () => {
|
|
101
|
+
const rows = deriveSchedulingRows(
|
|
102
|
+
{
|
|
103
|
+
scheduledRequests: observation({ cpu: "1", "example.com/fpga": "0" }),
|
|
104
|
+
allocatable: observation({ cpu: "4" }),
|
|
105
|
+
},
|
|
106
|
+
observation({}),
|
|
107
|
+
);
|
|
108
|
+
// Memory and the fpga row are all-zero against observed sources — noise.
|
|
109
|
+
expect(rows.map((row) => row.resource)).toEqual(["cpu"]);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("returns no rows for an empty fleet with no demand", () => {
|
|
113
|
+
// One pool, zero nodes, zero claims, nothing pending: every value is an
|
|
114
|
+
// observed zero. Stub bars would read as broken UI — the card renders an
|
|
115
|
+
// honest empty state instead.
|
|
116
|
+
const rows = deriveSchedulingRows(
|
|
117
|
+
{
|
|
118
|
+
scheduledRequests: observation({}),
|
|
119
|
+
allocatable: observation({}),
|
|
120
|
+
},
|
|
121
|
+
observation({}),
|
|
122
|
+
);
|
|
123
|
+
expect(rows).toEqual([]);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe("deriveSchedulingRows negative-priority split", () => {
|
|
128
|
+
it("computes a proportional fraction under exact coverage", () => {
|
|
129
|
+
const rows = deriveSchedulingRows(
|
|
130
|
+
{
|
|
131
|
+
scheduledRequests: observation({ cpu: "6", memory: "24Gi" }),
|
|
132
|
+
allocatable: observation({ cpu: "12", memory: "48Gi" }),
|
|
133
|
+
negativePriorityRequests: observation({ cpu: "3" }),
|
|
134
|
+
},
|
|
135
|
+
undefined,
|
|
136
|
+
);
|
|
137
|
+
const cpu = rows.find((row) => row.resource === "cpu");
|
|
138
|
+
expect(cpu?.negativePriority).toBe("3");
|
|
139
|
+
expect(cpu?.negativePriorityCertainty).toBe("exact");
|
|
140
|
+
expect(cpu?.negativePriorityFraction).toBeCloseTo(3 / 12, 5);
|
|
141
|
+
// Memory carries no negative-priority key: a present observation missing the
|
|
142
|
+
// resource is a structural "0", never a band.
|
|
143
|
+
const memory = rows.find((row) => row.resource === "memory");
|
|
144
|
+
expect(memory?.negativePriority).toBe("0");
|
|
145
|
+
expect(memory?.negativePriorityFraction).toBeNull();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("draws no proportional split under a lower-bound observation", () => {
|
|
149
|
+
const rows = deriveSchedulingRows(
|
|
150
|
+
{
|
|
151
|
+
scheduledRequests: observation({ cpu: "6" }, "lower_bound"),
|
|
152
|
+
allocatable: observation({ cpu: "12" }),
|
|
153
|
+
negativePriorityRequests: observation({ cpu: "3" }, "lower_bound"),
|
|
154
|
+
},
|
|
155
|
+
undefined,
|
|
156
|
+
);
|
|
157
|
+
const cpu = rows.find((row) => row.resource === "cpu");
|
|
158
|
+
// Two lower bounds cannot establish a share — the value survives for the
|
|
159
|
+
// text annotation, but no proportional band is drawn.
|
|
160
|
+
expect(cpu?.negativePriority).toBe("3");
|
|
161
|
+
expect(cpu?.negativePriorityCertainty).toBe("lower_bound");
|
|
162
|
+
expect(cpu?.negativePriorityFraction).toBeNull();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("requires allocatable itself to be exact for a proportional split", () => {
|
|
166
|
+
const rows = deriveSchedulingRows(
|
|
167
|
+
{
|
|
168
|
+
scheduledRequests: observation({ cpu: "6" }),
|
|
169
|
+
allocatable: observation({ cpu: "12" }, "lower_bound"),
|
|
170
|
+
negativePriorityRequests: observation({ cpu: "3" }),
|
|
171
|
+
},
|
|
172
|
+
undefined,
|
|
173
|
+
);
|
|
174
|
+
const cpu = rows.find((row) => row.resource === "cpu");
|
|
175
|
+
expect(cpu?.negativePriority).toBe("3");
|
|
176
|
+
expect(cpu?.negativePriorityFraction).toBeNull();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("leaves negative-priority null when the field is absent", () => {
|
|
180
|
+
const rows = deriveSchedulingRows(
|
|
181
|
+
{
|
|
182
|
+
scheduledRequests: observation({ cpu: "6" }),
|
|
183
|
+
allocatable: observation({ cpu: "12" }),
|
|
184
|
+
},
|
|
185
|
+
undefined,
|
|
186
|
+
);
|
|
187
|
+
const cpu = rows.find((row) => row.resource === "cpu");
|
|
188
|
+
expect(cpu?.negativePriority).toBeNull();
|
|
189
|
+
expect(cpu?.negativePriorityCertainty).toBeNull();
|
|
190
|
+
expect(cpu?.negativePriorityFraction).toBeNull();
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe("quantityToNumber", () => {
|
|
195
|
+
it("parses cpu, memory, and extended counts", () => {
|
|
196
|
+
expect(quantityToNumber("cpu", "500m")).toBeGreaterThan(0);
|
|
197
|
+
expect(quantityToNumber("cpu", "0")).toBe(0);
|
|
198
|
+
expect(quantityToNumber("memory", "1Gi")).toBe(1024 ** 3);
|
|
199
|
+
expect(quantityToNumber("nvidia.com/gpu", "8")).toBe(8);
|
|
200
|
+
expect(quantityToNumber("nvidia.com/gpu", "not-a-number")).toBeNull();
|
|
201
|
+
// Milli quantities must not read 1000× large through the byte parser.
|
|
202
|
+
expect(quantityToNumber("example.com/widget", "3000m")).toBe(3);
|
|
203
|
+
expect(quantityToNumber("cpu", "1500m")).toBe(1.5e9);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe("claimStagesDetail", () => {
|
|
208
|
+
it("lists nonzero stages in lifecycle order with the orphan note", () => {
|
|
209
|
+
expect(
|
|
210
|
+
claimStagesDetail(
|
|
211
|
+
{
|
|
212
|
+
total: 10,
|
|
213
|
+
pending: 0,
|
|
214
|
+
launched: 1,
|
|
215
|
+
registered: 0,
|
|
216
|
+
initialized: 0,
|
|
217
|
+
ready: 8,
|
|
218
|
+
failed: 1,
|
|
219
|
+
terminating: 0,
|
|
220
|
+
},
|
|
221
|
+
1,
|
|
222
|
+
),
|
|
223
|
+
).toBe("8 ready · 1 launched · 1 failed · +1 orphaned");
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("is honest about an empty fleet and absent rollups", () => {
|
|
227
|
+
expect(
|
|
228
|
+
claimStagesDetail(
|
|
229
|
+
{
|
|
230
|
+
total: 0,
|
|
231
|
+
pending: 0,
|
|
232
|
+
launched: 0,
|
|
233
|
+
registered: 0,
|
|
234
|
+
initialized: 0,
|
|
235
|
+
ready: 0,
|
|
236
|
+
failed: 0,
|
|
237
|
+
terminating: 0,
|
|
238
|
+
},
|
|
239
|
+
0,
|
|
240
|
+
),
|
|
241
|
+
).toBe("none yet");
|
|
242
|
+
expect(claimStagesDetail(undefined, 3)).toBeNull();
|
|
243
|
+
});
|
|
244
|
+
});
|