@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.
- package/README.md +7 -1
- package/package.json +33 -25
- package/src/App.tsx +1449 -382
- package/src/RadarApp.tsx +132 -19
- package/src/api/apiResources.ts +1 -1
- package/src/api/client.argoResourceSync.test.ts +69 -0
- package/src/api/client.delta.test.ts +89 -0
- package/src/api/client.deltaSync.test.ts +216 -0
- package/src/api/client.metrics.test.ts +106 -0
- package/src/api/client.rightsizing.test.ts +32 -0
- package/src/api/client.ts +2730 -271
- package/src/api/client.yaml.test.ts +45 -0
- package/src/api/diagnose.ts +289 -0
- package/src/api/quotas.ts +16 -0
- package/src/api/rbac.ts +57 -0
- package/src/api/timelineSource.test.ts +217 -0
- package/src/api/timelineSource.ts +582 -0
- package/src/components/ConnectionErrorView.tsx +186 -70
- package/src/components/ContextSwitcher.tsx +63 -18
- package/src/components/DebugOverlay.tsx +5 -3
- package/src/components/NamespaceSwitcher.tsx +41 -0
- package/src/components/UserMenu.tsx +69 -21
- package/src/components/applications/ApplicationsView.tsx +936 -0
- package/src/components/audit/AuditSettingsDialog.tsx +79 -17
- package/src/components/audit/AuditView.tsx +65 -62
- package/src/components/compare/CompareViewRoute.tsx +124 -0
- package/src/components/compare/useCompareCandidates.ts +27 -0
- package/src/components/compare/useCompareLauncher.tsx +79 -0
- package/src/components/cost/ApplicationCostTab.test.ts +204 -0
- package/src/components/cost/ApplicationCostTab.tsx +571 -0
- package/src/components/cost/CostTrendChart.tsx +106 -75
- package/src/components/cost/CostView.test.ts +12 -0
- package/src/components/cost/CostView.tsx +507 -223
- package/src/components/cost/CostViewTabs.test.tsx +21 -0
- package/src/components/cost/CostViewTabs.tsx +40 -0
- package/src/components/cost/CurrentAllocationUse.test.ts +21 -0
- package/src/components/cost/CurrentAllocationUse.tsx +126 -0
- package/src/components/cost/WorkloadCostTab.test.ts +153 -0
- package/src/components/cost/WorkloadCostTab.tsx +372 -0
- package/src/components/cost/cloud-console.test.ts +39 -0
- package/src/components/cost/cloud-console.ts +81 -0
- package/src/components/cost/errors.ts +8 -0
- package/src/components/cost/format.test.ts +27 -0
- package/src/components/cost/format.ts +46 -0
- package/src/components/cost/kinds.ts +5 -0
- package/src/components/curl/ServiceCurlButton.tsx +445 -0
- package/src/components/diagnose/AISettings.tsx +147 -0
- package/src/components/diagnose/DiagnoseContext.tsx +495 -0
- package/src/components/diagnose/DiagnoseSurface.tsx +394 -0
- package/src/components/diagnose/Home.tsx +163 -0
- package/src/components/diagnose/InvestigationView.tsx +622 -0
- package/src/components/diagnose/LocalDiagnoseAction.tsx +162 -0
- package/src/components/diagnose/launch.ts +65 -0
- package/src/components/diagnose/parts.tsx +1756 -0
- package/src/components/dock/BottomDock.tsx +2 -3
- package/src/components/dock/DockContext.tsx +1 -0
- package/src/components/dock/TerminalTab.tsx +1 -1
- package/src/components/dock/WorkloadLogsTab.tsx +21 -5
- package/src/components/dock/index.ts +1 -1
- package/src/components/execution/BatchExecutionView.test.ts +170 -0
- package/src/components/execution/BatchExecutionView.tsx +1329 -0
- package/src/components/execution/batch-run-actions.test.ts +48 -0
- package/src/components/execution/batch-run-actions.ts +24 -0
- package/src/components/execution/batch-timeline.test.ts +57 -0
- package/src/components/execution/batch-timeline.ts +46 -0
- package/src/components/execution/execution-definition.test.ts +208 -0
- package/src/components/execution/execution-definition.ts +245 -0
- package/src/components/gitops/ArgoResourceDiffLoader.tsx +23 -0
- package/src/components/gitops/GitOpsView.tsx +1042 -0
- package/src/components/gitops/RevisionMetaChip.tsx +63 -0
- package/src/components/helm/ChartBrowser.tsx +87 -31
- package/src/components/helm/HelmCompareRoute.tsx +1341 -0
- package/src/components/helm/HelmReleaseDrawer.test.ts +17 -0
- package/src/components/helm/HelmReleaseDrawer.tsx +1073 -102
- package/src/components/helm/HelmView.tsx +237 -96
- package/src/components/helm/InstallWizard.tsx +94 -38
- package/src/components/helm/ManifestDiffViewer.tsx +8 -27
- package/src/components/helm/OwnedResources.tsx +34 -59
- package/src/components/helm/RevisionHistory.tsx +52 -3
- package/src/components/helm/RoleGatedPanel.tsx +3 -3
- package/src/components/helm/TrackChartSourceDialog.tsx +185 -0
- package/src/components/helm/ValuesDiffPreview.tsx +17 -7
- package/src/components/helm/ValuesViewer.tsx +49 -53
- package/src/components/helm/helm-utils.ts +4 -0
- package/src/components/home/ActivitySummary.tsx +4 -1
- package/src/components/home/ClusterHealthCard.tsx +56 -42
- package/src/components/home/CostCard.tsx +21 -36
- package/src/components/home/GitOpsControllersCard.tsx +110 -0
- package/src/components/home/HelmSummary.tsx +3 -1
- package/src/components/home/HomeView.tsx +339 -105
- package/src/components/home/MCPSetupDialog.tsx +29 -87
- package/src/components/home/TrafficSummary.tsx +2 -2
- package/src/components/home/mcpToolCatalog.ts +333 -0
- package/src/components/issues/IssuesPane.tsx +151 -0
- package/src/components/logs/LogsViewer.tsx +4 -1
- package/src/components/logs/ScheduledWorkloadLogsViewer.tsx +135 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +4 -1
- package/src/components/nav/PrimaryNavRail.tsx +285 -0
- package/src/components/portforward/PortForwardButton.tsx +118 -47
- package/src/components/portforward/PortForwardManager.tsx +253 -131
- package/src/components/resource/HPACharts.tsx +237 -0
- package/src/components/resource/PVCUsageBar.tsx +59 -0
- package/src/components/resource/PrometheusCharts.tsx +160 -584
- package/src/components/resource/PrometheusChartsGrid.tsx +270 -0
- package/src/components/resource/RestartChart.tsx +133 -0
- package/src/components/resource/RightsizingStrip.test.ts +109 -0
- package/src/components/resource/RightsizingStrip.tsx +363 -0
- package/src/components/resource-drawer/ResourceDrawer.tsx +3 -1
- package/src/components/resources/CompositeRenderer.tsx +101 -0
- package/src/components/resources/ImageFilesystemModal.tsx +19 -12
- package/src/components/resources/PodFilesystemModal.tsx +6 -5
- package/src/components/resources/ResourceDetailDrawer.tsx +13 -3
- package/src/components/resources/ResourcesView.tsx +194 -17
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +1 -0
- package/src/components/resources/renderers/HPARenderer.tsx +20 -1
- package/src/components/resources/renderers/NamespaceRenderer.tsx +31 -0
- package/src/components/resources/renderers/NodeRenderer.tsx +10 -4
- package/src/components/resources/renderers/PVCRenderer.tsx +19 -1
- package/src/components/resources/renderers/PodRenderer.tsx +30 -6
- package/src/components/resources/renderers/RoleBindingRenderer.tsx +45 -1
- package/src/components/resources/renderers/RoleRenderer.tsx +27 -1
- package/src/components/resources/renderers/ServiceAccountRenderer.tsx +28 -1
- package/src/components/resources/renderers/ServiceRenderer.tsx +81 -8
- package/src/components/resources/renderers/WorkloadRenderer.tsx +51 -4
- package/src/components/resources/renderers/index.ts +2 -0
- package/src/components/resources/resource-utils.ts +2 -1
- package/src/components/rightsizing/RightsizingScanView.tsx +938 -0
- package/src/components/rightsizing/copy.test.ts +56 -0
- package/src/components/rightsizing/model.test.ts +227 -0
- package/src/components/rightsizing/model.ts +158 -0
- package/src/components/rightsizing/presentation.test.ts +104 -0
- package/src/components/rightsizing/presentation.ts +94 -0
- package/src/components/settings/MyPermissionsDialog.tsx +241 -0
- package/src/components/settings/SettingsDialog.tsx +1505 -165
- package/src/components/shared/CreateResourceDialog.tsx +9 -2
- package/src/components/shared/LargeClusterNamespacePicker.tsx +3 -3
- package/src/components/timeline/LocalTimelineScrubber.tsx +212 -0
- package/src/components/timeline/RetainedTimelineScrubber.tsx +311 -0
- package/src/components/timeline/TimelineList.tsx +86 -13
- package/src/components/timeline/TimelineSwimlanes.tsx +9 -1299
- package/src/components/timeline/TimelineView.tsx +873 -24
- package/src/components/timeline/TimelineView.urlparams.test.ts +335 -0
- package/src/components/traffic/TrafficFilterSidebar.tsx +10 -45
- package/src/components/traffic/TrafficFlowList.tsx +29 -15
- package/src/components/traffic/TrafficGraph.tsx +42 -24
- package/src/components/traffic/TrafficView.tsx +32 -19
- package/src/components/ui/CommandPalette.tsx +8 -215
- package/src/components/ui/DiagnosticsOverlay.tsx +219 -9
- package/src/components/ui/Markdown.tsx +3 -3
- package/src/components/ui/Omnibar.tsx +602 -0
- package/src/components/ui/RadarOmnibar.tsx +52 -0
- package/src/components/ui/SearchSyntaxHelp.tsx +89 -0
- package/src/components/ui/ShortcutHelpOverlay.tsx +3 -2
- package/src/components/ui/UpdateNotification.tsx +48 -36
- package/src/components/ui/command-items.ts +178 -0
- package/src/components/workload/WorkloadView.tsx +1342 -158
- package/src/context/ConnectionContext.tsx +146 -21
- package/src/context/DiagnoseCustomization.tsx +93 -0
- package/src/context/NavCustomization.tsx +75 -0
- package/src/context/TimelineSource.tsx +50 -0
- package/src/contexts/CapabilitiesContext.tsx +32 -8
- package/src/filter/FilterLocationBridge.tsx +30 -0
- package/src/hooks/useClusterLoadState.ts +73 -0
- package/src/hooks/useDocumentTitle.ts +25 -0
- package/src/hooks/useEventSource.ts +6 -0
- package/src/hooks/useKeyboardShortcuts.tsx +1 -0
- package/src/hooks/useMediaQuery.ts +21 -0
- package/src/hooks/useNavRailPinned.ts +46 -0
- package/src/hooks/useRecentResources.ts +49 -0
- package/src/index.css +162 -1
- package/src/index.ts +73 -1
- package/src/main.tsx +7 -5
- package/src/types/clusterLoadState.ts +33 -0
- package/src/types.ts +2 -0
- package/src/utils/auditBadges.ts +53 -0
- package/src/utils/navigation.ts +64 -1
- 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 {
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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">
|
|
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
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
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={!
|
|
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
|
-
|
|
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
|
-
|
|
154
|
-
|
|
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 {
|
|
5
|
-
import {
|
|
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
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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)
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
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
|
|
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
|
|
70
|
+
<p>No check data available</p>
|
|
65
71
|
</div>
|
|
66
72
|
)
|
|
67
73
|
}
|
|
68
74
|
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
+
}
|