@skyhook-io/radar-app 1.2.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyhook-io/radar-app",
3
- "version": "1.2.2",
3
+ "version": "1.3.0",
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",
@@ -33,13 +33,13 @@
33
33
  "diff": "^9.0.0",
34
34
  "monaco-editor": "^0.55.1",
35
35
  "react-markdown": "^10.1.0",
36
- "react-virtuoso": "^4.18.6",
36
+ "react-virtuoso": "^4.18.7",
37
37
  "remark-gfm": "^4.0.1",
38
38
  "shiki": "^4.0.1",
39
39
  "yaml": "^2.9.0"
40
40
  },
41
41
  "peerDependencies": {
42
- "@skyhook-io/k8s-ui": ">=1.5.0",
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",
@@ -55,22 +55,22 @@
55
55
  "@skyhook-io/k8s-ui": "*",
56
56
  "@tailwindcss/typography": "^0.5.19",
57
57
  "@tailwindcss/vite": "^4.3.0",
58
- "@tanstack/react-query": "^5.100.9",
58
+ "@tanstack/react-query": "^5.100.14",
59
59
  "@types/diff": "^8.0.0",
60
60
  "@types/node": "^25.7.0",
61
61
  "@types/react": "^19.2.14",
62
62
  "@types/react-dom": "^19.2.3",
63
- "@vitejs/plugin-react": "^6.0.1",
63
+ "@vitejs/plugin-react": "^6.0.2",
64
64
  "@xyflow/react": "^12.10.2",
65
65
  "clsx": "^2.1.1",
66
66
  "elkjs": "^0.11.1",
67
- "lucide-react": "^1.12.0",
67
+ "lucide-react": "^1.16.0",
68
68
  "postcss": "^8.5.14",
69
69
  "prettier": "^3.8.1",
70
70
  "react": "^19.2.6",
71
71
  "react-dom": "^19.2.6",
72
72
  "react-router-dom": "^7.15.0",
73
- "tailwind-merge": "^3.5.0",
73
+ "tailwind-merge": "^3.6.0",
74
74
  "tailwindcss": "^4.2.4",
75
75
  "typescript": "^6.0.2",
76
76
  "vite": "^8.0.12"
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).map(({ view, icon: Icon, label }) => (
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
- {mainView === 'audit' && (
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 { AuditFindingsTable, PaneLoader } from '@skyhook-io/k8s-ui'
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).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)
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 audit data…" className="flex-1" />
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 audit data</p>
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 audit data available</p>
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">Cluster Audit</h1>
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 checks based on Kubernetes best practices from NSA/CISA guidelines, CIS benchmarks, and industry tools like Polaris and Kubescape.
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="Audit settings"
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
- <AuditFindingsTable
103
- groups={data.groups}
104
- checks={data.checks}
105
- onResourceClick={(kind, namespace, name) =>
106
- onNavigateToResource({ kind, namespace, name })
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
  /**