@skyhook-io/k8s-ui 1.10.4 → 1.11.0
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 +5 -5
- package/src/components/applications/ApplicationDetail.test.tsx +46 -0
- package/src/components/applications/ApplicationDetail.tsx +14 -15
- package/src/components/issues/IssuesView.tsx +9 -0
- package/src/components/issues/diagnostic.ts +3 -0
- package/src/components/issues/issues.test.ts +11 -0
- package/src/components/issues/severity.ts +3 -0
- package/src/components/issues/types.ts +3 -0
- package/src/components/resources/ResourcesSidebar.tsx +1 -1
- package/src/components/resources/ResourcesView.tsx +259 -30
- package/src/components/resources/index.ts +2 -0
- package/src/components/resources/kyverno-cell-gating.test.ts +93 -0
- package/src/components/resources/kyverno-modern-posture.test.ts +290 -0
- package/src/components/resources/renderers/CNPGClusterRenderer.test.tsx +85 -0
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +109 -9
- package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +8 -4
- package/src/components/resources/renderers/EventRenderer.test.tsx +23 -0
- package/src/components/resources/renderers/EventRenderer.tsx +10 -18
- package/src/components/resources/renderers/KyvernoCELPolicyRenderers.tsx +317 -0
- package/src/components/resources/renderers/KyvernoExceptionRenderers.tsx +264 -0
- package/src/components/resources/renderers/KyvernoPolicyShared.tsx +212 -0
- package/src/components/resources/renderers/VeleroBSLRenderer.tsx +2 -4
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +32 -9
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +30 -9
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +12 -6
- package/src/components/resources/renderers/badge-no-handrolled.test.tsx +1 -1
- package/src/components/resources/renderers/cnpg-cells.tsx +37 -2
- package/src/components/resources/renderers/index.ts +4 -0
- package/src/components/resources/renderers/kyverno-modern-cells.tsx +152 -0
- package/src/components/resources/renderers/velero-cells.tsx +128 -9
- package/src/components/resources/renderers/velero-phase-recovery.test.tsx +79 -0
- package/src/components/resources/resource-utils-cnpg.golden.test.ts +112 -0
- package/src/components/resources/resource-utils-cnpg.test.ts +527 -1
- package/src/components/resources/resource-utils-cnpg.ts +373 -56
- package/src/components/resources/resource-utils-kyverno-exceptions.ts +182 -0
- package/src/components/resources/resource-utils-kyverno-modern.ts +588 -0
- package/src/components/resources/resource-utils-velero.test.ts +237 -0
- package/src/components/resources/resource-utils-velero.ts +214 -22
- package/src/components/resources/resource-utils.ts +70 -3
- package/src/components/shared/ResourceActionsBar.tsx +3 -1
- package/src/components/shared/ResourceRendererDispatch.test.tsx +119 -1
- package/src/components/shared/ResourceRendererDispatch.tsx +116 -22
- package/src/components/topology/K8sResourceNode.tsx +11 -0
- package/src/components/topology/TopologyGraph.tsx +85 -5
- package/src/components/trace/ReachabilityGraph.tsx +517 -0
- package/src/components/trace/ReachabilityView.tsx +1173 -0
- package/src/components/trace/TracePanel.test.ts +172 -0
- package/src/components/trace/TracePanel.tsx +558 -0
- package/src/components/trace/TraceSummary.test.ts +213 -0
- package/src/components/trace/TraceSummary.tsx +168 -0
- package/src/components/trace/inClusterSummary.test.ts +98 -0
- package/src/components/trace/inClusterSummary.ts +81 -0
- package/src/components/trace/index.ts +19 -0
- package/src/components/trace/podReach.test.ts +71 -0
- package/src/components/trace/podReach.ts +37 -0
- package/src/components/trace/probe-display.test.ts +107 -0
- package/src/components/trace/probe-display.ts +37 -0
- package/src/components/trace/problemRows.test.ts +107 -0
- package/src/components/trace/reachFixtures.ts +261 -0
- package/src/components/trace/reachGraphModel.test.ts +1423 -0
- package/src/components/trace/reachGraphModel.ts +1521 -0
- package/src/components/trace/reachInspector.test.ts +892 -0
- package/src/components/trace/reachInspector.ts +797 -0
- package/src/components/trace/reachMarks.test.ts +698 -0
- package/src/components/trace/reachMarks.ts +623 -0
- package/src/components/trace/reachOrigins.test.ts +392 -0
- package/src/components/trace/reachOrigins.ts +449 -0
- package/src/components/trace/reachVerdict.test.ts +537 -0
- package/src/components/trace/reachVerdict.ts +601 -0
- package/src/components/trace/traceFingerprint.test.ts +214 -0
- package/src/components/trace/traceFingerprint.ts +122 -0
- package/src/components/trace/traceToSubgraph.test.ts +623 -0
- package/src/components/trace/traceToSubgraph.ts +463 -0
- package/src/components/trace/types.ts +414 -0
- package/src/components/ui/Badge.tsx +36 -0
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +12 -1
- package/src/components/ui/InClusterConsentDialog.test.tsx +83 -0
- package/src/components/ui/InClusterConsentDialog.tsx +141 -0
- package/src/components/ui/index.ts +1 -0
- package/src/components/workload/WorkloadView.tsx +58 -5
- package/src/components/workload/index.ts +1 -1
- package/src/components/workload/reachabilityTab.test.ts +47 -0
- package/src/index.ts +5 -0
- package/src/theme/components.css +64 -0
- package/src/theme/variables.css +5 -0
- package/src/types/core.ts +8 -0
- package/src/utils/api-resources.ts +2 -0
- package/src/utils/application-history.test.ts +1 -1
- package/src/utils/applications.test.ts +63 -1
- package/src/utils/applications.ts +27 -0
- package/src/utils/inClusterConsent.test.ts +109 -0
- package/src/utils/inClusterConsent.ts +72 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/navigation.test.ts +61 -0
- package/src/utils/navigation.ts +11 -9
- package/src/utils/pluralize.test.ts +80 -1
- package/src/utils/pluralize.ts +34 -0
- package/src/utils/resource-icons.test.ts +33 -0
- package/src/utils/resource-icons.ts +65 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skyhook-io/k8s-ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/skyhook-io/radar",
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
},
|
|
90
90
|
"devDependencies": {
|
|
91
91
|
"@types/node": "^26.1.1",
|
|
92
|
-
"@types/react": "^19.2.
|
|
92
|
+
"@types/react": "^19.2.18",
|
|
93
93
|
"@types/react-dom": "^19.2.3",
|
|
94
94
|
"@xterm/addon-fit": "^0.11.0",
|
|
95
95
|
"@xterm/addon-web-links": "^0.12.0",
|
|
@@ -98,10 +98,10 @@
|
|
|
98
98
|
"clsx": "^2.1.1",
|
|
99
99
|
"diff": "^9.0.0",
|
|
100
100
|
"elkjs": "^0.11.1",
|
|
101
|
-
"lucide-react": "^1.
|
|
101
|
+
"lucide-react": "^1.28.0",
|
|
102
102
|
"monaco-editor": "0.55.1",
|
|
103
|
-
"react": "^19.2.
|
|
104
|
-
"react-dom": "^19.2.
|
|
103
|
+
"react": "^19.2.8",
|
|
104
|
+
"react-dom": "^19.2.8",
|
|
105
105
|
"typescript": "^6.0.2",
|
|
106
106
|
"vitest": "^4.1.10",
|
|
107
107
|
"yaml": "^2.9.0"
|
|
@@ -207,6 +207,52 @@ describe("ApplicationDetail shell", () => {
|
|
|
207
207
|
expect(html).not.toContain("Application views");
|
|
208
208
|
});
|
|
209
209
|
|
|
210
|
+
// A controlled host's only way to say "no workload in the URL" is null, so
|
|
211
|
+
// without this the application scope a single-workload app must not have
|
|
212
|
+
// becomes the default for every URL-driven host.
|
|
213
|
+
it("opens the workload directly for controlled single-workload apps with no workload in the URL", () => {
|
|
214
|
+
const singleApp: AppRow = {
|
|
215
|
+
...app,
|
|
216
|
+
workloads: [app.workloads[0]],
|
|
217
|
+
};
|
|
218
|
+
const html = renderDetail({
|
|
219
|
+
app: singleApp,
|
|
220
|
+
selectedWorkloadKey: null,
|
|
221
|
+
onSelectWorkload: () => {},
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
expect(html).toContain("Runtime for");
|
|
225
|
+
expect(html).toContain("checkout-api");
|
|
226
|
+
expect(html).not.toContain("Application views");
|
|
227
|
+
expect(html).not.toContain('aria-haspopup="listbox"');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("keeps workload scope for a controlled single-workload app whose URL names a workload it no longer has", () => {
|
|
231
|
+
const singleApp: AppRow = {
|
|
232
|
+
...app,
|
|
233
|
+
workloads: [app.workloads[0]],
|
|
234
|
+
};
|
|
235
|
+
const html = renderDetail({
|
|
236
|
+
app: singleApp,
|
|
237
|
+
selectedWorkloadKey: workloadKey(app.workloads[1]),
|
|
238
|
+
onSelectWorkload: () => {},
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
expect(html).toContain("Runtime for");
|
|
242
|
+
expect(html).toContain("checkout-api");
|
|
243
|
+
expect(html).not.toContain("Application views");
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("keeps application scope for a controlled multi-workload app with no workload in the URL", () => {
|
|
247
|
+
const html = renderDetail({
|
|
248
|
+
selectedWorkloadKey: null,
|
|
249
|
+
onSelectWorkload: () => {},
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
expect(html).toContain("Application views");
|
|
253
|
+
expect(html).not.toContain("Runtime for");
|
|
254
|
+
});
|
|
255
|
+
|
|
210
256
|
it("renders one filtered application chronology with workload and source actions", () => {
|
|
211
257
|
const html = renderDetail({
|
|
212
258
|
selectedView: "history",
|
|
@@ -71,6 +71,7 @@ import {
|
|
|
71
71
|
sourceReportedHealth,
|
|
72
72
|
sourceSyncHealth,
|
|
73
73
|
servingReadiness,
|
|
74
|
+
resolveAppWorkloadSelection,
|
|
74
75
|
worstHealth,
|
|
75
76
|
appGroupLagMessage,
|
|
76
77
|
compareVersions,
|
|
@@ -122,9 +123,10 @@ export interface AppIdentityInstance {
|
|
|
122
123
|
|
|
123
124
|
/** Workload selection is either fully controlled (key + callback, the host
|
|
124
125
|
* wires it to the URL so back/forward works) or fully internal — providing
|
|
125
|
-
* only half silently freezes the selector, so the types forbid it.
|
|
126
|
-
*
|
|
127
|
-
* workload
|
|
126
|
+
* only half silently freezes the selector, so the types forbid it. A key (see
|
|
127
|
+
* `workloadKey`) selects a workload scope; `null` asks for application scope,
|
|
128
|
+
* which a single-workload app does not have — there the component resolves to
|
|
129
|
+
* the sole workload regardless. See `rawSelected`. */
|
|
128
130
|
type SelectionProps =
|
|
129
131
|
| {
|
|
130
132
|
selectedWorkloadKey: string | null;
|
|
@@ -407,12 +409,13 @@ export function ApplicationDetail({
|
|
|
407
409
|
}, [app.key, selectedView]);
|
|
408
410
|
|
|
409
411
|
const [internalSelected, setInternalSelected] = useState<string | null>(null);
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
const rawSelected =
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
412
|
+
const hostSelected =
|
|
413
|
+
selectedWorkloadKey !== undefined ? selectedWorkloadKey : internalSelected;
|
|
414
|
+
const { selected: rawSelected, hostKeyIsStale } = useMemo(
|
|
415
|
+
() =>
|
|
416
|
+
resolveAppWorkloadSelection(workloads.map(workloadKey), hostSelected),
|
|
417
|
+
[workloads, hostSelected],
|
|
418
|
+
);
|
|
416
419
|
const setSelected = useCallback(
|
|
417
420
|
(key: string | null, options?: AppWorkloadSelectionOptions) =>
|
|
418
421
|
onSelectWorkload
|
|
@@ -425,14 +428,10 @@ export function ApplicationDetail({
|
|
|
425
428
|
: undefined;
|
|
426
429
|
const singleWorkloadScope = workloads.length === 1 && !!selectedWorkload;
|
|
427
430
|
useEffect(() => {
|
|
428
|
-
if (
|
|
429
|
-
selectedWorkloadKey !== undefined &&
|
|
430
|
-
selectedWorkloadKey !== null &&
|
|
431
|
-
!selectedWorkload
|
|
432
|
-
) {
|
|
431
|
+
if (selectedWorkloadKey !== undefined && hostKeyIsStale) {
|
|
433
432
|
onSelectWorkload?.(null);
|
|
434
433
|
}
|
|
435
|
-
}, [selectedWorkloadKey,
|
|
434
|
+
}, [selectedWorkloadKey, hostKeyIsStale, onSelectWorkload]);
|
|
436
435
|
useEffect(() => {
|
|
437
436
|
if (selectedWorkloadKey === undefined) setInternalSelected(null);
|
|
438
437
|
}, [app.key, selectedWorkloadKey]);
|
|
@@ -240,6 +240,13 @@ export function IssueRow({
|
|
|
240
240
|
</Tooltip>
|
|
241
241
|
) : null}
|
|
242
242
|
</>
|
|
243
|
+
) : issue.onset_unknown ? (
|
|
244
|
+
<Tooltip content="Radar can confirm this issue is active, but current Kubernetes state does not reveal when it began." delay={200} wrapperClassName="shrink-0">
|
|
245
|
+
<span className="flex items-center gap-1 text-xs text-theme-text-tertiary">
|
|
246
|
+
<Clock className="h-3 w-3" aria-hidden />
|
|
247
|
+
Onset unknown
|
|
248
|
+
</span>
|
|
249
|
+
</Tooltip>
|
|
243
250
|
) : null}
|
|
244
251
|
</div>
|
|
245
252
|
);
|
|
@@ -451,6 +458,8 @@ function Diagnosis({ issue, source }: { issue: Issue; source?: IssueDiagnosisSou
|
|
|
451
458
|
meta.push(timing.meta);
|
|
452
459
|
} else if (issue.first_seen) {
|
|
453
460
|
meta.push(`started ${formatRelativeAgeTime(issue.first_seen)}`);
|
|
461
|
+
} else if (issue.onset_unknown) {
|
|
462
|
+
meta.push('onset unknown');
|
|
454
463
|
}
|
|
455
464
|
if (issue.first_seen) {
|
|
456
465
|
if (issue.last_seen && timing?.kind !== 'creation') meta.push(`last seen ${formatRelativeAgeTime(issue.last_seen)}`);
|
|
@@ -46,6 +46,8 @@ export function diagnosticFactLabel(type: string): string {
|
|
|
46
46
|
return 'Stalled autoscalers';
|
|
47
47
|
case 'secret_not_ready':
|
|
48
48
|
return 'Dependent pods';
|
|
49
|
+
case 'admission_webhook_backend':
|
|
50
|
+
return 'Admission dependency';
|
|
49
51
|
default:
|
|
50
52
|
return type.replace(/_/g, ' ');
|
|
51
53
|
}
|
|
@@ -59,6 +61,7 @@ export function incidentParentLabel(factType?: string, confidence?: string): str
|
|
|
59
61
|
switch (factType) {
|
|
60
62
|
case 'pvc_blast_radius':
|
|
61
63
|
case 'secret_not_ready':
|
|
64
|
+
case 'admission_webhook_backend':
|
|
62
65
|
return 'Caused by';
|
|
63
66
|
case 'apiservice_hpa':
|
|
64
67
|
return 'Likely cause';
|
|
@@ -126,6 +126,17 @@ describe('IssueRow', () => {
|
|
|
126
126
|
expect(html).toContain('Control plane')
|
|
127
127
|
expect(html).toContain(groupBadgeClass('control_plane'))
|
|
128
128
|
})
|
|
129
|
+
|
|
130
|
+
it('shows an explicit unknown-onset state instead of inventing an age', () => {
|
|
131
|
+
const html = renderToString(createElement(IssueRow, {
|
|
132
|
+
issue: mk({ onset_unknown: true }),
|
|
133
|
+
open: false,
|
|
134
|
+
onToggle: () => undefined,
|
|
135
|
+
}))
|
|
136
|
+
|
|
137
|
+
expect(html).toContain('Onset unknown')
|
|
138
|
+
expect(html).not.toContain('0s')
|
|
139
|
+
})
|
|
129
140
|
})
|
|
130
141
|
|
|
131
142
|
describe('issueTiming', () => {
|
|
@@ -71,6 +71,7 @@ const CATEGORY_LABEL: Record<string, string> = {
|
|
|
71
71
|
secret_sync_failed: 'Secret sync failed',
|
|
72
72
|
service_no_endpoints: 'No endpoints',
|
|
73
73
|
ingress_backend_missing: 'Ingress backend missing',
|
|
74
|
+
ingress_class_missing: 'Ingress class missing',
|
|
74
75
|
load_balancer_pending: 'Load balancer pending',
|
|
75
76
|
gateway_not_ready: 'Gateway not ready',
|
|
76
77
|
gateway_route_invalid: 'Gateway route invalid',
|
|
@@ -82,6 +83,8 @@ const CATEGORY_LABEL: Record<string, string> = {
|
|
|
82
83
|
pvc_resize_failed: 'PVC resize failed',
|
|
83
84
|
volume_mount_failed: 'Volume mount failed',
|
|
84
85
|
volume_access_mode_conflict: 'Volume access conflict',
|
|
86
|
+
backup_failed: 'Backup failed',
|
|
87
|
+
backup_target_unavailable: 'Backup target unavailable',
|
|
85
88
|
job_failed: 'Job failed',
|
|
86
89
|
cronjob_failed: 'CronJob failed',
|
|
87
90
|
rollout_stalled: 'Rollout stalled',
|
|
@@ -198,6 +198,9 @@ export interface Issue {
|
|
|
198
198
|
* Capacity / Demand view, so a generic scheduling failure never links. */
|
|
199
199
|
capacity_relevant?: boolean;
|
|
200
200
|
first_seen?: string;
|
|
201
|
+
/** Radar can confirm the issue is active, but current cluster evidence does
|
|
202
|
+
* not establish when the failing state began. */
|
|
203
|
+
onset_unknown?: boolean;
|
|
201
204
|
last_seen?: string;
|
|
202
205
|
/** Affected-resource fan-out, EXCLUDING the subject (the row header).
|
|
203
206
|
* 0/omitted for a single-resource issue; e.g. 50 for one Deployment's
|
|
@@ -107,7 +107,7 @@ interface ResourceTypeButtonProps {
|
|
|
107
107
|
|
|
108
108
|
const ResourceTypeButton = forwardRef<HTMLButtonElement, ResourceTypeButtonProps>(
|
|
109
109
|
function ResourceTypeButton({ resource, count, isSelected, isHighlighted, isForbidden: forbidden, isPinned, onTogglePin, onClick }, ref) {
|
|
110
|
-
const Icon = getResourceIcon(resource.kind)
|
|
110
|
+
const Icon = getResourceIcon(resource.kind, resource.group)
|
|
111
111
|
return (
|
|
112
112
|
<button
|
|
113
113
|
ref={ref}
|