@skyhook-io/radar-app 1.6.2 → 1.7.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.6.2",
3
+ "version": "1.7.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",
package/src/App.tsx CHANGED
@@ -29,6 +29,7 @@ import { DURATION_DOCK } from '@skyhook-io/k8s-ui/utils/animation'
29
29
  import { ContextSwitcher } from './components/ContextSwitcher'
30
30
  import { NamespaceSwitcher, type NamespaceSwitcherHandle } from './components/NamespaceSwitcher'
31
31
  import { useNavCustomization } from './context/NavCustomization'
32
+ import type { FleetTakeoverTarget } from './context/NavCustomization'
32
33
  import { PrimaryNavRail } from './components/nav/PrimaryNavRail'
33
34
  import { useNavRailPinned } from './hooks/useNavRailPinned'
34
35
  import { useMediaQuery } from './hooks/useMediaQuery'
@@ -276,21 +277,46 @@ function AppInner() {
276
277
  navigate({ pathname: path, search: newParams.toString() })
277
278
  }, [navigate, searchParams])
278
279
 
279
- // Cloud (embedded) makes the host's fleet Checks queue the one canonical
280
- // surface — owned by the host's left rail — so Radar drops its own Audit
281
- // pill (see the nav below) and any route to /audit redirects to the fleet
282
- // Checks queue scoped to this cluster. Entry points that still land on
283
- // /auditthe Home "Cluster Audit" card, ⌘K, WorkloadView's "view all"
284
- // findings, bookmarks/deep links — all funnel through here. `replace` (not
285
- // assign) keeps the transient /audit URL out of history so Back doesn't
286
- // bounce off the redirect. Standalone OSS (no clusterChecksHref) is
287
- // unaffected and renders the in-app audit view as before.
280
+ // Cloud (embedded) takes over the "fleet-shaped" per-cluster views with its
281
+ // own fleet pages scoped to this cluster — owned by the host's left rail — so
282
+ // Radar drops the matching pills (see the nav below) and any route into one
283
+ // of these views redirects to the host. Entry points that still land on the
284
+ // view — Home cards, ⌘K, WorkloadView's "view all" findings, bookmarks/deep
285
+ // links — all funnel through here. The view name doubles as the takeover
286
+ // target ('issues' | 'gitops' | 'checks'; 'audit' normalizes to 'checks' via
287
+ // getViewFromPath). `replace` (not assign) keeps the transient /<view> URL
288
+ // out of history so Back doesn't bounce off the redirect. Standalone OSS (no
289
+ // fleetTakeoverHref) is unaffected and renders the in-app view as before.
290
+ // Resolve every takeover target's host URL ONCE per render so the redirect
291
+ // effect, nav-pill filtering, inline-view gating, and the cert click handler
292
+ // all close over the SAME value — host callbacks aren't guaranteed idempotent
293
+ // (scope / flags / signed URLs can shift between calls). undefined = not taken
294
+ // over → Radar renders the view itself. `clusterChecksHref` is the deprecated
295
+ // pre-1.7 hook, folded into the 'checks' target for back-compat.
296
+ const fleetTakeoverHref = navCustomization.fleetTakeoverHref
288
297
  const clusterChecksHref = navCustomization.clusterChecksHref
298
+ const takeover: Record<FleetTakeoverTarget, string | undefined> = {
299
+ issues: fleetTakeoverHref?.('issues'),
300
+ gitops: fleetTakeoverHref?.('gitops'),
301
+ checks: fleetTakeoverHref?.('checks') ?? clusterChecksHref?.(),
302
+ certs: fleetTakeoverHref?.('certs'),
303
+ }
304
+ // Has the host claimed this view? View-shaped targets only ('certs' has no
305
+ // Radar view — only its Home card consults `takeover`). Used to drop the nav
306
+ // pill and gate the inline view render in favor of the "Opening…" splash.
307
+ const isViewTakenOver = (view: ExtendedMainView): boolean =>
308
+ (view === 'issues' || view === 'gitops' || view === 'checks') && !!takeover[view]
309
+ // The host's URL for the CURRENT view, if taken over. Drives the redirect
310
+ // effect and the "Opening…" splash.
311
+ const viewTakeoverHref =
312
+ mainView === 'issues' || mainView === 'gitops' || mainView === 'checks'
313
+ ? takeover[mainView]
314
+ : undefined
289
315
  useEffect(() => {
290
- if (clusterChecksHref && mainView === 'checks') {
291
- window.location.replace(clusterChecksHref())
316
+ if (viewTakeoverHref) {
317
+ window.location.replace(viewTakeoverHref)
292
318
  }
293
- }, [clusterChecksHref, mainView])
319
+ }, [viewTakeoverHref])
294
320
 
295
321
  const [namespaces, setNamespaces] = useState<string[]>(getInitialState().namespaces)
296
322
  // For large clusters: force SSE to reconnect with namespace filter
@@ -1318,15 +1344,14 @@ function AppInner() {
1318
1344
  // command palette (⌘K). Remove this comment to restore it.
1319
1345
  { view: 'checks' as const, icon: ShieldCheck, label: 'Checks' },
1320
1346
  ] as const)
1321
- // In Cloud, Checks is a fleet-scoped feature owned by the host's
1322
- // left rail; the per-cluster view is just that fleet queue filtered
1323
- // to this cluster, so duplicating it as a peer pill here would be a
1324
- // second "Checks" that teleports out of the cluster shell. Drop the
1325
- // Audit tab when embedded — cluster-scoped access stays available
1326
- // via the Home "Cluster Audit" card ( /audit, redirected to the
1327
- // scoped fleet Checks by the clusterChecksHref effect above), ⌘K,
1328
- // and bookmarks. Standalone OSS keeps the Audit tab.
1329
- .filter(({ view }) => !(view === 'checks' && clusterChecksHref))
1347
+ // In Cloud, fleet-shaped views (Checks, Issues, GitOps) are owned by
1348
+ // the host's left rail; the per-cluster view is just that fleet page
1349
+ // filtered to this cluster, so duplicating it as a peer pill here
1350
+ // would be a second copy that teleports out of the cluster shell.
1351
+ // Drop any pill the host took over — cluster-scoped access stays
1352
+ // available via the Home cards (redirected by the takeover effect
1353
+ // above), ⌘K, and bookmarks. Standalone OSS keeps every pill.
1354
+ .filter(({ view }) => !isViewTakenOver(view))
1330
1355
  .map(({ view, icon: Icon, label }) => (
1331
1356
  <Tooltip key={view} content={label} delay={100} position="bottom">
1332
1357
  <button
@@ -1382,8 +1407,6 @@ function AppInner() {
1382
1407
  <div className="flex items-center gap-3 shrink-0">
1383
1408
  <NamespaceSwitcher
1384
1409
  ref={namespaceSwitcherRef}
1385
- disabled={mainView === 'helm'}
1386
- disabledTooltip="Helm view always shows all namespaces"
1387
1410
  />
1388
1411
 
1389
1412
 
@@ -1611,6 +1634,15 @@ function AppInner() {
1611
1634
  navigate({ pathname: `/resources/${kind}`, search: newParams.toString() })
1612
1635
  }}
1613
1636
  onNavigateToResource={navigateFromIssue}
1637
+ // Certs has no Radar view, so it can't ride the view-redirect effect
1638
+ // above — wire the Certificate Health card straight to the host's
1639
+ // fleet Certs page (scoped to this cluster) when claimed. `assign`
1640
+ // (not replace): the user is navigating forward from a card, so this
1641
+ // belongs in history. Omitted → the card falls back to Radar's own
1642
+ // TLS-secrets resource list.
1643
+ onNavigateToCerts={
1644
+ takeover.certs ? () => window.location.assign(takeover.certs!) : undefined
1645
+ }
1614
1646
  />
1615
1647
  )}
1616
1648
 
@@ -1746,10 +1778,9 @@ function AppInner() {
1746
1778
  />
1747
1779
  )}
1748
1780
 
1749
- {/* Helm view - always show all namespaces since releases span multiple ns */}
1750
1781
  {mainView === 'helm' && (
1751
1782
  <HelmView
1752
- namespace=""
1783
+ namespaces={namespaces}
1753
1784
  selectedRelease={selectedHelmRelease}
1754
1785
  onReleaseClick={(ns, name, storageNamespace) => {
1755
1786
  setSelectedHelmRelease({ namespace: ns, name, storageNamespace })
@@ -1765,8 +1796,9 @@ function AppInner() {
1765
1796
  />
1766
1797
  )}
1767
1798
 
1768
- {/* GitOps view */}
1769
- {mainView === 'gitops' && (
1799
+ {/* GitOps view (inline only when the host hasn't taken it over — see
1800
+ the takeover splash below). */}
1801
+ {mainView === 'gitops' && !isViewTakenOver('gitops') && (
1770
1802
  <GitOpsView
1771
1803
  namespaces={namespaces}
1772
1804
  onOpenResource={(resource) => {
@@ -1796,17 +1828,21 @@ function AppInner() {
1796
1828
  <CostView onBack={() => setMainView('home')} />
1797
1829
  )}
1798
1830
 
1799
- {/* Best practices detail view. In Cloud this redirects to the host's
1800
- fleet Checks queue (clusterChecksHref effect above) — render a brief
1801
- splash instead of the single-cluster view while the cross-document
1802
- nav lands. */}
1803
- {mainView === 'checks' && clusterChecksHref && (
1831
+ {/* Takeover splash. When the host claims the current view via
1832
+ fleetTakeoverHref, the redirect effect above is mid-flight — render a
1833
+ brief splash instead of the inline view (which would flash + fire its
1834
+ own fetches) while the cross-document nav lands. Covers checks /
1835
+ issues / gitops with one block since only one view is active. */}
1836
+ {viewTakeoverHref && (
1804
1837
  <div className="flex-1 flex flex-col items-center justify-center gap-3 bg-theme-base">
1805
1838
  <img src={radarLoadingIcon} alt="" aria-hidden className="w-11 h-11" />
1806
- <p className="text-sm text-theme-text-secondary">Opening Checks…</p>
1839
+ <p className="text-sm text-theme-text-secondary">Opening…</p>
1807
1840
  </div>
1808
1841
  )}
1809
- {mainView === 'checks' && !clusterChecksHref && (
1842
+
1843
+ {/* Best practices detail view (inline only when the host hasn't taken
1844
+ Checks over — standalone OSS, or Cloud without a checks takeover). */}
1845
+ {mainView === 'checks' && !isViewTakenOver('checks') && (
1810
1846
  <AuditView
1811
1847
  namespaces={namespaces}
1812
1848
  onNavigateToResource={navigateToResourceList}
@@ -1816,8 +1852,9 @@ function AppInner() {
1816
1852
  {/* Issues — per-cluster live triage queue (hidden route: not yet in the
1817
1853
  nav `views` list; reachable at /issues). Same shared <IssuesView> the
1818
1854
  Hub fleet uses; a GitOps reconciler subject routes to its detail page,
1819
- other resources open the standard resource view. */}
1820
- {mainView === 'issues' && (
1855
+ other resources open the standard resource view. Inline only when the
1856
+ host hasn't taken it over. */}
1857
+ {mainView === 'issues' && !isViewTakenOver('issues') && (
1821
1858
  <IssuesPane
1822
1859
  namespaces={namespaces}
1823
1860
  onNavigateToResource={navigateFromIssue}
package/src/api/client.ts CHANGED
@@ -2311,11 +2311,15 @@ export function useDrainNode() {
2311
2311
  // Helm API hooks
2312
2312
  // ============================================================================
2313
2313
 
2314
+ function helmNamespaceParams(namespaces: string[] = []) {
2315
+ return namespaces.length > 0 ? `?namespaces=${namespaces.join(',')}` : ''
2316
+ }
2317
+
2314
2318
  // List all Helm releases
2315
- export function useHelmReleases(namespace?: string) {
2316
- const params = namespace ? `?namespace=${namespace}` : ''
2319
+ export function useHelmReleases(namespaces: string[] = []) {
2320
+ const params = helmNamespaceParams(namespaces)
2317
2321
  return useQuery<HelmRelease[]>({
2318
- queryKey: ['helm-releases', namespace],
2322
+ queryKey: ['helm-releases', namespaces],
2319
2323
  queryFn: () => fetchJSON(`/helm/releases${params}`),
2320
2324
  staleTime: 30000, // 30 seconds
2321
2325
  })
@@ -2393,10 +2397,10 @@ export function useHelmUpgradeInfo(namespace: string, name: string, enabled = tr
2393
2397
  }
2394
2398
 
2395
2399
  // Batch check for upgrade availability (for list view)
2396
- export function useHelmBatchUpgradeInfo(namespace?: string, enabled = true) {
2397
- const params = namespace ? `?namespace=${namespace}` : ''
2400
+ export function useHelmBatchUpgradeInfo(namespaces: string[] = [], enabled = true) {
2401
+ const params = helmNamespaceParams(namespaces)
2398
2402
  return useQuery<BatchUpgradeInfo>({
2399
- queryKey: ['helm-batch-upgrade-info', namespace],
2403
+ queryKey: ['helm-batch-upgrade-info', namespaces],
2400
2404
  queryFn: () => fetchJSON(`/helm/upgrade-check${params}`),
2401
2405
  enabled,
2402
2406
  staleTime: 30000, // 30 seconds - keep in sync with release list
@@ -15,23 +15,23 @@ import { InstallWizard } from './InstallWizard'
15
15
  type ViewTab = 'releases' | 'charts'
16
16
 
17
17
  interface HelmViewProps {
18
- namespace: string
18
+ namespaces: string[]
19
19
  selectedRelease?: SelectedHelmRelease | null
20
20
  onReleaseClick?: (namespace: string, name: string, storageNamespace?: string) => void
21
21
  }
22
22
 
23
- export function HelmView({ namespace, selectedRelease, onReleaseClick }: HelmViewProps) {
23
+ export function HelmView({ namespaces, selectedRelease, onReleaseClick }: HelmViewProps) {
24
24
  const [activeTab, setActiveTab] = useState<ViewTab>('releases')
25
25
  const [searchTerm, setSearchTerm] = useState('')
26
26
  const [selectedChart, setSelectedChart] = useState<{ repo: string; chart: string; version: string; source: ChartSource } | null>(null)
27
27
 
28
- const { data: releases, isLoading, error: releasesError, refetch: refetchReleases } = useHelmReleases(namespace || undefined)
28
+ const { data: releases, isLoading, error: releasesError, refetch: refetchReleases } = useHelmReleases(namespaces)
29
29
  const isForbidden = isForbiddenError(releasesError)
30
30
  const releasesErrorMessage = releasesError instanceof Error ? releasesError.message : 'Failed to load Helm releases'
31
31
 
32
32
  // Lazy load upgrade info after releases are loaded
33
33
  const { data: upgradeInfo, isLoading: upgradeLoading, error: upgradeError, refetch: refetchUpgradeInfo } = useHelmBatchUpgradeInfo(
34
- namespace || undefined,
34
+ namespaces,
35
35
  Boolean(releases && releases.length > 0)
36
36
  )
37
37
  const upgradeErrorMessage = upgradeError instanceof Error ? upgradeError.message : 'Upgrade checks failed'
@@ -29,9 +29,16 @@ interface HomeViewProps {
29
29
  onNavigateToView: (view: ExtendedMainView, params?: Record<string, string>) => void
30
30
  onNavigateToResourceKind: (kind: string, group?: string, filters?: Record<string, string[]>) => void
31
31
  onNavigateToResource: (resource: SelectedResource) => void
32
+ /**
33
+ * Optional override for the Certificate Health card's click. When an embedded
34
+ * host (Radar Cloud) takes Certs over with its own fleet page, it passes this
35
+ * to route there instead of Radar's TLS-secrets resource list. Omitted in
36
+ * standalone OSS → the card drills into secrets as before.
37
+ */
38
+ onNavigateToCerts?: () => void
32
39
  }
33
40
 
34
- export function HomeView({ namespaces, topology, onNavigateToView, onNavigateToResourceKind, onNavigateToResource }: HomeViewProps) {
41
+ export function HomeView({ namespaces, topology, onNavigateToView, onNavigateToResourceKind, onNavigateToResource, onNavigateToCerts }: HomeViewProps) {
35
42
  const { data, isLoading, error } = useDashboard(namespaces)
36
43
  const { data: issuesData, isLoading: issuesLoading, isFetching: issuesFetching, error: issuesError } = useIssues(namespaces)
37
44
  const issues = issuesData?.issues ?? []
@@ -165,7 +172,7 @@ export function HomeView({ namespaces, topology, onNavigateToView, onNavigateToR
165
172
  <BandItem>
166
173
  <CertificateHealthCard
167
174
  data={data.certificateHealth}
168
- onNavigate={() => onNavigateToResourceKind('secrets', undefined, { type: ['TLS'] })}
175
+ onNavigate={onNavigateToCerts ?? (() => onNavigateToResourceKind('secrets', undefined, { type: ['TLS'] }))}
169
176
  />
170
177
  </BandItem>
171
178
  )}
@@ -13,6 +13,14 @@
13
13
  import { createContext, useContext } from 'react';
14
14
  import type { ReactNode } from 'react';
15
15
 
16
+ /**
17
+ * Per-cluster destinations an embedded host can take over with its own
18
+ * fleet-scoped pages. See `fleetTakeoverHref`. 'issues' | 'gitops' | 'checks'
19
+ * are also Radar view names (so route entry redirects too); 'certs' is
20
+ * card-only (Radar has no certs view).
21
+ */
22
+ export type FleetTakeoverTarget = 'issues' | 'gitops' | 'checks' | 'certs';
23
+
16
24
  interface NavCustomizationBase {
17
25
  /** Replaces Radar's Skyhook/radar logo + wordmark. */
18
26
  brandSlot?: ReactNode;
@@ -32,15 +40,32 @@ interface NavCustomizationBase {
32
40
  group?: string;
33
41
  }) => string;
34
42
  /**
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.
43
+ * Lets an embedded host (e.g. Radar Cloud) take over selected per-cluster
44
+ * destinations with its OWN fleet pages scoped to this cluster, instead of
45
+ * Radar rendering them inline. Given a semantic target the host returns the
46
+ * URL to navigate to, or `undefined`/omits the hook to let Radar render its
47
+ * own view as usual (standalone OSS does the latter for everything).
48
+ *
49
+ * This is how the Home dashboard's "fleet-shaped" cards reach the host's
50
+ * canonical surfaces rather than a second, diverging per-cluster copy:
51
+ * - 'issues' → the Active Issues panel + cluster-health issues count
52
+ * - 'gitops' → the GitOps controllers card
53
+ * - 'checks' → the Cluster Audit card (and any route to /audit; legacy
54
+ * `clusterChecksHref` folded in here)
55
+ * - 'certs' → the Certificate Health card
56
+ *
57
+ * View-shaped targets (issues / gitops / checks) are also honored for any
58
+ * entry into that view — ⌘K, bookmarks, deep links — via a redirect effect
59
+ * in App.tsx, using window.location.replace so the transient /<view> URL
60
+ * stays out of history. 'certs' has no Radar view, so only the card consults
61
+ * it (window.location.assign — a real forward navigation the user initiated).
62
+ */
63
+ fleetTakeoverHref?: (target: FleetTakeoverTarget) => string | undefined;
64
+ /**
65
+ * @deprecated Superseded by `fleetTakeoverHref('checks')`. Kept so consumers
66
+ * still on the pre-1.7 hook keep working (App.tsx folds it into the 'checks'
67
+ * target) — this makes adding `fleetTakeoverHref` an additive, non-breaking
68
+ * change. Remove in a major release once all consumers have migrated.
44
69
  */
45
70
  clusterChecksHref?: () => string;
46
71
  /**
package/src/index.ts CHANGED
@@ -14,7 +14,7 @@ export {
14
14
  getAuthHeaders,
15
15
  getCredentialsMode,
16
16
  } from './api/config';
17
- export type { NavCustomization } from './context/NavCustomization';
17
+ export type { NavCustomization, FleetTakeoverTarget } from './context/NavCustomization';
18
18
  export { ShortcutHelpOverlay } from './components/ui/ShortcutHelpOverlay';
19
19
 
20
20
  // Shared cluster-switcher primitive — re-exported from @skyhook-io/k8s-ui so