@skyhook-io/radar-app 1.2.3 → 1.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skyhook-io/radar-app",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "Radar's full web UI as a reusable React component. Used by Radar's own binary and by external consumers like Radar Cloud.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"yaml": "^2.9.0"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
|
-
"@skyhook-io/k8s-ui": ">=1.
|
|
42
|
+
"@skyhook-io/k8s-ui": ">=1.7.3",
|
|
43
43
|
"@tanstack/react-query": ">=5",
|
|
44
44
|
"@xyflow/react": ">=12.0.0",
|
|
45
45
|
"clsx": ">=2",
|
package/src/App.tsx
CHANGED
|
@@ -273,6 +273,22 @@ function AppInner() {
|
|
|
273
273
|
navigate({ pathname: path, search: newParams.toString() })
|
|
274
274
|
}, [navigate, searchParams])
|
|
275
275
|
|
|
276
|
+
// Cloud (embedded) makes the host's fleet Checks queue the one canonical
|
|
277
|
+
// surface — owned by the host's left rail — so Radar drops its own Audit
|
|
278
|
+
// pill (see the nav below) and any route to /audit redirects to the fleet
|
|
279
|
+
// Checks queue scoped to this cluster. Entry points that still land on
|
|
280
|
+
// /audit — the Home "Cluster Audit" card, ⌘K, WorkloadView's "view all"
|
|
281
|
+
// findings, bookmarks/deep links — all funnel through here. `replace` (not
|
|
282
|
+
// assign) keeps the transient /audit URL out of history so Back doesn't
|
|
283
|
+
// bounce off the redirect. Standalone OSS (no clusterChecksHref) is
|
|
284
|
+
// unaffected and renders the in-app audit view as before.
|
|
285
|
+
const clusterChecksHref = navCustomization.clusterChecksHref
|
|
286
|
+
useEffect(() => {
|
|
287
|
+
if (clusterChecksHref && mainView === 'audit') {
|
|
288
|
+
window.location.replace(clusterChecksHref())
|
|
289
|
+
}
|
|
290
|
+
}, [clusterChecksHref, mainView])
|
|
291
|
+
|
|
276
292
|
const [namespaces, setNamespaces] = useState<string[]>(getInitialState().namespaces)
|
|
277
293
|
// For large clusters: force SSE to reconnect with namespace filter
|
|
278
294
|
const [forceNamespaceFilter, setForceNamespaceFilter] = useState<string[] | undefined>(undefined)
|
|
@@ -769,6 +785,25 @@ function AppInner() {
|
|
|
769
785
|
// lists) stay in lockstep with the picker. The dedicated URL-write effect
|
|
770
786
|
// below propagates the mirrored state to `?namespaces=`.
|
|
771
787
|
const setActiveNamespace = useSetActiveNamespace()
|
|
788
|
+
// Defer the state flip to onSuccess. Setting namespaces to [] before the
|
|
789
|
+
// server-side pref has actually been cleared makes React Query refetch
|
|
790
|
+
// under the new empty key while the server still returns the previous
|
|
791
|
+
// pick's scope, caching stale data under the new key with no later
|
|
792
|
+
// invalidation. onSettled would do the same on errors, leaving the UI
|
|
793
|
+
// showing "All namespaces" while data is still namespace-scoped — onSuccess
|
|
794
|
+
// keeps state aligned with the server.
|
|
795
|
+
//
|
|
796
|
+
// Don't touch the URL here either: setSearchParams on a still-set state
|
|
797
|
+
// trips the URL→state sync into firing setNamespaces([]) and a duplicate
|
|
798
|
+
// mutation immediately, which re-introduces the same race. The state→URL
|
|
799
|
+
// effect propagates state=[] → URL on its own after onSuccess flips state.
|
|
800
|
+
const clearAllNamespaces = useCallback(() => {
|
|
801
|
+
if (namespaces.length === 0) return
|
|
802
|
+
setActiveNamespace.mutate(
|
|
803
|
+
{ namespaces: [] },
|
|
804
|
+
{ onSuccess: () => setNamespaces([]) },
|
|
805
|
+
)
|
|
806
|
+
}, [namespaces.length, setActiveNamespace])
|
|
772
807
|
const initialBookmarkReconciledRef = useRef(false)
|
|
773
808
|
const scopeActives = useMemo(() => namespaceScope?.actives ?? [], [namespaceScope?.actives])
|
|
774
809
|
const namespaceScopeKey = useMemo(() => namespaceScope ? [...scopeActives].sort().join(',') : null, [namespaceScope, scopeActives])
|
|
@@ -1113,7 +1148,17 @@ function AppInner() {
|
|
|
1113
1148
|
// exists and is reachable via /cost, the Home dashboard card, and the
|
|
1114
1149
|
// command palette (⌘K). Remove this comment to restore it.
|
|
1115
1150
|
{ view: 'audit' as const, icon: ShieldCheck, label: 'Audit' },
|
|
1116
|
-
] as const)
|
|
1151
|
+
] as const)
|
|
1152
|
+
// In Cloud, Checks is a fleet-scoped feature owned by the host's
|
|
1153
|
+
// left rail; the per-cluster view is just that fleet queue filtered
|
|
1154
|
+
// to this cluster, so duplicating it as a peer pill here would be a
|
|
1155
|
+
// second "Checks" that teleports out of the cluster shell. Drop the
|
|
1156
|
+
// Audit tab when embedded — cluster-scoped access stays available
|
|
1157
|
+
// via the Home "Cluster Audit" card (→ /audit, redirected to the
|
|
1158
|
+
// scoped fleet Checks by the clusterChecksHref effect above), ⌘K,
|
|
1159
|
+
// and bookmarks. Standalone OSS keeps the Audit tab.
|
|
1160
|
+
.filter(({ view }) => !(view === 'audit' && clusterChecksHref))
|
|
1161
|
+
.map(({ view, icon: Icon, label }) => (
|
|
1117
1162
|
<Tooltip key={view} content={label} delay={100} position="bottom">
|
|
1118
1163
|
<button
|
|
1119
1164
|
onClick={() => setMainView(view)}
|
|
@@ -1490,6 +1535,7 @@ function AppInner() {
|
|
|
1490
1535
|
onResourceClick={(res) => res ? navigateToResource(res) : setSelectedResource(null)}
|
|
1491
1536
|
onResourceClickYaml={(res) => navigateToResource(res, 'yaml')}
|
|
1492
1537
|
onKindChange={() => setSelectedResource(null)}
|
|
1538
|
+
onClearNamespaces={clearAllNamespaces}
|
|
1493
1539
|
/>
|
|
1494
1540
|
)}
|
|
1495
1541
|
|
|
@@ -1538,6 +1584,7 @@ function AppInner() {
|
|
|
1538
1584
|
onOpenResource={(resource) => {
|
|
1539
1585
|
setSelectedResource(resource)
|
|
1540
1586
|
}}
|
|
1587
|
+
onClearNamespaces={clearAllNamespaces}
|
|
1541
1588
|
/>
|
|
1542
1589
|
)}
|
|
1543
1590
|
|
|
@@ -1551,8 +1598,17 @@ function AppInner() {
|
|
|
1551
1598
|
<CostView onBack={() => setMainView('home')} />
|
|
1552
1599
|
)}
|
|
1553
1600
|
|
|
1554
|
-
{/* Best practices detail view
|
|
1555
|
-
|
|
1601
|
+
{/* Best practices detail view. In Cloud this redirects to the host's
|
|
1602
|
+
fleet Checks queue (clusterChecksHref effect above) — render a brief
|
|
1603
|
+
splash instead of the single-cluster view while the cross-document
|
|
1604
|
+
nav lands. */}
|
|
1605
|
+
{mainView === 'audit' && clusterChecksHref && (
|
|
1606
|
+
<div className="flex-1 flex flex-col items-center justify-center gap-3 bg-theme-base">
|
|
1607
|
+
<img src={radarLoadingIcon} alt="" aria-hidden className="w-11 h-11" />
|
|
1608
|
+
<p className="text-sm text-theme-text-secondary">Opening Checks…</p>
|
|
1609
|
+
</div>
|
|
1610
|
+
)}
|
|
1611
|
+
{mainView === 'audit' && !clusterChecksHref && (
|
|
1556
1612
|
<AuditView
|
|
1557
1613
|
namespaces={namespaces}
|
|
1558
1614
|
onBack={() => setMainView('home')}
|
package/src/api/client.ts
CHANGED
|
@@ -237,15 +237,19 @@ export interface DashboardCRDCount {
|
|
|
237
237
|
}
|
|
238
238
|
|
|
239
239
|
// Re-export shared types from k8s-ui — single source of truth
|
|
240
|
-
import type { AuditCardData, AuditFinding, ResourceGroup, CheckMeta } from '@skyhook-io/k8s-ui'
|
|
240
|
+
import type { AuditCardData, AuditFinding, ResourceGroup, CheckMeta, Check } from '@skyhook-io/k8s-ui'
|
|
241
241
|
export type DashboardAudit = AuditCardData
|
|
242
|
-
export type { AuditFinding, ResourceGroup, CheckMeta }
|
|
242
|
+
export type { AuditFinding, ResourceGroup, CheckMeta, Check }
|
|
243
243
|
|
|
244
244
|
export interface AuditResponse {
|
|
245
245
|
summary: DashboardAudit
|
|
246
246
|
findings: AuditFinding[]
|
|
247
247
|
groups: ResourceGroup[]
|
|
248
248
|
checks: Record<string, CheckMeta>
|
|
249
|
+
// Remediation-queue rollup: findings grouped by check, prioritized. Present
|
|
250
|
+
// on the non-raw scan (standalone + embedded per-cluster views); the Checks
|
|
251
|
+
// queue renders this.
|
|
252
|
+
groupedChecks?: Check[]
|
|
249
253
|
}
|
|
250
254
|
|
|
251
255
|
export interface DashboardCertificateHealth {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useState, useCallback } from 'react'
|
|
2
2
|
import { useAudit, useAuditSettings, useUpdateAuditSettings } from '../../api/client'
|
|
3
3
|
import type { SelectedResource } from '../../types'
|
|
4
|
-
import {
|
|
4
|
+
import { ChecksView, PaneLoader, type CheckResourceRef } from '@skyhook-io/k8s-ui'
|
|
5
5
|
import { ArrowLeft, ClipboardCheck, Settings } from 'lucide-react'
|
|
6
6
|
import { AuditSettingsDialog } from './AuditSettingsDialog'
|
|
7
7
|
|
|
@@ -11,6 +11,12 @@ interface AuditViewProps {
|
|
|
11
11
|
onNavigateToResource: (resource: SelectedResource) => void
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
// The per-cluster Checks surface. Renders the same shared remediation queue
|
|
15
|
+
// (ChecksView) the Hub fleet view uses — single cluster here, so no cluster
|
|
16
|
+
// label and in-app (client-side) resource navigation. The rollup + priority
|
|
17
|
+
// come pre-computed from radar's /api/audit (pkg/audit.BuildChecks); local
|
|
18
|
+
// ~/.radar settings are this cluster's "policy" and the row hide-menu writes to
|
|
19
|
+
// them.
|
|
14
20
|
export function AuditView({ namespaces, onBack, onNavigateToResource }: AuditViewProps) {
|
|
15
21
|
const { data, isLoading, error } = useAudit(namespaces)
|
|
16
22
|
const { data: auditSettings } = useAuditSettings()
|
|
@@ -19,7 +25,7 @@ export function AuditView({ namespaces, onBack, onNavigateToResource }: AuditVie
|
|
|
19
25
|
|
|
20
26
|
const ignoredCount = auditSettings?.ignoredNamespaces?.length ?? 0
|
|
21
27
|
|
|
22
|
-
// Inline hide actions — persist to settings immediately
|
|
28
|
+
// Inline hide actions — persist to local settings immediately.
|
|
23
29
|
const hideCheck = useCallback((checkID: string) => {
|
|
24
30
|
if (!auditSettings) return
|
|
25
31
|
const current = auditSettings.disabledChecks || []
|
|
@@ -29,31 +35,23 @@ export function AuditView({ namespaces, onBack, onNavigateToResource }: AuditVie
|
|
|
29
35
|
|
|
30
36
|
const hideCategory = useCallback((category: string) => {
|
|
31
37
|
if (!auditSettings || !data?.checks) return
|
|
32
|
-
const checksInCategory = Object.values(data.checks)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}).map(c => c.id)
|
|
38
|
+
const checksInCategory = Object.values(data.checks)
|
|
39
|
+
.filter((c) => data.findings.some((f) => f.checkID === c.id && f.category === category))
|
|
40
|
+
.map((c) => c.id)
|
|
36
41
|
const current = auditSettings.disabledChecks || []
|
|
37
|
-
const toAdd = checksInCategory.filter(id => !current.includes(id))
|
|
42
|
+
const toAdd = checksInCategory.filter((id) => !current.includes(id))
|
|
38
43
|
if (toAdd.length === 0) return
|
|
39
44
|
updateSettings.mutate({ ...auditSettings, disabledChecks: [...current, ...toAdd] })
|
|
40
45
|
}, [auditSettings, data, updateSettings])
|
|
41
46
|
|
|
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
47
|
if (isLoading) {
|
|
50
|
-
return <PaneLoader label="Loading
|
|
48
|
+
return <PaneLoader label="Loading checks…" className="flex-1" />
|
|
51
49
|
}
|
|
52
50
|
|
|
53
51
|
if (error) {
|
|
54
52
|
return (
|
|
55
53
|
<div className="flex-1 flex items-center justify-center text-theme-text-secondary">
|
|
56
|
-
<p>Failed to load
|
|
54
|
+
<p>Failed to load checks</p>
|
|
57
55
|
</div>
|
|
58
56
|
)
|
|
59
57
|
}
|
|
@@ -61,11 +59,14 @@ export function AuditView({ namespaces, onBack, onNavigateToResource }: AuditVie
|
|
|
61
59
|
if (!data) {
|
|
62
60
|
return (
|
|
63
61
|
<div className="flex-1 flex items-center justify-center text-theme-text-secondary">
|
|
64
|
-
<p>No
|
|
62
|
+
<p>No check data available</p>
|
|
65
63
|
</div>
|
|
66
64
|
)
|
|
67
65
|
}
|
|
68
66
|
|
|
67
|
+
const onResourceClick = (ref: CheckResourceRef) =>
|
|
68
|
+
onNavigateToResource({ kind: ref.kind, namespace: ref.namespace, name: ref.name, group: ref.group })
|
|
69
|
+
|
|
69
70
|
return (
|
|
70
71
|
<div className="flex-1 flex flex-col min-h-0 p-6 gap-6 overflow-auto">
|
|
71
72
|
{/* Header */}
|
|
@@ -79,10 +80,10 @@ export function AuditView({ namespaces, onBack, onNavigateToResource }: AuditVie
|
|
|
79
80
|
<div className="flex-1">
|
|
80
81
|
<div className="flex items-center gap-2">
|
|
81
82
|
<ClipboardCheck className="w-5 h-5 text-theme-text-secondary" />
|
|
82
|
-
<h1 className="text-lg font-semibold text-theme-text-primary">
|
|
83
|
+
<h1 className="text-lg font-semibold text-theme-text-primary">Checks</h1>
|
|
83
84
|
</div>
|
|
84
85
|
<p className="text-sm text-theme-text-tertiary mt-1 ml-7">
|
|
85
|
-
Security, reliability, and efficiency
|
|
86
|
+
Security, reliability, and efficiency best practices (NSA/CISA, CIS, Polaris, Kubescape), grouped into a remediation queue.
|
|
86
87
|
</p>
|
|
87
88
|
</div>
|
|
88
89
|
<div className="flex items-center gap-2 shrink-0">
|
|
@@ -92,22 +93,20 @@ export function AuditView({ namespaces, onBack, onNavigateToResource }: AuditVie
|
|
|
92
93
|
<button
|
|
93
94
|
onClick={() => setShowSettings(true)}
|
|
94
95
|
className="p-2 rounded-lg hover:bg-theme-hover text-theme-text-tertiary hover:text-theme-text-secondary transition-colors"
|
|
95
|
-
title="
|
|
96
|
+
title="Checks settings"
|
|
96
97
|
>
|
|
97
98
|
<Settings className="w-4 h-4" />
|
|
98
99
|
</button>
|
|
99
100
|
</div>
|
|
100
101
|
</div>
|
|
101
102
|
|
|
102
|
-
<
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
103
|
+
<ChecksView
|
|
104
|
+
checks={data.groupedChecks ?? []}
|
|
105
|
+
catalog={data.checks ?? {}}
|
|
106
|
+
anyData
|
|
107
|
+
onResourceClick={onResourceClick}
|
|
108
108
|
onHideCheck={hideCheck}
|
|
109
109
|
onHideCategory={hideCategory}
|
|
110
|
-
onHideNamespace={hideNamespace}
|
|
111
110
|
/>
|
|
112
111
|
|
|
113
112
|
{showSettings && <AuditSettingsDialog namespaces={namespaces} onClose={() => setShowSettings(false)} />}
|
|
@@ -86,17 +86,18 @@ interface ResourceCountsResponse {
|
|
|
86
86
|
interface GitOpsViewProps {
|
|
87
87
|
namespaces: string[]
|
|
88
88
|
onOpenResource: (resource: SelectedResource) => void
|
|
89
|
+
onClearNamespaces?: () => void
|
|
89
90
|
}
|
|
90
91
|
|
|
91
|
-
export function GitOpsView({ namespaces, onOpenResource }: GitOpsViewProps) {
|
|
92
|
+
export function GitOpsView({ namespaces, onOpenResource, onClearNamespaces }: GitOpsViewProps) {
|
|
92
93
|
const location = useLocation()
|
|
93
94
|
if (location.pathname.startsWith('/gitops/detail/')) {
|
|
94
95
|
return <GitOpsDetailView namespaces={namespaces} onOpenResource={onOpenResource} />
|
|
95
96
|
}
|
|
96
|
-
return <GitOpsTableView namespaces={namespaces} />
|
|
97
|
+
return <GitOpsTableView namespaces={namespaces} onClearNamespaces={onClearNamespaces} />
|
|
97
98
|
}
|
|
98
99
|
|
|
99
|
-
function GitOpsTableView({ namespaces }: { namespaces: string[] }) {
|
|
100
|
+
function GitOpsTableView({ namespaces, onClearNamespaces }: { namespaces: string[]; onClearNamespaces?: () => void }) {
|
|
100
101
|
const navigate = useNavigate()
|
|
101
102
|
const namespacesParam = namespaces.join(',')
|
|
102
103
|
const { data: apiResources, isLoading: apiResourcesLoading } = useAPIResources()
|
|
@@ -170,6 +171,8 @@ function GitOpsTableView({ namespaces }: { namespaces: string[] }) {
|
|
|
170
171
|
navigate({ pathname: gitOpsDetailPath(row.kindName, ns, row.name), search: params.toString() })
|
|
171
172
|
}}
|
|
172
173
|
searchHotkey
|
|
174
|
+
globalNamespaces={namespaces}
|
|
175
|
+
onClearNamespaces={onClearNamespaces}
|
|
173
176
|
/>
|
|
174
177
|
)
|
|
175
178
|
}
|
|
@@ -28,9 +28,10 @@ interface ResourcesViewProps {
|
|
|
28
28
|
onResourceClick?: (resource: SelectedResource | null) => void
|
|
29
29
|
onResourceClickYaml?: NavigateToResource
|
|
30
30
|
onKindChange?: () => void
|
|
31
|
+
onClearNamespaces?: () => void
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
export function ResourcesView({ namespaces, selectedResource, onResourceClick, onResourceClickYaml, onKindChange }: ResourcesViewProps) {
|
|
34
|
+
export function ResourcesView({ namespaces, selectedResource, onResourceClick, onResourceClickYaml, onKindChange, onClearNamespaces }: ResourcesViewProps) {
|
|
34
35
|
const location = useLocation()
|
|
35
36
|
const navigate = useNavigate()
|
|
36
37
|
|
|
@@ -191,6 +192,7 @@ export function ResourcesView({ namespaces, selectedResource, onResourceClick, o
|
|
|
191
192
|
onResourceClick={onResourceClick}
|
|
192
193
|
onResourceClickYaml={onResourceClickYaml}
|
|
193
194
|
onKindChange={onKindChange}
|
|
195
|
+
onClearNamespaces={onClearNamespaces}
|
|
194
196
|
// Injected data
|
|
195
197
|
apiResources={apiResources}
|
|
196
198
|
// Lightweight counts for sidebar (replaces 233 parallel queries)
|
|
@@ -31,6 +31,18 @@ interface NavCustomizationBase {
|
|
|
31
31
|
name: string;
|
|
32
32
|
group?: string;
|
|
33
33
|
}) => string;
|
|
34
|
+
/**
|
|
35
|
+
* When set, Radar treats the host's fleet Checks page as the one canonical
|
|
36
|
+
* Checks surface in Cloud: it removes its own per-cluster Audit tab and
|
|
37
|
+
* redirects any route to /audit (the Home "Cluster Audit" card, ⌘K,
|
|
38
|
+
* bookmarks) to the URL returned here — the host's fleet Checks page scoped
|
|
39
|
+
* to this cluster. Navigated via window.location.replace (a cross-document
|
|
40
|
+
* hop into the host's router) so the transient /audit URL stays out of
|
|
41
|
+
* history. This keeps the per-cluster view and the host's fleet nav from
|
|
42
|
+
* presenting two diverging Checks surfaces. Standalone Radar omits this and
|
|
43
|
+
* keeps its single-cluster Audit tab.
|
|
44
|
+
*/
|
|
45
|
+
clusterChecksHref?: () => string;
|
|
34
46
|
}
|
|
35
47
|
|
|
36
48
|
/**
|