@skyhook-io/radar-app 0.2.2 → 0.3.1

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 (177) hide show
  1. package/README.md +7 -1
  2. package/package.json +33 -25
  3. package/src/App.tsx +1449 -382
  4. package/src/RadarApp.tsx +132 -19
  5. package/src/api/apiResources.ts +1 -1
  6. package/src/api/client.argoResourceSync.test.ts +69 -0
  7. package/src/api/client.delta.test.ts +89 -0
  8. package/src/api/client.deltaSync.test.ts +216 -0
  9. package/src/api/client.metrics.test.ts +106 -0
  10. package/src/api/client.rightsizing.test.ts +32 -0
  11. package/src/api/client.ts +2730 -271
  12. package/src/api/client.yaml.test.ts +45 -0
  13. package/src/api/diagnose.ts +289 -0
  14. package/src/api/quotas.ts +16 -0
  15. package/src/api/rbac.ts +57 -0
  16. package/src/api/timelineSource.test.ts +217 -0
  17. package/src/api/timelineSource.ts +582 -0
  18. package/src/components/ConnectionErrorView.tsx +186 -70
  19. package/src/components/ContextSwitcher.tsx +63 -18
  20. package/src/components/DebugOverlay.tsx +5 -3
  21. package/src/components/NamespaceSwitcher.tsx +41 -0
  22. package/src/components/UserMenu.tsx +69 -21
  23. package/src/components/applications/ApplicationsView.tsx +936 -0
  24. package/src/components/audit/AuditSettingsDialog.tsx +79 -17
  25. package/src/components/audit/AuditView.tsx +65 -62
  26. package/src/components/compare/CompareViewRoute.tsx +124 -0
  27. package/src/components/compare/useCompareCandidates.ts +27 -0
  28. package/src/components/compare/useCompareLauncher.tsx +79 -0
  29. package/src/components/cost/ApplicationCostTab.test.ts +204 -0
  30. package/src/components/cost/ApplicationCostTab.tsx +571 -0
  31. package/src/components/cost/CostTrendChart.tsx +106 -75
  32. package/src/components/cost/CostView.test.ts +12 -0
  33. package/src/components/cost/CostView.tsx +507 -223
  34. package/src/components/cost/CostViewTabs.test.tsx +21 -0
  35. package/src/components/cost/CostViewTabs.tsx +40 -0
  36. package/src/components/cost/CurrentAllocationUse.test.ts +21 -0
  37. package/src/components/cost/CurrentAllocationUse.tsx +126 -0
  38. package/src/components/cost/WorkloadCostTab.test.ts +153 -0
  39. package/src/components/cost/WorkloadCostTab.tsx +372 -0
  40. package/src/components/cost/cloud-console.test.ts +39 -0
  41. package/src/components/cost/cloud-console.ts +81 -0
  42. package/src/components/cost/errors.ts +8 -0
  43. package/src/components/cost/format.test.ts +27 -0
  44. package/src/components/cost/format.ts +46 -0
  45. package/src/components/cost/kinds.ts +5 -0
  46. package/src/components/curl/ServiceCurlButton.tsx +445 -0
  47. package/src/components/diagnose/AISettings.tsx +147 -0
  48. package/src/components/diagnose/DiagnoseContext.tsx +495 -0
  49. package/src/components/diagnose/DiagnoseSurface.tsx +394 -0
  50. package/src/components/diagnose/Home.tsx +163 -0
  51. package/src/components/diagnose/InvestigationView.tsx +622 -0
  52. package/src/components/diagnose/LocalDiagnoseAction.tsx +162 -0
  53. package/src/components/diagnose/launch.ts +65 -0
  54. package/src/components/diagnose/parts.tsx +1756 -0
  55. package/src/components/dock/BottomDock.tsx +2 -3
  56. package/src/components/dock/DockContext.tsx +1 -0
  57. package/src/components/dock/TerminalTab.tsx +1 -1
  58. package/src/components/dock/WorkloadLogsTab.tsx +21 -5
  59. package/src/components/dock/index.ts +1 -1
  60. package/src/components/execution/BatchExecutionView.test.ts +170 -0
  61. package/src/components/execution/BatchExecutionView.tsx +1329 -0
  62. package/src/components/execution/batch-run-actions.test.ts +48 -0
  63. package/src/components/execution/batch-run-actions.ts +24 -0
  64. package/src/components/execution/batch-timeline.test.ts +57 -0
  65. package/src/components/execution/batch-timeline.ts +46 -0
  66. package/src/components/execution/execution-definition.test.ts +208 -0
  67. package/src/components/execution/execution-definition.ts +245 -0
  68. package/src/components/gitops/ArgoResourceDiffLoader.tsx +23 -0
  69. package/src/components/gitops/GitOpsView.tsx +1042 -0
  70. package/src/components/gitops/RevisionMetaChip.tsx +63 -0
  71. package/src/components/helm/ChartBrowser.tsx +87 -31
  72. package/src/components/helm/HelmCompareRoute.tsx +1341 -0
  73. package/src/components/helm/HelmReleaseDrawer.test.ts +17 -0
  74. package/src/components/helm/HelmReleaseDrawer.tsx +1073 -102
  75. package/src/components/helm/HelmView.tsx +237 -96
  76. package/src/components/helm/InstallWizard.tsx +94 -38
  77. package/src/components/helm/ManifestDiffViewer.tsx +8 -27
  78. package/src/components/helm/OwnedResources.tsx +34 -59
  79. package/src/components/helm/RevisionHistory.tsx +52 -3
  80. package/src/components/helm/RoleGatedPanel.tsx +3 -3
  81. package/src/components/helm/TrackChartSourceDialog.tsx +185 -0
  82. package/src/components/helm/ValuesDiffPreview.tsx +17 -7
  83. package/src/components/helm/ValuesViewer.tsx +49 -53
  84. package/src/components/helm/helm-utils.ts +4 -0
  85. package/src/components/home/ActivitySummary.tsx +4 -1
  86. package/src/components/home/ClusterHealthCard.tsx +56 -42
  87. package/src/components/home/CostCard.tsx +21 -36
  88. package/src/components/home/GitOpsControllersCard.tsx +110 -0
  89. package/src/components/home/HelmSummary.tsx +3 -1
  90. package/src/components/home/HomeView.tsx +339 -105
  91. package/src/components/home/MCPSetupDialog.tsx +29 -87
  92. package/src/components/home/TrafficSummary.tsx +2 -2
  93. package/src/components/home/mcpToolCatalog.ts +333 -0
  94. package/src/components/issues/IssuesPane.tsx +151 -0
  95. package/src/components/logs/LogsViewer.tsx +4 -1
  96. package/src/components/logs/ScheduledWorkloadLogsViewer.tsx +135 -0
  97. package/src/components/logs/WorkloadLogsViewer.tsx +4 -1
  98. package/src/components/nav/PrimaryNavRail.tsx +285 -0
  99. package/src/components/portforward/PortForwardButton.tsx +118 -47
  100. package/src/components/portforward/PortForwardManager.tsx +253 -131
  101. package/src/components/resource/HPACharts.tsx +237 -0
  102. package/src/components/resource/PVCUsageBar.tsx +59 -0
  103. package/src/components/resource/PrometheusCharts.tsx +160 -584
  104. package/src/components/resource/PrometheusChartsGrid.tsx +270 -0
  105. package/src/components/resource/RestartChart.tsx +133 -0
  106. package/src/components/resource/RightsizingStrip.test.ts +109 -0
  107. package/src/components/resource/RightsizingStrip.tsx +363 -0
  108. package/src/components/resource-drawer/ResourceDrawer.tsx +3 -1
  109. package/src/components/resources/CompositeRenderer.tsx +101 -0
  110. package/src/components/resources/ImageFilesystemModal.tsx +19 -12
  111. package/src/components/resources/PodFilesystemModal.tsx +6 -5
  112. package/src/components/resources/ResourceDetailDrawer.tsx +13 -3
  113. package/src/components/resources/ResourcesView.tsx +194 -17
  114. package/src/components/resources/renderers/CronWorkflowRenderer.tsx +1 -0
  115. package/src/components/resources/renderers/HPARenderer.tsx +20 -1
  116. package/src/components/resources/renderers/NamespaceRenderer.tsx +31 -0
  117. package/src/components/resources/renderers/NodeRenderer.tsx +10 -4
  118. package/src/components/resources/renderers/PVCRenderer.tsx +19 -1
  119. package/src/components/resources/renderers/PodRenderer.tsx +30 -6
  120. package/src/components/resources/renderers/RoleBindingRenderer.tsx +45 -1
  121. package/src/components/resources/renderers/RoleRenderer.tsx +27 -1
  122. package/src/components/resources/renderers/ServiceAccountRenderer.tsx +28 -1
  123. package/src/components/resources/renderers/ServiceRenderer.tsx +81 -8
  124. package/src/components/resources/renderers/WorkloadRenderer.tsx +51 -4
  125. package/src/components/resources/renderers/index.ts +2 -0
  126. package/src/components/resources/resource-utils.ts +2 -1
  127. package/src/components/rightsizing/RightsizingScanView.tsx +938 -0
  128. package/src/components/rightsizing/copy.test.ts +56 -0
  129. package/src/components/rightsizing/model.test.ts +227 -0
  130. package/src/components/rightsizing/model.ts +158 -0
  131. package/src/components/rightsizing/presentation.test.ts +104 -0
  132. package/src/components/rightsizing/presentation.ts +94 -0
  133. package/src/components/settings/MyPermissionsDialog.tsx +241 -0
  134. package/src/components/settings/SettingsDialog.tsx +1505 -165
  135. package/src/components/shared/CreateResourceDialog.tsx +9 -2
  136. package/src/components/shared/LargeClusterNamespacePicker.tsx +3 -3
  137. package/src/components/timeline/LocalTimelineScrubber.tsx +212 -0
  138. package/src/components/timeline/RetainedTimelineScrubber.tsx +311 -0
  139. package/src/components/timeline/TimelineList.tsx +86 -13
  140. package/src/components/timeline/TimelineSwimlanes.tsx +9 -1299
  141. package/src/components/timeline/TimelineView.tsx +873 -24
  142. package/src/components/timeline/TimelineView.urlparams.test.ts +335 -0
  143. package/src/components/traffic/TrafficFilterSidebar.tsx +10 -45
  144. package/src/components/traffic/TrafficFlowList.tsx +29 -15
  145. package/src/components/traffic/TrafficGraph.tsx +42 -24
  146. package/src/components/traffic/TrafficView.tsx +32 -19
  147. package/src/components/ui/CommandPalette.tsx +8 -215
  148. package/src/components/ui/DiagnosticsOverlay.tsx +219 -9
  149. package/src/components/ui/Markdown.tsx +3 -3
  150. package/src/components/ui/Omnibar.tsx +602 -0
  151. package/src/components/ui/RadarOmnibar.tsx +52 -0
  152. package/src/components/ui/SearchSyntaxHelp.tsx +89 -0
  153. package/src/components/ui/ShortcutHelpOverlay.tsx +3 -2
  154. package/src/components/ui/UpdateNotification.tsx +48 -36
  155. package/src/components/ui/command-items.ts +178 -0
  156. package/src/components/workload/WorkloadView.tsx +1342 -158
  157. package/src/context/ConnectionContext.tsx +146 -21
  158. package/src/context/DiagnoseCustomization.tsx +93 -0
  159. package/src/context/NavCustomization.tsx +75 -0
  160. package/src/context/TimelineSource.tsx +50 -0
  161. package/src/contexts/CapabilitiesContext.tsx +32 -8
  162. package/src/filter/FilterLocationBridge.tsx +30 -0
  163. package/src/hooks/useClusterLoadState.ts +73 -0
  164. package/src/hooks/useDocumentTitle.ts +25 -0
  165. package/src/hooks/useEventSource.ts +6 -0
  166. package/src/hooks/useKeyboardShortcuts.tsx +1 -0
  167. package/src/hooks/useMediaQuery.ts +21 -0
  168. package/src/hooks/useNavRailPinned.ts +46 -0
  169. package/src/hooks/useRecentResources.ts +49 -0
  170. package/src/index.css +162 -1
  171. package/src/index.ts +73 -1
  172. package/src/main.tsx +7 -5
  173. package/src/types/clusterLoadState.ts +33 -0
  174. package/src/types.ts +2 -0
  175. package/src/utils/auditBadges.ts +53 -0
  176. package/src/utils/navigation.ts +64 -1
  177. package/src/components/ui/NamespaceSelector.tsx +0 -436
@@ -1,7 +1,11 @@
1
- import { useState, useEffect } from 'react'
2
- import { X, Plus, Trash2 } from 'lucide-react'
3
- import { useAuditSettings, useUpdateAuditSettings, useAudit } from '../../api/client'
1
+ import { useState, useEffect, useMemo } from 'react'
2
+ import { X, Plus, Trash2, Lock } from 'lucide-react'
3
+ import { clsx } from 'clsx'
4
+ import { useAuditSettings, useUpdateAuditSettings, useAudit, useCloudRole } from '../../api/client'
4
5
  import type { CheckMeta } from '@skyhook-io/k8s-ui'
6
+ import { validateRFC1123Label, type ValidationResult } from '@skyhook-io/k8s-ui/utils/validators'
7
+ import { Tooltip } from '../ui/Tooltip'
8
+ import { Input } from '@skyhook-io/k8s-ui'
5
9
 
6
10
  interface AuditSettingsDialogProps {
7
11
  namespaces: string[]
@@ -12,6 +16,11 @@ export function AuditSettingsDialog({ namespaces, onClose }: AuditSettingsDialog
12
16
  const { data: settings } = useAuditSettings()
13
17
  const { data: auditData } = useAudit(namespaces)
14
18
  const updateSettings = useUpdateAuditSettings()
19
+ // Audit policy is cluster-shared, so writes are owner-gated (enforced
20
+ // server-side too). Non-owners get a read-only view. Non-Cloud callers
21
+ // have no role and pass.
22
+ const { canAtLeast } = useCloudRole()
23
+ const canEdit = canAtLeast('owner')
15
24
  const [ignoredNs, setIgnoredNs] = useState<string[]>([])
16
25
  const [disabledChecks, setDisabledChecks] = useState<string[]>([])
17
26
  const [newNs, setNewNs] = useState('')
@@ -28,12 +37,23 @@ export function AuditSettingsDialog({ namespaces, onClose }: AuditSettingsDialog
28
37
  ? Object.values(auditData.checks).sort((a, b) => a.title.localeCompare(b.title))
29
38
  : []
30
39
 
40
+ // Validate the staged namespace input against RFC 1123. Saving a bogus
41
+ // entry would silently never match anything in the audit pipeline,
42
+ // leaving the user thinking the ignore filter doesn't work.
43
+ const newNsTrimmed = newNs.trim()
44
+ const newNsValidation = useMemo<ValidationResult>(
45
+ () => (newNsTrimmed === '' ? { valid: true } : validateRFC1123Label(newNsTrimmed)),
46
+ [newNsTrimmed],
47
+ )
48
+ const newNsError = newNsValidation.valid ? null : newNsValidation.error
49
+ const newNsDuplicate = newNsTrimmed !== '' && ignoredNs.includes(newNsTrimmed)
50
+ const canAddNamespace =
51
+ newNsTrimmed !== '' && newNsValidation.valid && !newNsDuplicate
52
+
31
53
  const addNamespace = () => {
32
- const ns = newNs.trim()
33
- if (ns && !ignoredNs.includes(ns)) {
34
- setIgnoredNs([...ignoredNs, ns])
35
- setNewNs('')
36
- }
54
+ if (!canAddNamespace) return
55
+ setIgnoredNs([...ignoredNs, newNsTrimmed])
56
+ setNewNs('')
37
57
  }
38
58
 
39
59
  const toggleCheck = (checkID: string) => {
@@ -55,13 +75,22 @@ export function AuditSettingsDialog({ namespaces, onClose }: AuditSettingsDialog
55
75
  <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={onClose}>
56
76
  <div className="bg-theme-surface rounded-xl shadow-xl w-full max-w-lg mx-4 max-h-[80vh] flex flex-col" onClick={e => e.stopPropagation()}>
57
77
  <div className="flex items-center justify-between px-5 py-4 border-b border-theme-border shrink-0">
58
- <h2 className="text-sm font-semibold text-theme-text-primary">Audit Settings</h2>
78
+ <h2 className="text-sm font-semibold text-theme-text-primary">Checks Settings</h2>
59
79
  <button onClick={onClose} className="p-1 rounded-lg hover:bg-theme-hover transition-colors">
60
80
  <X className="w-4 h-4 text-theme-text-tertiary" />
61
81
  </button>
62
82
  </div>
63
83
 
64
84
  <div className="px-5 py-4 overflow-y-auto flex-1">
85
+ {!canEdit && (
86
+ <div className="mb-4 rounded-lg border border-theme-border bg-theme-elevated/50 p-3 flex items-start gap-2.5">
87
+ <Lock className="w-3.5 h-3.5 mt-0.5 shrink-0 text-theme-text-tertiary" />
88
+ <p className="text-xs text-theme-text-tertiary">
89
+ Audit policy is shared across everyone using this Radar instance, so editing
90
+ is limited to owners. You can review the current settings here.
91
+ </p>
92
+ </div>
93
+ )}
65
94
  {/* Ignored Namespaces */}
66
95
  <div className="mb-6">
67
96
  <label className="text-xs font-medium text-theme-text-secondary uppercase tracking-wider">
@@ -77,7 +106,8 @@ export function AuditSettingsDialog({ namespaces, onClose }: AuditSettingsDialog
77
106
  <span className="text-sm text-theme-text-primary">{ns}</span>
78
107
  <button
79
108
  onClick={() => setIgnoredNs(ignoredNs.filter(n => n !== ns))}
80
- className="p-1 rounded hover:bg-theme-hover text-theme-text-tertiary hover:text-red-400 transition-colors"
109
+ disabled={!canEdit}
110
+ className="p-1 rounded hover:bg-theme-hover text-theme-text-tertiary hover:text-red-400 transition-colors disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:text-theme-text-tertiary"
81
111
  >
82
112
  <Trash2 className="w-3.5 h-3.5" />
83
113
  </button>
@@ -89,22 +119,36 @@ export function AuditSettingsDialog({ namespaces, onClose }: AuditSettingsDialog
89
119
  </div>
90
120
 
91
121
  <div className="flex gap-2">
92
- <input
93
- type="text"
122
+ <Input
94
123
  value={newNs}
95
124
  onChange={e => setNewNs(e.target.value)}
96
125
  onKeyDown={e => { if (e.key === 'Enter') addNamespace() }}
97
126
  placeholder="Add namespace..."
98
- className="flex-1 px-3 py-1.5 bg-theme-elevated border border-theme-border-light rounded-lg text-sm text-theme-text-primary placeholder-theme-text-disabled focus:outline-none focus:ring-2 focus:ring-skyhook-500"
127
+ disabled={!canEdit}
128
+ aria-invalid={newNsError ? true : undefined}
129
+ aria-describedby="new-ns-help"
130
+ className={clsx(
131
+ 'flex-1 px-3 py-1.5 bg-theme-elevated border rounded-lg text-sm text-theme-text-primary placeholder-theme-text-disabled focus:outline-none focus:ring-2',
132
+ newNsError || newNsDuplicate
133
+ ? 'border-red-500/60 focus:ring-red-500'
134
+ : 'border-theme-border-light focus:ring-skyhook-500',
135
+ )}
99
136
  />
100
137
  <button
101
138
  onClick={addNamespace}
102
- disabled={!newNs.trim()}
139
+ disabled={!canEdit || !canAddNamespace}
103
140
  className="px-3 py-1.5 text-sm btn-brand rounded-lg disabled:opacity-50 disabled:cursor-not-allowed"
104
141
  >
105
142
  <Plus className="w-4 h-4" />
106
143
  </button>
107
144
  </div>
145
+ {(newNsError || newNsDuplicate) && (
146
+ <p id="new-ns-help" className="mt-1.5 text-xs text-red-400">
147
+ {newNsDuplicate
148
+ ? `"${newNsTrimmed}" is already in the list.`
149
+ : `Namespace ${newNsError}.`}
150
+ </p>
151
+ )}
108
152
  </div>
109
153
 
110
154
  {/* Disabled Checks */}
@@ -128,7 +172,8 @@ export function AuditSettingsDialog({ namespaces, onClose }: AuditSettingsDialog
128
172
  type="checkbox"
129
173
  checked={!disabled}
130
174
  onChange={() => toggleCheck(check.id)}
131
- className="w-4 h-4 rounded border-theme-border text-skyhook-500 focus:ring-skyhook-500"
175
+ disabled={!canEdit}
176
+ className="w-4 h-4 rounded border-theme-border text-skyhook-500 focus:ring-skyhook-500 disabled:opacity-40 disabled:cursor-not-allowed"
132
177
  />
133
178
  <div className="flex-1 min-w-0">
134
179
  <span className="text-sm text-theme-text-primary">{check.title}</span>
@@ -148,13 +193,30 @@ export function AuditSettingsDialog({ namespaces, onClose }: AuditSettingsDialog
148
193
  >
149
194
  Cancel
150
195
  </button>
196
+ <Tooltip
197
+ content={
198
+ !canEdit
199
+ ? 'Audit settings can only be changed by owners'
200
+ : newNsError
201
+ ? 'Fix or clear the pending namespace input before saving'
202
+ : newNsDuplicate
203
+ ? 'Clear the duplicate pending input before saving'
204
+ : ''
205
+ }
206
+ >
151
207
  <button
152
208
  onClick={handleSave}
153
- disabled={updateSettings.isPending}
154
- className="px-4 py-1.5 text-sm btn-brand rounded-lg disabled:opacity-50"
209
+ // Block save while the namespace input has unfixed pending
210
+ // text otherwise the user clicks Save expecting their
211
+ // entry to be included and it's silently dropped.
212
+ disabled={
213
+ !canEdit || updateSettings.isPending || newNsError !== null || newNsDuplicate
214
+ }
215
+ className="px-4 py-1.5 text-sm btn-brand rounded-lg disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none"
155
216
  >
156
217
  {updateSettings.isPending ? 'Saving...' : 'Save'}
157
218
  </button>
219
+ </Tooltip>
158
220
  </div>
159
221
  </div>
160
222
  </div>
@@ -1,25 +1,39 @@
1
1
  import { useState, useCallback } from 'react'
2
- import { useAudit, useAuditSettings, useUpdateAuditSettings } from '../../api/client'
2
+ import { useAudit, useAuditSettings, useUpdateAuditSettings, useCloudRole } from '../../api/client'
3
3
  import type { SelectedResource } from '../../types'
4
- import { AuditFindingsTable, PaneLoader } from '@skyhook-io/k8s-ui'
5
- import { ArrowLeft, ClipboardCheck, Settings } from 'lucide-react'
4
+ import { ChecksView, PaneLoader, PageHeader, FreshnessControl, type CheckResourceRef } from '@skyhook-io/k8s-ui'
5
+ import { ShieldCheck, Settings } from 'lucide-react'
6
6
  import { AuditSettingsDialog } from './AuditSettingsDialog'
7
+ import { Tooltip } from '../ui/Tooltip'
8
+ import { useConnection } from '../../context/ConnectionContext'
7
9
 
8
10
  interface AuditViewProps {
9
11
  namespaces: string[]
10
- onBack: () => void
11
12
  onNavigateToResource: (resource: SelectedResource) => void
12
13
  }
13
14
 
14
- export function AuditView({ namespaces, onBack, onNavigateToResource }: AuditViewProps) {
15
- const { data, isLoading, error } = useAudit(namespaces)
15
+ // The per-cluster Checks surface. Renders the same shared remediation queue
16
+ // (ChecksView) the Hub fleet view uses — single cluster here, so no cluster
17
+ // label and in-app (client-side) resource navigation. The rollup + priority
18
+ // come pre-computed from radar's /api/audit (pkg/audit.BuildChecks); local
19
+ // ~/.radar settings are this cluster's "policy" and the row hide-menu writes to
20
+ // them.
21
+ export function AuditView({ namespaces, onNavigateToResource }: AuditViewProps) {
22
+ const { data, isLoading, error, dataUpdatedAt, refetch } = useAudit(namespaces)
16
23
  const { data: auditSettings } = useAuditSettings()
17
24
  const updateSettings = useUpdateAuditSettings()
25
+ // Audit policy is owner-gated (enforced server-side). Withhold the inline
26
+ // hide affordances from non-owners so they don't click into a 403 — the
27
+ // hide menus render only when these callbacks are passed.
28
+ const { canAtLeast } = useCloudRole()
29
+ const canEdit = canAtLeast('owner')
18
30
  const [showSettings, setShowSettings] = useState(false)
19
31
 
20
32
  const ignoredCount = auditSettings?.ignoredNamespaces?.length ?? 0
21
33
 
22
- // Inline hide actions persist to settings immediately
34
+ const { connection } = useConnection()
35
+
36
+ // Inline hide actions — persist to local settings immediately.
23
37
  const hideCheck = useCallback((checkID: string) => {
24
38
  if (!auditSettings) return
25
39
  const current = auditSettings.disabledChecks || []
@@ -29,31 +43,23 @@ export function AuditView({ namespaces, onBack, onNavigateToResource }: AuditVie
29
43
 
30
44
  const hideCategory = useCallback((category: string) => {
31
45
  if (!auditSettings || !data?.checks) return
32
- const checksInCategory = Object.values(data.checks).filter(c => {
33
- // Match checks whose findings are in this category
34
- return data.findings.some(f => f.checkID === c.id && f.category === category)
35
- }).map(c => c.id)
46
+ const checksInCategory = Object.values(data.checks)
47
+ .filter((c) => data.findings.some((f) => f.checkID === c.id && f.category === category))
48
+ .map((c) => c.id)
36
49
  const current = auditSettings.disabledChecks || []
37
- const toAdd = checksInCategory.filter(id => !current.includes(id))
50
+ const toAdd = checksInCategory.filter((id) => !current.includes(id))
38
51
  if (toAdd.length === 0) return
39
52
  updateSettings.mutate({ ...auditSettings, disabledChecks: [...current, ...toAdd] })
40
53
  }, [auditSettings, data, updateSettings])
41
54
 
42
- const hideNamespace = useCallback((ns: string) => {
43
- if (!auditSettings) return
44
- const current = auditSettings.ignoredNamespaces || []
45
- if (current.includes(ns)) return
46
- updateSettings.mutate({ ...auditSettings, ignoredNamespaces: [...current, ns] })
47
- }, [auditSettings, updateSettings])
48
-
49
55
  if (isLoading) {
50
- return <PaneLoader label="Loading audit data…" className="flex-1" />
56
+ return <PaneLoader label="Loading checks…" className="flex-1" />
51
57
  }
52
58
 
53
59
  if (error) {
54
60
  return (
55
61
  <div className="flex-1 flex items-center justify-center text-theme-text-secondary">
56
- <p>Failed to load audit data</p>
62
+ <p>Failed to load checks</p>
57
63
  </div>
58
64
  )
59
65
  }
@@ -61,53 +67,50 @@ export function AuditView({ namespaces, onBack, onNavigateToResource }: AuditVie
61
67
  if (!data) {
62
68
  return (
63
69
  <div className="flex-1 flex items-center justify-center text-theme-text-secondary">
64
- <p>No audit data available</p>
70
+ <p>No check data available</p>
65
71
  </div>
66
72
  )
67
73
  }
68
74
 
69
- return (
70
- <div className="flex-1 flex flex-col min-h-0 p-6 gap-6 overflow-auto">
71
- {/* Header */}
72
- <div className="flex items-center gap-4">
73
- <button
74
- onClick={onBack}
75
- className="p-1.5 rounded-lg hover:bg-theme-hover transition-colors"
76
- >
77
- <ArrowLeft className="w-5 h-5 text-theme-text-secondary" />
78
- </button>
79
- <div className="flex-1">
80
- <div className="flex items-center gap-2">
81
- <ClipboardCheck className="w-5 h-5 text-theme-text-secondary" />
82
- <h1 className="text-lg font-semibold text-theme-text-primary">Cluster Audit</h1>
83
- </div>
84
- <p className="text-sm text-theme-text-tertiary mt-1 ml-7">
85
- Security, reliability, and efficiency checks based on Kubernetes best practices from NSA/CISA guidelines, CIS benchmarks, and industry tools like Polaris and Kubescape.
86
- </p>
87
- </div>
88
- <div className="flex items-center gap-2 shrink-0">
89
- {ignoredCount > 0 && (
90
- <button onClick={() => setShowSettings(true)} className="text-xs text-theme-text-tertiary hover:text-theme-text-secondary transition-colors">{ignoredCount} {ignoredCount === 1 ? 'namespace' : 'namespaces'} hidden</button>
91
- )}
92
- <button
93
- onClick={() => setShowSettings(true)}
94
- className="p-2 rounded-lg hover:bg-theme-hover text-theme-text-tertiary hover:text-theme-text-secondary transition-colors"
95
- title="Audit settings"
96
- >
97
- <Settings className="w-4 h-4" />
98
- </button>
99
- </div>
100
- </div>
75
+ const onResourceClick = (ref: CheckResourceRef) =>
76
+ onNavigateToResource({ kind: ref.kind, namespace: ref.namespace, name: ref.name, group: ref.group })
101
77
 
102
- <AuditFindingsTable
103
- groups={data.groups}
104
- checks={data.checks}
105
- onResourceClick={(kind, namespace, name) =>
106
- onNavigateToResource({ kind, namespace, name })
78
+ return (
79
+ <div className="flex-1 flex flex-col min-h-0 p-4 gap-4 overflow-auto">
80
+ <PageHeader
81
+ icon={ShieldCheck}
82
+ title="Checks"
83
+ description="Security, reliability, and efficiency best practices (NSA/CISA, CIS, Polaris, Kubescape), grouped into a remediation queue."
84
+ actions={
85
+ <>
86
+ <FreshnessControl
87
+ mode="auto"
88
+ dataUpdatedAt={dataUpdatedAt}
89
+ onRefresh={() => refetch()}
90
+ connectionState={connection.state}
91
+ />
92
+ {ignoredCount > 0 && (
93
+ <button onClick={() => setShowSettings(true)} className="text-xs text-theme-text-tertiary hover:text-theme-text-secondary transition-colors">{ignoredCount} {ignoredCount === 1 ? 'namespace' : 'namespaces'} hidden</button>
94
+ )}
95
+ <Tooltip content="Checks settings">
96
+ <button
97
+ onClick={() => setShowSettings(true)}
98
+ className="p-2 rounded-lg hover:bg-theme-hover text-theme-text-tertiary hover:text-theme-text-secondary transition-colors"
99
+ >
100
+ <Settings className="w-4 h-4" />
101
+ </button>
102
+ </Tooltip>
103
+ </>
107
104
  }
108
- onHideCheck={hideCheck}
109
- onHideCategory={hideCategory}
110
- onHideNamespace={hideNamespace}
105
+ />
106
+
107
+ <ChecksView
108
+ checks={data.groupedChecks ?? []}
109
+ catalog={data.checks ?? {}}
110
+ anyData
111
+ onResourceClick={onResourceClick}
112
+ onHideCheck={canEdit ? hideCheck : undefined}
113
+ onHideCategory={canEdit ? hideCategory : undefined}
111
114
  />
112
115
 
113
116
  {showSettings && <AuditSettingsDialog namespaces={namespaces} onClose={() => setShowSettings(false)} />}
@@ -0,0 +1,124 @@
1
+ import { useCallback, useMemo, useState } from 'react'
2
+ import { useNavigate, useSearchParams } from 'react-router-dom'
3
+ import {
4
+ ResourceCompareView,
5
+ CompareResourcePicker,
6
+ parseRef,
7
+ refToParam,
8
+ type CompareResourceRef,
9
+ type CompareSide,
10
+ type CompareSideError,
11
+ } from '@skyhook-io/k8s-ui'
12
+ import { useResource } from '../../api/client'
13
+ import { useTheme } from '../../context/ThemeContext'
14
+ import { useCompareCandidates } from './useCompareCandidates'
15
+
16
+ export function CompareViewRoute() {
17
+ const navigate = useNavigate()
18
+ const [searchParams, setSearchParams] = useSearchParams()
19
+ const { theme } = useTheme()
20
+
21
+ const kind = (searchParams.get('kind') ?? '').toLowerCase()
22
+ // Matches Radar's repo-wide URL convention. The bare `group` param is
23
+ // reserved for topology grouping mode and gets stripped by App.tsx's URL
24
+ // sync on every non-topology view.
25
+ const group = searchParams.get('apiGroup') ?? undefined
26
+ const aRaw = searchParams.get('a')
27
+ const bRaw = searchParams.get('b')
28
+
29
+ const [pickerOpen, setPickerOpen] = useState<CompareSide | null>(null)
30
+
31
+ // Memoized so the refs keep a stable identity across renders — otherwise the
32
+ // useCallbacks below (which depend on a/b) would be rebuilt every render.
33
+ const a: CompareResourceRef | null = useMemo(() => {
34
+ const p = parseRef(aRaw)
35
+ return p ? { kind, namespace: p.namespace, name: p.name, group } : null
36
+ }, [aRaw, kind, group])
37
+ const b: CompareResourceRef | null = useMemo(() => {
38
+ const p = parseRef(bRaw)
39
+ return p ? { kind, namespace: p.namespace, name: p.name, group } : null
40
+ }, [bRaw, kind, group])
41
+
42
+ const aQuery = useResource<unknown>(a?.kind ?? '', a?.namespace ?? '', a?.name ?? '', a?.group)
43
+ const bQuery = useResource<unknown>(b?.kind ?? '', b?.namespace ?? '', b?.name ?? '', b?.group)
44
+
45
+ const { candidates, isPending: candidatesPending, error: candidatesError } = useCompareCandidates(kind, group, !!pickerOpen)
46
+
47
+ const updateParam = useCallback(
48
+ (next: Record<string, string>) => {
49
+ const params = new URLSearchParams(searchParams)
50
+ for (const [k, v] of Object.entries(next)) params.set(k, v)
51
+ setSearchParams(params, { replace: true })
52
+ },
53
+ [searchParams, setSearchParams],
54
+ )
55
+
56
+ const handleSwap = useCallback(() => {
57
+ if (!a || !b) return
58
+ updateParam({ a: refToParam(b), b: refToParam(a) })
59
+ }, [a, b, updateParam])
60
+
61
+ const handleClose = useCallback(() => {
62
+ navigate(-1)
63
+ }, [navigate])
64
+
65
+ const handlePick = useCallback(
66
+ (picked: CompareResourceRef) => {
67
+ if (!pickerOpen) return
68
+ updateParam({ [pickerOpen]: refToParam({ namespace: picked.namespace, name: picked.name }) })
69
+ setPickerOpen(null)
70
+ },
71
+ [pickerOpen, updateParam],
72
+ )
73
+
74
+ if (!kind || !a || !b) {
75
+ return (
76
+ <div className="flex flex-col items-center justify-center h-full text-theme-text-secondary gap-3 p-8">
77
+ <p className="text-sm">This compare link is missing required parameters.</p>
78
+ <button
79
+ onClick={() => navigate('/resources')}
80
+ className="px-3 py-1.5 text-xs font-medium btn-brand rounded-lg"
81
+ >
82
+ Back to resources
83
+ </button>
84
+ </div>
85
+ )
86
+ }
87
+
88
+ // A refetch failure with cached data is not worth shouting about — show the
89
+ // stale data instead of blanking the side with a misleading "failed" banner.
90
+ const errors: CompareSideError[] = []
91
+ if (aQuery.error && !aQuery.data) errors.push({ side: 'a', message: aQuery.error instanceof Error ? aQuery.error.message : String(aQuery.error) })
92
+ if (bQuery.error && !bQuery.data) errors.push({ side: 'b', message: bQuery.error instanceof Error ? bQuery.error.message : String(bQuery.error) })
93
+
94
+ const source = pickerOpen === 'a' ? a : pickerOpen === 'b' ? b : null
95
+
96
+ return (
97
+ <>
98
+ <ResourceCompareView
99
+ a={a}
100
+ b={b}
101
+ aData={aQuery.data}
102
+ bData={bQuery.data}
103
+ errors={errors}
104
+ editorTheme={theme === 'dark' ? 'vs-dark' : 'vs'}
105
+ onSwap={handleSwap}
106
+ onClose={handleClose}
107
+ onChangeA={() => setPickerOpen('a')}
108
+ onChangeB={() => setPickerOpen('b')}
109
+ />
110
+ {source && pickerOpen && (
111
+ <CompareResourcePicker
112
+ open={true}
113
+ onClose={() => setPickerOpen(null)}
114
+ source={source}
115
+ sourceSide={pickerOpen}
116
+ candidates={candidates}
117
+ loading={candidatesPending}
118
+ error={candidatesError}
119
+ onPick={handlePick}
120
+ />
121
+ )}
122
+ </>
123
+ )
124
+ }
@@ -0,0 +1,27 @@
1
+ import { useMemo } from 'react'
2
+ import type { CompareResourceRef } from '@skyhook-io/k8s-ui'
3
+ import { useResources } from '../../api/client'
4
+
5
+ /**
6
+ * Fetch candidates for the compare picker — same kind as the source.
7
+ * Pass `enabled=false` when the picker is closed to avoid hitting the API.
8
+ */
9
+ export function useCompareCandidates(kind: string, group: string | undefined, enabled: boolean) {
10
+ const query = useResources<{ metadata?: { name?: string; namespace?: string } }>(
11
+ enabled ? kind : '',
12
+ undefined,
13
+ group,
14
+ )
15
+ const candidates: CompareResourceRef[] = useMemo(() => {
16
+ if (!query.data) return []
17
+ return query.data
18
+ .filter(r => r?.metadata?.name)
19
+ .map(r => ({
20
+ kind,
21
+ namespace: r.metadata?.namespace ?? '',
22
+ name: r.metadata!.name!,
23
+ group,
24
+ }))
25
+ }, [query.data, kind, group])
26
+ return { candidates, isPending: query.isPending, error: query.error }
27
+ }
@@ -0,0 +1,79 @@
1
+ import { useCallback, useState } from 'react'
2
+ import { useNavigate } from 'react-router-dom'
3
+ import { CompareResourcePicker, refToParam, type CompareResourceRef } from '@skyhook-io/k8s-ui'
4
+ import { useCompareCandidates } from './useCompareCandidates'
5
+ import { useNavCustomization } from '../../context/NavCustomization'
6
+
7
+ interface UseCompareLauncherArgs {
8
+ /** API plural kind (e.g. "deployments") — must match the route segment used by `/api/resources/{kind}`. */
9
+ kind: string
10
+ namespace: string
11
+ name: string
12
+ /** API group for the resource — required for CRDs that collide with core kinds. */
13
+ group?: string
14
+ }
15
+
16
+ interface CompareLauncher {
17
+ /** Wire this to ResourceActionsBar's `onCompareTo` prop. */
18
+ onCompareTo: () => void
19
+ /**
20
+ * Wire this to ResourceActionsBar's `onCompareAcrossClusters` prop. Undefined
21
+ * when the host (NavCustomization.crossClusterCompareHref) hasn't opted in
22
+ * — keeps the standalone Radar experience identical.
23
+ */
24
+ onCompareAcrossClusters?: () => void
25
+ /** Render this anywhere in the same tree to surface the picker dialog. */
26
+ picker: React.ReactNode
27
+ }
28
+
29
+ export function useCompareLauncher({ kind, namespace, name, group }: UseCompareLauncherArgs): CompareLauncher {
30
+ const navigate = useNavigate()
31
+ const [open, setOpen] = useState(false)
32
+ const kindLower = kind.toLowerCase()
33
+ const { candidates, isPending, error } = useCompareCandidates(kindLower, group, open)
34
+ const { crossClusterCompareHref, onHostNavigate } = useNavCustomization()
35
+
36
+ const onCompareTo = useCallback(() => setOpen(true), [])
37
+
38
+ const onCompareAcrossClusters = useCallback(() => {
39
+ if (!crossClusterCompareHref) return
40
+ const href = crossClusterCompareHref({ kind: kindLower, namespace, name, group })
41
+ // Smooth same-document hand-off when the host supports it (Radar Cloud's
42
+ // cross-tree swap), else a hard nav — same contract as App.tsx's goHost.
43
+ if (onHostNavigate) onHostNavigate(href)
44
+ else window.location.assign(href)
45
+ }, [crossClusterCompareHref, onHostNavigate, kindLower, namespace, name, group])
46
+
47
+ const handlePick = useCallback(
48
+ (picked: CompareResourceRef) => {
49
+ setOpen(false)
50
+ const params = new URLSearchParams()
51
+ params.set('kind', kindLower)
52
+ if (group) params.set('apiGroup', group)
53
+ params.set('a', refToParam({ namespace, name }))
54
+ params.set('b', refToParam({ namespace: picked.namespace, name: picked.name }))
55
+ navigate({ pathname: '/compare', search: params.toString() })
56
+ },
57
+ [navigate, kindLower, group, namespace, name],
58
+ )
59
+
60
+ const source: CompareResourceRef = { kind: kindLower, namespace, name, group }
61
+
62
+ const picker = (
63
+ <CompareResourcePicker
64
+ open={open}
65
+ onClose={() => setOpen(false)}
66
+ source={source}
67
+ candidates={candidates}
68
+ loading={open && isPending}
69
+ error={open ? error : null}
70
+ onPick={handlePick}
71
+ />
72
+ )
73
+
74
+ return {
75
+ onCompareTo,
76
+ onCompareAcrossClusters: crossClusterCompareHref ? onCompareAcrossClusters : undefined,
77
+ picker,
78
+ }
79
+ }