@skyhook-io/k8s-ui 1.10.5 → 1.12.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 (80) hide show
  1. package/package.json +1 -1
  2. package/src/components/gitops/GitOpsStatusBadge.tsx +40 -0
  3. package/src/components/gitops/GitOpsTableView.tsx +22 -2
  4. package/src/components/gitops/ReconcilingIndicator.test.tsx +31 -0
  5. package/src/components/gitops/detail-helpers.ts +4 -1
  6. package/src/components/issues/IssuesView.tsx +9 -0
  7. package/src/components/issues/diagnostic.ts +3 -0
  8. package/src/components/issues/issues.test.ts +11 -0
  9. package/src/components/issues/severity.ts +3 -0
  10. package/src/components/issues/types.ts +3 -0
  11. package/src/components/resources/ResourcesSidebar.tsx +1 -1
  12. package/src/components/resources/ResourcesView.tsx +261 -30
  13. package/src/components/resources/index.ts +2 -0
  14. package/src/components/resources/kyverno-cell-gating.test.ts +93 -0
  15. package/src/components/resources/kyverno-modern-posture.test.ts +290 -0
  16. package/src/components/resources/renderers/AlertRenderer.tsx +4 -1
  17. package/src/components/resources/renderers/CNPGClusterRenderer.test.tsx +85 -0
  18. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +133 -9
  19. package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +19 -4
  20. package/src/components/resources/renderers/CompositeRenderer.test.tsx +81 -0
  21. package/src/components/resources/renderers/CompositeRenderer.tsx +67 -7
  22. package/src/components/resources/renderers/EventRenderer.test.tsx +23 -0
  23. package/src/components/resources/renderers/EventRenderer.tsx +10 -18
  24. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +4 -1
  25. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +4 -1
  26. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +4 -1
  27. package/src/components/resources/renderers/KustomizationRenderer.tsx +4 -1
  28. package/src/components/resources/renderers/KyvernoCELPolicyRenderers.tsx +317 -0
  29. package/src/components/resources/renderers/KyvernoExceptionRenderers.tsx +264 -0
  30. package/src/components/resources/renderers/KyvernoPolicyShared.tsx +212 -0
  31. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +4 -1
  32. package/src/components/resources/renderers/PodRenderer.tsx +13 -0
  33. package/src/components/resources/renderers/PolicySection.test.tsx +102 -0
  34. package/src/components/resources/renderers/PolicySection.tsx +170 -0
  35. package/src/components/resources/renderers/VeleroBSLRenderer.tsx +2 -4
  36. package/src/components/resources/renderers/VeleroBackupRenderer.tsx +32 -9
  37. package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +30 -9
  38. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +12 -6
  39. package/src/components/resources/renderers/WorkloadRenderer.tsx +15 -1
  40. package/src/components/resources/renderers/badge-no-handrolled.test.tsx +1 -1
  41. package/src/components/resources/renderers/cnpg-cells.tsx +37 -2
  42. package/src/components/resources/renderers/index.ts +5 -0
  43. package/src/components/resources/renderers/kyverno-cells.tsx +9 -0
  44. package/src/components/resources/renderers/kyverno-modern-cells.tsx +152 -0
  45. package/src/components/resources/renderers/policyreport-subject.test.tsx +27 -0
  46. package/src/components/resources/renderers/velero-cells.tsx +128 -9
  47. package/src/components/resources/renderers/velero-phase-recovery.test.tsx +79 -0
  48. package/src/components/resources/resource-utils-cnpg.golden.test.ts +112 -0
  49. package/src/components/resources/resource-utils-cnpg.test.ts +590 -1
  50. package/src/components/resources/resource-utils-cnpg.ts +433 -56
  51. package/src/components/resources/resource-utils-crossplane.test.ts +68 -0
  52. package/src/components/resources/resource-utils-kyverno-exceptions.ts +182 -0
  53. package/src/components/resources/resource-utils-kyverno-modern.ts +588 -0
  54. package/src/components/resources/resource-utils-velero.test.ts +237 -0
  55. package/src/components/resources/resource-utils-velero.ts +214 -22
  56. package/src/components/resources/resource-utils.ts +70 -3
  57. package/src/components/shared/ResourceActionsBar.tsx +3 -1
  58. package/src/components/shared/ResourceRendererDispatch.test.tsx +119 -1
  59. package/src/components/shared/ResourceRendererDispatch.tsx +116 -22
  60. package/src/components/topology/K8sResourceNode.tsx +6 -0
  61. package/src/components/trace/ReachabilityGraph.tsx +20 -6
  62. package/src/components/trace/ReachabilityView.tsx +253 -52
  63. package/src/components/trace/problemRows.test.ts +107 -0
  64. package/src/components/trace/reachGraphModel.test.ts +16 -0
  65. package/src/components/trace/reachInspector.test.ts +233 -10
  66. package/src/components/trace/reachInspector.ts +166 -36
  67. package/src/components/trace/reachMarks.test.ts +32 -1
  68. package/src/components/trace/reachMarks.ts +44 -0
  69. package/src/components/trace/reachOrigins.ts +12 -0
  70. package/src/components/trace/types.ts +25 -0
  71. package/src/components/ui/Badge.tsx +36 -0
  72. package/src/components/ui/ForceDeleteConfirmDialog.tsx +12 -1
  73. package/src/theme/components.css +26 -0
  74. package/src/types/gitops-flux-status.test.ts +163 -0
  75. package/src/types/gitops.ts +80 -24
  76. package/src/types/index.ts +1 -0
  77. package/src/types/policy.ts +42 -0
  78. package/src/utils/api-resources.ts +2 -0
  79. package/src/utils/resource-icons.test.ts +33 -0
  80. 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.5",
3
+ "version": "1.12.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/skyhook-io/radar",
@@ -2,6 +2,8 @@ import { clsx } from 'clsx'
2
2
  import { CheckCircle2, AlertCircle, Loader2, Pause, HelpCircle, XCircle } from 'lucide-react'
3
3
  import type { GitOpsStatus, SyncStatus, GitOpsHealthStatus } from '../../types/gitops'
4
4
  import { SEVERITY_BADGE_BORDERED, SEVERITY_BADGE } from '../../utils/badge-colors'
5
+ import { formatCompactAge } from '../../utils/format'
6
+ import { Tooltip } from '../ui/Tooltip'
5
7
 
6
8
  interface GitOpsStatusBadgeProps {
7
9
  status: GitOpsStatus
@@ -176,3 +178,41 @@ export function HealthStatusBadge({ health }: { health: GitOpsHealthStatus }) {
176
178
  </span>
177
179
  )
178
180
  }
181
+
182
+
183
+ /**
184
+ * ReconcilingIndicator rides alongside the sync badge rather than replacing it,
185
+ * the same primary-plus-secondary shape the Velero schedule cell uses for a
186
+ * paused-and-rejected schedule.
187
+ *
188
+ * A pass being in flight says nothing about whether the declared state is
189
+ * applied, so it must not touch the sync word — but a pass that never finishes
190
+ * is the one fault nothing else in the product notices, and on a healthy object
191
+ * a real pass completes in milliseconds. In practice this only becomes visible
192
+ * when a controller is wedged, which is exactly when it should be.
193
+ */
194
+ export function ReconcilingIndicator({
195
+ reconciling,
196
+ since,
197
+ sync,
198
+ }: {
199
+ reconciling?: boolean
200
+ since?: string
201
+ /** The sync word shown beside this. Omitted when there is no badge alongside. */
202
+ sync?: SyncStatus
203
+ }) {
204
+ // Suppressed when the sync badge already says Reconciling — it carries the
205
+ // same fact and spins for it. Showing both puts two spinners on one row for
206
+ // one thing. The secondary exists for the case the primary CANNOT express:
207
+ // an applied object whose reconcile pass never finished.
208
+ if (!reconciling || sync === 'Reconciling') return null
209
+ const age = formatCompactAge(since)
210
+ const tip = age
211
+ ? `A reconcile pass has been in flight for ${age}. On a healthy resource a pass finishes in milliseconds, so a long-running one usually means the controller is stuck.`
212
+ : 'A reconcile pass is in flight.'
213
+ return (
214
+ <Tooltip content={tip}>
215
+ <Loader2 className="w-3 h-3 shrink-0 animate-spin text-theme-text-tertiary" aria-label="Reconcile pass in flight" />
216
+ </Tooltip>
217
+ )
218
+ }
@@ -24,7 +24,7 @@ import {
24
24
  Zap,
25
25
  } from 'lucide-react'
26
26
 
27
- import { HealthStatusBadge, SyncStatusBadge } from './GitOpsStatusBadge'
27
+ import { HealthStatusBadge, ReconcilingIndicator, SyncStatusBadge } from './GitOpsStatusBadge'
28
28
  import { Tooltip } from '../ui/Tooltip'
29
29
  import { Input } from '../ui/Input'
30
30
  import { PageHeader } from '../ui/PageHeader'
@@ -150,6 +150,14 @@ export interface GitOpsRow {
150
150
  sync: string
151
151
  health: string
152
152
  suspended: boolean
153
+ /**
154
+ * A reconcile pass is executing. Deliberately separate from sync: a pass
155
+ * being in flight says nothing about whether the declared state is applied,
156
+ * but a pass that never finishes is the one fault nothing else in the
157
+ * product notices.
158
+ */
159
+ reconciling?: boolean
160
+ reconcilingSince?: string
153
161
  repository: string
154
162
  targetRevision: string
155
163
  path: string
@@ -1300,7 +1308,12 @@ function GitOpsTable({
1300
1308
  <TableCell>
1301
1309
  {row.terminating
1302
1310
  ? <span className="text-[11px] text-theme-text-tertiary">—</span>
1303
- : <SyncStatusBadge sync={row.sync as any} suspended={row.suspended} />}
1311
+ : (
1312
+ <span className="inline-flex items-center gap-1 min-w-0">
1313
+ <SyncStatusBadge sync={row.sync as any} suspended={row.suspended} />
1314
+ <ReconcilingIndicator reconciling={row.reconciling} since={row.reconcilingSince} sync={row.sync as any} />
1315
+ </span>
1316
+ )}
1304
1317
  </TableCell>
1305
1318
  <TableCell>
1306
1319
  {row.terminating
@@ -1529,6 +1542,7 @@ function GitOpsTile({
1529
1542
  ) : (
1530
1543
  <>
1531
1544
  <SyncStatusBadge sync={row.sync as any} suspended={row.suspended} />
1545
+ <ReconcilingIndicator reconciling={row.reconciling} since={row.reconcilingSince} sync={row.sync as any} />
1532
1546
  <HealthStatusBadge health={row.health as any} />
1533
1547
  </>
1534
1548
  )}
@@ -1896,6 +1910,8 @@ export function normalizeArgoApplication(resource: any): GitOpsRow {
1896
1910
  labels: (resource.metadata?.labels ?? {}) as Record<string, string>,
1897
1911
  sync: status?.sync ?? 'Unknown',
1898
1912
  health: status?.health ?? 'Unknown',
1913
+ reconciling: status?.reconciling,
1914
+ reconcilingSince: status?.reconcilingSince,
1899
1915
  suspended: (status?.suspended ?? false) || isArgoSuspendedByRadar(resource),
1900
1916
  repository: resource.spec?.source?.repoURL ?? '',
1901
1917
  targetRevision: resource.spec?.source?.targetRevision ?? '',
@@ -1932,6 +1948,8 @@ export function normalizeFluxKustomization(resource: any, fluxSourceUrls?: Map<s
1932
1948
  labels: (resource.metadata?.labels ?? {}) as Record<string, string>,
1933
1949
  sync: status?.sync ?? 'Unknown',
1934
1950
  health: status?.health ?? 'Unknown',
1951
+ reconciling: status?.reconciling,
1952
+ reconcilingSince: status?.reconcilingSince,
1935
1953
  suspended: status?.suspended ?? resource.spec?.suspend === true,
1936
1954
  repository,
1937
1955
  targetRevision: resource.status?.lastAppliedRevision ?? resource.status?.lastAttemptedRevision ?? '',
@@ -1969,6 +1987,8 @@ export function normalizeFluxHelmRelease(resource: any, fluxSourceUrls?: Map<str
1969
1987
  labels: (resource.metadata?.labels ?? {}) as Record<string, string>,
1970
1988
  sync: status?.sync ?? 'Unknown',
1971
1989
  health: status?.health ?? 'Unknown',
1990
+ reconciling: status?.reconciling,
1991
+ reconcilingSince: status?.reconcilingSince,
1972
1992
  suspended: status?.suspended ?? resource.spec?.suspend === true,
1973
1993
  repository,
1974
1994
  targetRevision: chartSpec.version ?? resource.status?.lastAttemptedRevision ?? '',
@@ -0,0 +1,31 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { renderToString } from 'react-dom/server'
3
+ import { ReconcilingIndicator } from './GitOpsStatusBadge'
4
+
5
+ const LABEL = 'Reconcile pass in flight'
6
+
7
+ // A reconcile pass in flight is worth showing only where the sync word cannot
8
+ // already say it. Radar's own precedent for a secondary indicator (the Velero
9
+ // schedule cell's paused-and-rejected case) suppresses the icon when the badge
10
+ // carries the same fact, and without that rule one row shows two spinners.
11
+ describe('ReconcilingIndicator', () => {
12
+ it('shows on an applied object whose pass never finished', () => {
13
+ // The case the sync word cannot express, and the reason this exists: a
14
+ // wedged controller behind a correct "Synced".
15
+ const html = renderToString(
16
+ <ReconcilingIndicator reconciling since="2026-08-11T06:05:00Z" sync="Synced" />,
17
+ )
18
+ expect(html).toContain(LABEL)
19
+ })
20
+
21
+ it('is suppressed when the sync badge already reports Reconciling', () => {
22
+ const html = renderToString(
23
+ <ReconcilingIndicator reconciling since="2026-08-11T06:05:00Z" sync="Reconciling" />,
24
+ )
25
+ expect(html).toBe('')
26
+ })
27
+
28
+ it('is absent when nothing is in flight', () => {
29
+ expect(renderToString(<ReconcilingIndicator reconciling={false} sync="Synced" />)).toBe('')
30
+ })
31
+ })
@@ -100,7 +100,10 @@ export function getGitOpsResourceStatus(kind: string, resource: any): GitOpsStat
100
100
  return argoStatusToGitOpsStatus(resource?.status ?? {})
101
101
  }
102
102
  const conditions = (resource?.status?.conditions ?? []) as FluxCondition[]
103
- return fluxConditionsToGitOpsStatus(conditions, resource?.spec?.suspend === true)
103
+ return fluxConditionsToGitOpsStatus(conditions, resource?.spec?.suspend === true, {
104
+ generation: resource?.metadata?.generation,
105
+ observedGeneration: resource?.status?.observedGeneration,
106
+ })
104
107
  }
105
108
 
106
109
  // getGitOpsTool routes a kind+group to 'argo' or 'flux'. Same shape used
@@ -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}