@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.
Files changed (99) hide show
  1. package/package.json +5 -5
  2. package/src/components/applications/ApplicationDetail.test.tsx +46 -0
  3. package/src/components/applications/ApplicationDetail.tsx +14 -15
  4. package/src/components/issues/IssuesView.tsx +9 -0
  5. package/src/components/issues/diagnostic.ts +3 -0
  6. package/src/components/issues/issues.test.ts +11 -0
  7. package/src/components/issues/severity.ts +3 -0
  8. package/src/components/issues/types.ts +3 -0
  9. package/src/components/resources/ResourcesSidebar.tsx +1 -1
  10. package/src/components/resources/ResourcesView.tsx +259 -30
  11. package/src/components/resources/index.ts +2 -0
  12. package/src/components/resources/kyverno-cell-gating.test.ts +93 -0
  13. package/src/components/resources/kyverno-modern-posture.test.ts +290 -0
  14. package/src/components/resources/renderers/CNPGClusterRenderer.test.tsx +85 -0
  15. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +109 -9
  16. package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +8 -4
  17. package/src/components/resources/renderers/EventRenderer.test.tsx +23 -0
  18. package/src/components/resources/renderers/EventRenderer.tsx +10 -18
  19. package/src/components/resources/renderers/KyvernoCELPolicyRenderers.tsx +317 -0
  20. package/src/components/resources/renderers/KyvernoExceptionRenderers.tsx +264 -0
  21. package/src/components/resources/renderers/KyvernoPolicyShared.tsx +212 -0
  22. package/src/components/resources/renderers/VeleroBSLRenderer.tsx +2 -4
  23. package/src/components/resources/renderers/VeleroBackupRenderer.tsx +32 -9
  24. package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +30 -9
  25. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +12 -6
  26. package/src/components/resources/renderers/badge-no-handrolled.test.tsx +1 -1
  27. package/src/components/resources/renderers/cnpg-cells.tsx +37 -2
  28. package/src/components/resources/renderers/index.ts +4 -0
  29. package/src/components/resources/renderers/kyverno-modern-cells.tsx +152 -0
  30. package/src/components/resources/renderers/velero-cells.tsx +128 -9
  31. package/src/components/resources/renderers/velero-phase-recovery.test.tsx +79 -0
  32. package/src/components/resources/resource-utils-cnpg.golden.test.ts +112 -0
  33. package/src/components/resources/resource-utils-cnpg.test.ts +527 -1
  34. package/src/components/resources/resource-utils-cnpg.ts +373 -56
  35. package/src/components/resources/resource-utils-kyverno-exceptions.ts +182 -0
  36. package/src/components/resources/resource-utils-kyverno-modern.ts +588 -0
  37. package/src/components/resources/resource-utils-velero.test.ts +237 -0
  38. package/src/components/resources/resource-utils-velero.ts +214 -22
  39. package/src/components/resources/resource-utils.ts +70 -3
  40. package/src/components/shared/ResourceActionsBar.tsx +3 -1
  41. package/src/components/shared/ResourceRendererDispatch.test.tsx +119 -1
  42. package/src/components/shared/ResourceRendererDispatch.tsx +116 -22
  43. package/src/components/topology/K8sResourceNode.tsx +11 -0
  44. package/src/components/topology/TopologyGraph.tsx +85 -5
  45. package/src/components/trace/ReachabilityGraph.tsx +517 -0
  46. package/src/components/trace/ReachabilityView.tsx +1173 -0
  47. package/src/components/trace/TracePanel.test.ts +172 -0
  48. package/src/components/trace/TracePanel.tsx +558 -0
  49. package/src/components/trace/TraceSummary.test.ts +213 -0
  50. package/src/components/trace/TraceSummary.tsx +168 -0
  51. package/src/components/trace/inClusterSummary.test.ts +98 -0
  52. package/src/components/trace/inClusterSummary.ts +81 -0
  53. package/src/components/trace/index.ts +19 -0
  54. package/src/components/trace/podReach.test.ts +71 -0
  55. package/src/components/trace/podReach.ts +37 -0
  56. package/src/components/trace/probe-display.test.ts +107 -0
  57. package/src/components/trace/probe-display.ts +37 -0
  58. package/src/components/trace/problemRows.test.ts +107 -0
  59. package/src/components/trace/reachFixtures.ts +261 -0
  60. package/src/components/trace/reachGraphModel.test.ts +1423 -0
  61. package/src/components/trace/reachGraphModel.ts +1521 -0
  62. package/src/components/trace/reachInspector.test.ts +892 -0
  63. package/src/components/trace/reachInspector.ts +797 -0
  64. package/src/components/trace/reachMarks.test.ts +698 -0
  65. package/src/components/trace/reachMarks.ts +623 -0
  66. package/src/components/trace/reachOrigins.test.ts +392 -0
  67. package/src/components/trace/reachOrigins.ts +449 -0
  68. package/src/components/trace/reachVerdict.test.ts +537 -0
  69. package/src/components/trace/reachVerdict.ts +601 -0
  70. package/src/components/trace/traceFingerprint.test.ts +214 -0
  71. package/src/components/trace/traceFingerprint.ts +122 -0
  72. package/src/components/trace/traceToSubgraph.test.ts +623 -0
  73. package/src/components/trace/traceToSubgraph.ts +463 -0
  74. package/src/components/trace/types.ts +414 -0
  75. package/src/components/ui/Badge.tsx +36 -0
  76. package/src/components/ui/ForceDeleteConfirmDialog.tsx +12 -1
  77. package/src/components/ui/InClusterConsentDialog.test.tsx +83 -0
  78. package/src/components/ui/InClusterConsentDialog.tsx +141 -0
  79. package/src/components/ui/index.ts +1 -0
  80. package/src/components/workload/WorkloadView.tsx +58 -5
  81. package/src/components/workload/index.ts +1 -1
  82. package/src/components/workload/reachabilityTab.test.ts +47 -0
  83. package/src/index.ts +5 -0
  84. package/src/theme/components.css +64 -0
  85. package/src/theme/variables.css +5 -0
  86. package/src/types/core.ts +8 -0
  87. package/src/utils/api-resources.ts +2 -0
  88. package/src/utils/application-history.test.ts +1 -1
  89. package/src/utils/applications.test.ts +63 -1
  90. package/src/utils/applications.ts +27 -0
  91. package/src/utils/inClusterConsent.test.ts +109 -0
  92. package/src/utils/inClusterConsent.ts +72 -0
  93. package/src/utils/index.ts +1 -0
  94. package/src/utils/navigation.test.ts +61 -0
  95. package/src/utils/navigation.ts +11 -9
  96. package/src/utils/pluralize.test.ts +80 -1
  97. package/src/utils/pluralize.ts +34 -0
  98. package/src/utils/resource-icons.test.ts +33 -0
  99. 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.10.4",
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.17",
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.24.0",
101
+ "lucide-react": "^1.28.0",
102
102
  "monaco-editor": "0.55.1",
103
- "react": "^19.2.7",
104
- "react-dom": "^19.2.7",
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. `null` means
126
- * the application itself is selected; a key (see `workloadKey`) selects a
127
- * workload scope. */
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 implicitSingleWorkloadKey =
411
- workloads.length === 1 ? workloadKey(workloads[0]) : null;
412
- const rawSelected =
413
- selectedWorkloadKey !== undefined
414
- ? selectedWorkloadKey
415
- : (internalSelected ?? implicitSingleWorkloadKey);
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, selectedWorkload, onSelectWorkload]);
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}