@skyhook-io/radar-app 1.14.5 → 1.14.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.md +2 -9
  2. package/package.json +6 -6
  3. package/src/App.tsx +91 -328
  4. package/src/RadarApp.tsx +3 -8
  5. package/src/api/client.jobset.test.ts +18 -0
  6. package/src/api/client.resource-identity.test.ts +44 -0
  7. package/src/api/client.rightsizing.test.ts +7 -3
  8. package/src/api/client.ts +233 -78
  9. package/src/api/preferences.test.ts +54 -0
  10. package/src/api/preferences.ts +38 -0
  11. package/src/components/CloudFunnelButton.test.tsx +155 -0
  12. package/src/components/CloudFunnelButton.tsx +14 -8
  13. package/src/components/ContextSwitcher.tsx +13 -132
  14. package/src/components/applications/ApplicationsView.identity.test.ts +39 -0
  15. package/src/components/applications/ApplicationsView.tsx +5 -3
  16. package/src/components/audit/AuditSettingsDialog.tsx +15 -11
  17. package/src/components/diagnose/AgentControls.tsx +26 -3
  18. package/src/components/diagnose/DiagnoseContext.tsx +1 -0
  19. package/src/components/diagnose/agentCatalog.ts +6 -0
  20. package/src/components/diagnose/launch.test.ts +23 -0
  21. package/src/components/diagnose/launch.ts +4 -0
  22. package/src/components/diagnose/parts.test.tsx +18 -0
  23. package/src/components/dock/NodeTerminalTab.test.tsx +34 -0
  24. package/src/components/dock/NodeTerminalTab.tsx +5 -3
  25. package/src/components/dock/WorkloadLogsTab.test.tsx +66 -0
  26. package/src/components/dock/WorkloadLogsTab.tsx +2 -1
  27. package/src/components/execution/BatchExecutionView.render.test.tsx +2 -1
  28. package/src/components/execution/BatchExecutionView.test.ts +52 -1
  29. package/src/components/execution/BatchExecutionView.tsx +324 -65
  30. package/src/components/execution/JobSetAdmission.test.tsx +42 -0
  31. package/src/components/execution/JobSetAdmission.tsx +17 -0
  32. package/src/components/execution/JobSetMemberComparison.test.tsx +30 -0
  33. package/src/components/execution/JobSetMemberComparison.tsx +88 -0
  34. package/src/components/execution/batch-run-actions.test.ts +1 -0
  35. package/src/components/execution/batch-timeline.test.ts +3 -0
  36. package/src/components/execution/execution-definition.test.ts +15 -1
  37. package/src/components/execution/execution-definition.ts +4 -0
  38. package/src/components/execution/member-collection.render.test.tsx +142 -0
  39. package/src/components/gitops/GitOpsView.tsx +33 -2
  40. package/src/components/gitops/destination-toast.test.ts +33 -0
  41. package/src/components/gitops/destination-toast.ts +46 -0
  42. package/src/components/gitops/useDestinationCluster.ts +30 -0
  43. package/src/components/helm/TrackChartSourceDialog.tsx +20 -11
  44. package/src/components/home/MCPSetupDialog.tsx +10 -0
  45. package/src/components/logs/ScheduledWorkloadLogsViewer.tsx +75 -22
  46. package/src/components/logs/WorkloadLogsViewer.tsx +7 -4
  47. package/src/components/nav/PrimaryNavRail.tsx +2 -5
  48. package/src/components/resource/PVCUsageBar.render.test.tsx +168 -0
  49. package/src/components/resource/PVCUsageBar.tsx +62 -16
  50. package/src/components/resource/PrometheusChartsGrid.render.test.tsx +1 -1
  51. package/src/components/resources/ResourcesView.tsx +3 -3
  52. package/src/components/resources/renderers/CAPIClusterRenderer.tsx +8 -0
  53. package/src/components/resources/renderers/KueueWorkloadRenderer.test.tsx +39 -0
  54. package/src/components/resources/renderers/KueueWorkloadRenderer.tsx +37 -0
  55. package/src/components/resources/renderers/RayClusterRenderer.test.tsx +18 -0
  56. package/src/components/resources/renderers/RayClusterRenderer.tsx +17 -0
  57. package/src/components/resources/renderers/RayServiceRenderer.test.tsx +69 -0
  58. package/src/components/resources/renderers/RayServiceRenderer.tsx +38 -0
  59. package/src/components/rightsizing/RightsizingScanView.tsx +34 -22
  60. package/src/components/rightsizing/copy.test.ts +7 -0
  61. package/src/components/rightsizing/model.test.ts +2 -1
  62. package/src/components/rightsizing/notices.test.tsx +4 -3
  63. package/src/components/settings/OperatorManagedNotice.tsx +17 -0
  64. package/src/components/settings/SettingsDialog.tsx +157 -42
  65. package/src/components/settings/settings-state.test.ts +18 -0
  66. package/src/components/settings/settings-state.ts +18 -0
  67. package/src/components/timeline/TimelineView.tsx +9 -3
  68. package/src/components/timeline/TimelineView.urlparams.test.ts +18 -1
  69. package/src/components/ui/command-items.ts +0 -3
  70. package/src/components/useContextSwitchFlow.tsx +147 -0
  71. package/src/components/workload/WorkloadView.test.ts +17 -1
  72. package/src/components/workload/WorkloadView.tsx +36 -18
  73. package/src/context/ContextSwitchContext.tsx +18 -2
  74. package/src/context/NavCustomization.tsx +4 -58
  75. package/src/context/ThemeContext.tsx +3 -11
  76. package/src/hooks/useClusterLoadState.ts +2 -2
  77. package/src/hooks/useFavorites.ts +3 -5
  78. package/src/utils/auditBadges.ts +1 -1
  79. package/src/utils/navigation.test.ts +18 -1
  80. package/src/utils/navigation.ts +12 -5
  81. package/src/utils/topology-selection.test.ts +56 -0
  82. package/src/utils/topology-selection.ts +9 -9
  83. package/src/components/ui/CommandPalette.tsx +0 -261
package/src/App.tsx CHANGED
@@ -9,8 +9,9 @@ import { DebugOverlay } from './components/DebugOverlay'
9
9
  import { GlobalDiagnoseButton } from './components/diagnose/LocalDiagnoseAction'
10
10
  import { investigationWorkspaceSearch, isInvestigationWorkspacePath, useDiagnoseLayout } from './components/diagnose/DiagnoseContext'
11
11
  import { DiagnoseSurface } from './components/diagnose/DiagnoseSurface'
12
- import { TopologyGraph, TopologySearch, TopologyBreadcrumb, TopologyFilterSidebar, TopologyControls, FreshnessControl, gitOpsRouteForKind, gitOpsRouteForResource, ScopePill, PaneLoader, assetUrl } from '@skyhook-io/k8s-ui'
12
+ import { TopologyGraph, TopologySearch, TopologyBreadcrumb, TopologyFilterSidebar, TopologyControls, FreshnessControl, gitOpsRouteForKind, gitOpsRouteForResource, ScopePill, PaneLoader } from '@skyhook-io/k8s-ui'
13
13
  import { initNavigationMap } from '@skyhook-io/k8s-ui/utils/navigation'
14
+ import { topologyNodeResourceKind } from '@skyhook-io/k8s-ui/utils/topology-neighborhood'
14
15
  import { useAPIResources, findAPIResourceForRoute } from './api/apiResources'
15
16
  import { TimelineView } from './components/timeline/TimelineView'
16
17
  import { ResourcesView } from './components/resources/ResourcesView'
@@ -49,7 +50,6 @@ import { UserMenu } from './components/UserMenu'
49
50
  import { ErrorBoundary } from './components/ui/ErrorBoundary'
50
51
  import { UpdateNotification } from './components/ui/UpdateNotification'
51
52
  import { ShortcutHelpOverlay } from './components/ui/ShortcutHelpOverlay'
52
- import { CommandPalette } from './components/ui/CommandPalette'
53
53
  import { DiagnosticsOverlay } from './components/ui/DiagnosticsOverlay'
54
54
  import { useEventSource } from './hooks/useEventSource'
55
55
  import { debugNamespaceLog, useNamespaces, useNamespaceScope, useSetActiveNamespace, useSwitchContext, useAuthMe, useAudit } from './api/client'
@@ -61,7 +61,7 @@ import { useAnimatedUnmount } from './hooks/useAnimatedUnmount'
61
61
  import { useDocumentTitle } from './hooks/useDocumentTitle'
62
62
  import type { ClusterLoadState } from './types/clusterLoadState'
63
63
  import { useClusterLoadState } from './hooks/useClusterLoadState'
64
- import { Network, List, Clock, Package, Sun, Moon, Activity, Home, Star, Search, Bug, SquareTerminal, ShieldCheck, GitBranch, Gauge, HelpCircle, Loader2, RefreshCw } from 'lucide-react'
64
+ import { Network, Sun, Moon, Star, Bug, SquareTerminal, HelpCircle, Loader2, RefreshCw } from 'lucide-react'
65
65
  import { useTheme } from './context/ThemeContext'
66
66
  import { Tooltip } from './components/ui/Tooltip'
67
67
  import { LargeClusterNamespacePicker } from './components/shared/LargeClusterNamespacePicker'
@@ -73,8 +73,6 @@ import { type OmnibarHandle } from './components/ui/Omnibar'
73
73
  import { RadarOmnibar } from './components/ui/RadarOmnibar'
74
74
  import type { ContextSwitcherHandle } from './components/ContextSwitcher'
75
75
 
76
- const radarLogoUrl = assetUrl('/images/radar/radar-icon.svg')
77
-
78
76
  // All possible node kinds (core + GitOps)
79
77
  const ALL_NODE_KINDS: NodeKind[] = [
80
78
  'Internet', 'Ingress', 'Gateway', 'HTTPRoute', 'GRPCRoute', 'TCPRoute', 'TLSRoute',
@@ -343,30 +341,23 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
343
341
  [navCustomization],
344
342
  )
345
343
  // Resolve every host-takeover URL ONCE (memoized on navCustomization) so the
346
- // setMainView intercept, redirect effect, nav-pill filtering, inline-view
347
- // gating, and the cert click handler all consume the SAME value — host
344
+ // setMainView intercept, redirect effect, inline-view gating, and the cert
345
+ // click handler all consume the SAME value — host
348
346
  // callbacks aren't guaranteed idempotent (scope / flags / signed URLs can
349
347
  // shift between calls). undefined = not taken over → Radar renders the view
350
- // itself. `clusterChecksHref` is the deprecated pre-1.7 hook, folded into the
351
- // 'checks' target for back-compat.
348
+ // itself.
352
349
  const takeover: Record<FleetTakeoverTarget, string | undefined> = useMemo(
353
350
  () => ({
354
351
  issues: navCustomization.fleetTakeoverHref?.('issues'),
355
352
  gitops: navCustomization.fleetTakeoverHref?.('gitops'),
356
- checks: navCustomization.fleetTakeoverHref?.('checks') ?? navCustomization.clusterChecksHref?.(),
353
+ checks: navCustomization.fleetTakeoverHref?.('checks'),
357
354
  certs: navCustomization.fleetTakeoverHref?.('certs'),
358
355
  }),
359
356
  [navCustomization],
360
357
  )
361
358
  const { pinned: navRailPinned, togglePinned: toggleNavRailPinned } = useNavRailPinned()
362
- // Standalone Radar gets the left nav rail; embedded hosts (Radar Hub) own
363
- // the left chrome via their own fleet rail and keep Radar's top-bar pills.
364
- const showNavRail = !navCustomization.embedded
365
- // Chromeless embed: the host (Radar Hub) owns ALL chrome and drives view
366
- // navigation + scope from its own UI, so Radar renders just the active view's
367
- // content — no top bar, no view-switcher. Used for per-cluster views surfaced
368
- // as native cloud destinations behind a cluster picker.
369
- const chromeless = navCustomization.embedded === true && navCustomization.chrome === 'none'
359
+ const embedded = navCustomization.embedded === true
360
+ const showNavRail = !embedded
370
361
  // Force the slim rail on narrow windows: a pinned 176px rail needs viewport
371
362
  // ≥976 to keep content above its ~800px floor (collapsed needs only ≥856).
372
363
  // Below 976 we render collapsed regardless of the pin preference — a
@@ -513,15 +504,14 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
513
504
  }, [diagnoseOpen, diagnoseMaximized, dismissDiagnoseForNavigation, setMainView])
514
505
 
515
506
  // Cloud (embedded) takes over the "fleet-shaped" per-cluster views with its
516
- // own fleet pages scoped to this cluster — owned by the host's left rail — so
517
- // Radar drops the matching pills (see the nav below). In-app nav hands off in
507
+ // own fleet pages scoped to this cluster. In-app navigation hands off in
518
508
  // setMainView (above); direct /<view> URL entry funnels through the redirect
519
509
  // effect below. Both consume the memoized `takeover` resolved above. Standalone
520
510
  // OSS (no fleetTakeoverHref) is unaffected and renders the in-app view.
521
511
  //
522
512
  // Has the host claimed this view? View-shaped targets only ('certs' has no
523
- // Radar view — only its Home card consults `takeover`). Used to drop the nav
524
- // pill and gate the inline view render in favor of the "Opening…" splash.
513
+ // Radar view — only its Home card consults `takeover`). Used to gate the
514
+ // inline view render in favor of the "Opening…" splash.
525
515
  const isViewTakenOver = (view: ExtendedMainView): boolean =>
526
516
  (view === 'issues' || view === 'gitops' || view === 'checks') && !!takeover[view]
527
517
  // The host's URL for the CURRENT view, if taken over. Drives the redirect
@@ -565,9 +555,6 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
565
555
  // Help overlay state
566
556
  const [showHelp, setShowHelp] = useState(false)
567
557
 
568
- // Command palette state
569
- const [showCommandPalette, setShowCommandPalette] = useState(false)
570
-
571
558
  // Settings dialog state
572
559
  const [showSettings, setShowSettings] = useState(false)
573
560
  const [settingsSection, setSettingsSection] = useState<SettingsSectionId>('overview')
@@ -683,7 +670,6 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
683
670
  // Dialog-kind overlays wait their exit duration (shorter than the entrance);
684
671
  // drawers keep the symmetric slide.
685
672
  const helpOverlay = useAnimatedUnmount(showHelp, overlayExitMs('dialog'))
686
- const commandPaletteAnim = useAnimatedUnmount(showCommandPalette, overlayExitMs('dialog'))
687
673
  const diagnosticsOverlay = useAnimatedUnmount(showDiagnostics, overlayExitMs('dialog'))
688
674
 
689
675
  // Hold last valid values so drawers can animate out before data disappears
@@ -830,7 +816,6 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
830
816
  // Theme toggle for keyboard shortcut
831
817
  const { toggleTheme } = useTheme()
832
818
 
833
- // Context switching for command palette
834
819
  const switchContext = useSwitchContext()
835
820
 
836
821
  // Refs for dropdown components to trigger them via shortcuts
@@ -909,10 +894,8 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
909
894
  category: 'General' as const,
910
895
  scope: 'global' as const,
911
896
  allowInInputs: true,
912
- // Standalone focuses the top-center omnibar; embedded opens the modal. In
913
- // a chromeless embed the HOST owns ⌘K (its own omnibar), so do nothing —
914
- // otherwise both the host omnibar and Radar's palette fire on one ⌘K.
915
- handler: () => { if (showNavRail) omnibarRef.current?.focus(); else if (!chromeless) setShowCommandPalette(true) },
897
+ // Radar Hub owns its own omnibar.
898
+ handler: () => { if (!embedded) omnibarRef.current?.focus() },
916
899
  },
917
900
  {
918
901
  id: 'diagnostics',
@@ -981,7 +964,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
981
964
  const { data: namespaceScope } = useNamespaceScope()
982
965
 
983
966
  // Context switch state
984
- const { isSwitching, targetContext, progressMessage, updateProgress, endSwitch } = useContextSwitch()
967
+ const { isSwitching, targetContext, progressMessage, updateProgress, endSwitch, takeOpenAfterSwitch } = useContextSwitch()
985
968
 
986
969
  // Connection state (for graceful startup)
987
970
  const { connection, retry: retryConnection, isRetrying, updateFromSSE: updateConnectionFromSSE } = useConnection()
@@ -1008,7 +991,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
1008
991
  const { clusterLoadState, showHomeClusterLoadFallback, clusterLoadInitial } = useClusterLoadState({
1009
992
  namespaces,
1010
993
  mainView,
1011
- chromeless,
994
+ chromeless: embedded,
1012
995
  contentReady,
1013
996
  onClusterLoadStateChange,
1014
997
  })
@@ -1158,7 +1141,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
1158
1141
  const { topology, connected: eventStreamConnected, connecting: eventStreamConnecting, reconnect: reconnectEventStream } = useEventSource(namespaces, sseMode as 'resources' | 'traffic', {
1159
1142
  onContextSwitchComplete: endSwitch,
1160
1143
  onContextSwitchProgress: updateProgress,
1161
- onContextChanged: () => {
1144
+ onContextChanged: (context) => {
1162
1145
  // Clear all React Query caches when cluster context changes
1163
1146
  // This ensures helm releases, resources, etc. are refetched from the new cluster
1164
1147
  // removeQueries clears cached data, invalidateQueries triggers refetch
@@ -1181,7 +1164,16 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
1181
1164
  // Reset resource-specific params while retaining the durable investigation
1182
1165
  // focus. Diagnose resolves runs by id and owns whether the focused run is
1183
1166
  // still readable after the context switch.
1184
- if (isInvestigationWorkspacePath(location.pathname)) {
1167
+ const openAfter = takeOpenAfterSwitch(context)
1168
+ if (openAfter) {
1169
+ // The switch was made to open this resource on the new cluster; the
1170
+ // page it was requested from belongs to the old one.
1171
+ setSelectedResource(openAfter)
1172
+ const params = new URLSearchParams()
1173
+ params.set('resource', openAfter.namespace ? `${openAfter.namespace}/${openAfter.name}` : openAfter.name)
1174
+ if (openAfter.group) params.set('apiGroup', openAfter.group)
1175
+ navigate({ pathname: `/resources/${openAfter.kind}`, search: params.toString() }, { replace: true })
1176
+ } else if (isInvestigationWorkspacePath(location.pathname)) {
1185
1177
  navigate(
1186
1178
  {
1187
1179
  pathname: location.pathname,
@@ -1321,6 +1313,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
1321
1313
  if (node.kind === 'Internet') return
1322
1314
 
1323
1315
  const nodeGroup = apiVersionToGroup(node.data.apiVersion as string | undefined)
1316
+ const resourceKind = topologyNodeResourceKind(node)
1324
1317
  // Radar's topology PodGroup is a virtual pod aggregate. A Kubernetes
1325
1318
  // scheduling.k8s.io PodGroup carries apiVersion and is a real resource.
1326
1319
  if (node.kind === 'PodGroup' && !nodeGroup) return
@@ -1330,14 +1323,14 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
1330
1323
  // detail page with tree + insights + ops that the drawer can't reproduce.
1331
1324
  // Route there from the main topology when the node is one of those kinds;
1332
1325
  // everything else falls back to the drawer.
1333
- const gitOpsPath = gitOpsRouteForKind(node.kind, namespace, node.name)
1326
+ const gitOpsPath = gitOpsRouteForKind(resourceKind, namespace, node.name, nodeGroup, true)
1334
1327
  if (gitOpsPath) {
1335
1328
  navigate(gitOpsPath)
1336
1329
  return
1337
1330
  }
1338
1331
 
1339
1332
  navigateToResource({
1340
- kind: kindToPluralWithGroup(node.kind, nodeGroup),
1333
+ kind: kindToPluralWithGroup(resourceKind, nodeGroup),
1341
1334
  namespace,
1342
1335
  name: node.name,
1343
1336
  group: nodeGroup,
@@ -1752,46 +1745,20 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
1752
1745
  − nav rail), not the viewport, so it collapses gracefully on narrow windows.
1753
1746
  The AI panel docks BELOW the navbar and pushes only the content region, so the
1754
1747
  navbar is never squeezed by it — these thresholds react to real window width. */}
1755
- {!chromeless && (
1748
+ {!embedded && (
1756
1749
  <header className="@container relative z-50 flex items-center justify-between px-4 py-2 bg-theme-base/90 backdrop-blur-sm border-b border-theme-border/50">
1757
- {/* Left: Logo + Cluster info. In the standalone (nav-rail) layout this
1758
- is a FIXED-WIDTH column so the omnibar after it is force-pinned: the
1759
- scope pill + status dot can change width (cluster/namespace value)
1760
- without ever shifting the search box. The pill's own name/value caps
1761
- keep it inside this width; the embedded/pill layout keeps auto width
1762
- (its center bar is absolutely positioned). */}
1763
- <div className={`flex items-center gap-4 shrink-0 ${showNavRail ? 'w-[492px]' : ''}`}>
1764
- {/* Standalone rail owns the brand; only the embedded/pill layout
1765
- shows it in the header (host may override via brandSlot). */}
1766
- {navCustomization.brandSlot ?? (showNavRail ? null : <Logo />)}
1767
-
1768
- <div className={`flex items-center gap-2 min-w-0 ${showNavRail ? 'flex-1' : ''}`}>
1769
- {navCustomization.contextSlot ? (
1770
- // Embedded host supplies its own cluster switcher — keep the two
1771
- // controls separate (the host owns the cluster chip's styling).
1772
- <>
1773
- {navCustomization.contextSlot}
1774
- <NamespaceSwitcher
1775
- ref={namespaceSwitcherRef}
1776
- disabled={namespaceFilter.disabled}
1777
- disabledTooltip={namespaceFilter.tooltip}
1778
- />
1779
- </>
1780
- ) : (
1781
- // Standalone: cluster + namespace as one "scope" pill — two
1782
- // borderless segments split by a divider, reading as a single
1783
- // "what am I looking at" unit. The shared ScopePill shell is the
1784
- // same one Radar Hub's cluster top bar uses, so the two match.
1785
- <ScopePill>
1786
- <ContextSwitcher ref={contextSwitcherRef} variant="segment" />
1787
- <NamespaceSwitcher
1788
- ref={namespaceSwitcherRef}
1789
- variant="segment"
1790
- disabled={namespaceFilter.disabled}
1791
- disabledTooltip={namespaceFilter.tooltip}
1792
- />
1793
- </ScopePill>
1794
- )}
1750
+ {/* Fixed width keeps the omnibar steady as cluster and namespace names change. */}
1751
+ <div className="flex items-center gap-4 shrink-0 w-[492px]">
1752
+ <div className="flex items-center gap-2 min-w-0 flex-1">
1753
+ <ScopePill>
1754
+ <ContextSwitcher ref={contextSwitcherRef} variant="segment" />
1755
+ <NamespaceSwitcher
1756
+ ref={namespaceSwitcherRef}
1757
+ variant="segment"
1758
+ disabled={namespaceFilter.disabled}
1759
+ disabledTooltip={namespaceFilter.tooltip}
1760
+ />
1761
+ </ScopePill>
1795
1762
  {/* Connection status — a fixed-size dot (state in the tooltip), an
1796
1763
  optional reconnect button, and when the header is wide enough
1797
1764
  (xl+), a label. The label is nowrap and unbounded: it overflows
@@ -1817,7 +1784,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
1817
1784
  }`}
1818
1785
  />
1819
1786
  </Tooltip>
1820
- {showNavRail && headerConnectionDisplayLabel && (
1787
+ {headerConnectionDisplayLabel && (
1821
1788
  <span className="hidden xl:flex items-center gap-1.5 whitespace-nowrap text-[11px] text-theme-text-tertiary">
1822
1789
  {showClusterWarmupLabel && <Loader2 className="w-3 h-3 animate-spin" />}
1823
1790
  {headerConnectionDisplayLabel}
@@ -1842,65 +1809,6 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
1842
1809
  </div>
1843
1810
  </div>
1844
1811
 
1845
- {/* Center: View tabs — embedded/pill layout only. Standalone Radar
1846
- navigates via the left rail (showNavRail), so the pill bar is
1847
- suppressed there to avoid a duplicate primary nav. */}
1848
- {!showNavRail && (
1849
- <div className="@min-[920px]:absolute @min-[920px]:left-1/2 @min-[920px]:-translate-x-1/2 flex items-center gap-0.5 bg-theme-elevated/50 rounded-full p-1 ml-2 @min-[920px]:ml-0">
1850
- {([
1851
- { view: 'home' as const, icon: Home, label: 'Home' },
1852
- { view: 'topology' as const, icon: Network, label: 'Topology' },
1853
- { view: 'resources' as const, icon: List, label: 'Resources' },
1854
- { view: 'capacity' as const, icon: Gauge, label: 'Capacity' },
1855
- { view: 'timeline' as const, icon: Clock, label: 'Timeline' },
1856
- { view: 'helm' as const, icon: Package, label: 'Helm' },
1857
- { view: 'gitops' as const, icon: GitBranch, label: 'GitOps' },
1858
- // Applications is intentionally hidden from the pill bar for now —
1859
- // the bar is full, and the view's primary home is Cloud's fleet
1860
- // rail. The view still exists and is reachable via /applications
1861
- // and the view-switching shortcuts. Same treatment as Cost below.
1862
- { view: 'traffic' as const, icon: Activity, label: 'Live Traffic' },
1863
- // Cost is intentionally hidden from the pill bar for now — the view still
1864
- // exists and is reachable via /cost, the Home dashboard card, and the
1865
- // command palette (⌘K). Remove this comment to restore it.
1866
- { view: 'checks' as const, icon: ShieldCheck, label: 'Checks' },
1867
- ] as const)
1868
- // In Cloud, fleet-shaped views (Checks, Issues, GitOps) are owned by
1869
- // the host's left rail; the per-cluster view is just that fleet page
1870
- // filtered to this cluster, so duplicating it as a peer pill here
1871
- // would be a second copy that teleports out of the cluster shell.
1872
- // Drop any pill the host took over — cluster-scoped access stays
1873
- // available via the Home cards (redirected by the takeover effect
1874
- // above), ⌘K, and bookmarks. Standalone OSS keeps every pill.
1875
- .filter(({ view }) => !isViewTakenOver(view))
1876
- .map(({ view, icon: Icon, label }) => (
1877
- <Tooltip key={view} content={label} delay={100} position="bottom">
1878
- <button
1879
- onClick={() => setMainView(view)}
1880
- className={`flex items-center gap-1 px-2 py-1 text-[13px] rounded-full transition-colors ${
1881
- mainView === view || (mainView === 'helmCompare' && view === 'helm')
1882
- ? 'bg-skyhook-600 dark:bg-skyhook-500 text-white shadow-glow-brand-sm'
1883
- : 'text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-hover'
1884
- }`}
1885
- >
1886
- <Icon className="w-4 h-4" />
1887
- {/* Labels appear only when the absolute-centered nav has
1888
- enough horizontal room past the left section. Right-side
1889
- chrome that adds further pressure (Connected text, star
1890
- count) is intentionally pushed to the next tier (xl) so
1891
- label rendering and right-side expansion stay decoupled.
1892
- Per-button Tooltip discloses labels on hover when the
1893
- icon-only viewport is in effect. The 1440 anchor is an
1894
- off-system breakpoint chosen by measurement at the time
1895
- of this PR — recompute if the cluster switcher cap or
1896
- other left-section chrome changes appreciably. */}
1897
- <span className="hidden @min-[1264px]:inline">{label}</span>
1898
- </button>
1899
- </Tooltip>
1900
- ))}
1901
- </div>
1902
- )}
1903
-
1904
1812
  {/* Center: omnibar — standalone search + command surface (the ⌘K entry).
1905
1813
  Its container is one of three EQUAL flex-1 columns (see the left/right
1906
1814
  groups): equal side columns keep this middle column — and the search
@@ -1908,60 +1816,37 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
1908
1816
  chrome gets (cluster name, namespace label, "Discovering…" /
1909
1817
  "Disconnected" text). Overflowing side content truncates instead of
1910
1818
  dragging the box. Same pattern as Radar Hub's ClusterTopBar. */}
1911
- {showNavRail && (
1912
- <div className="hidden @min-[720px]:flex flex-1 justify-center min-w-0 px-3">
1913
- <RadarOmnibar
1914
- ref={omnibarRef}
1915
- onNavigateView={(view) => setMainView(view)}
1916
- onNavigateKind={(kind, group) => {
1917
- const params = new URLSearchParams(searchParams)
1918
- params.delete('kind')
1919
- if (group) params.set('apiGroup', group); else params.delete('apiGroup')
1920
- params.delete('resource')
1921
- params.delete('full')
1922
- params.delete('tab')
1923
- navigate({ pathname: `/resources/${kind}`, search: params.toString() })
1924
- }}
1925
- onSwitchContext={(name) => switchContext.mutate({ name }, { onSettled: () => setNamespaces([]) })}
1926
- onSetNamespaces={(ns) => { setNamespaces(ns); setActiveNamespace.mutate({ namespaces: ns }) }}
1927
- onToggleTheme={toggleTheme}
1928
- onShowDiagnostics={() => setShowDiagnostics(true)}
1929
- onOpenResource={(hit) => navigateToResourceList(searchHitToSelectedResource(hit))}
1930
- />
1931
- </div>
1932
- )}
1819
+ <div className="hidden @min-[720px]:flex flex-1 justify-center min-w-0 px-3">
1820
+ <RadarOmnibar
1821
+ ref={omnibarRef}
1822
+ onNavigateView={(view) => setMainView(view)}
1823
+ onNavigateKind={(kind, group) => {
1824
+ const params = new URLSearchParams(searchParams)
1825
+ params.delete('kind')
1826
+ if (group) params.set('apiGroup', group); else params.delete('apiGroup')
1827
+ params.delete('resource')
1828
+ params.delete('full')
1829
+ params.delete('tab')
1830
+ navigate({ pathname: `/resources/${kind}`, search: params.toString() })
1831
+ }}
1832
+ onSwitchContext={(name) => switchContext.mutate({ name }, { onSettled: () => setNamespaces([]) })}
1833
+ onSetNamespaces={(ns) => { setNamespaces(ns); setActiveNamespace.mutate({ namespaces: ns }) }}
1834
+ onToggleTheme={toggleTheme}
1835
+ onShowDiagnostics={() => setShowDiagnostics(true)}
1836
+ onOpenResource={(hit) => navigateToResourceList(searchHitToSelectedResource(hit))}
1837
+ />
1838
+ </div>
1933
1839
 
1934
1840
  {/* Right: Controls */}
1935
1841
  <div className="flex items-center gap-3 shrink-0">
1936
- {/* Command palette trigger — embedded only; standalone has the
1937
- top-center omnibar (which is the ⌘K surface). */}
1938
- {!showNavRail && (
1939
- <button
1940
- onClick={() => setShowCommandPalette(true)}
1941
- aria-label="Open command palette"
1942
- className="hidden lg:flex items-center gap-2 h-7 px-2.5 rounded-md bg-theme-elevated hover:bg-theme-hover text-theme-text-secondary hover:text-theme-text-primary transition-colors"
1943
- >
1944
- <Search className="w-3.5 h-3.5" />
1945
- <kbd className="text-[10px] text-theme-text-tertiary bg-theme-surface px-1 py-0.5 rounded border border-theme-border-light">
1946
- {typeof navigator !== 'undefined' && navigator.platform.includes('Mac') ? '⌘' : 'Ctrl+'}K
1947
- </kbd>
1948
- </button>
1949
- )}
1950
-
1951
- {/* GitHub star — hidden in embedded mode (not OSS-distribution chrome). */}
1952
- {!navCustomization.embedded && (
1953
- <div className="hidden @min-[1100px]:block">
1954
- <GitHubStarButton />
1955
- </div>
1956
- )}
1842
+ <div className="hidden @min-[1100px]:block">
1843
+ <GitHubStarButton />
1844
+ </div>
1957
1845
 
1958
1846
  {/* AI investigations (self-hides when no agent CLI is present) */}
1959
1847
  <GlobalDiagnoseButton />
1960
1848
 
1961
- {/* Radar Cloud funnel — OSS-only chrome, same gate as the star.
1962
- Cloud embeds render chromeless anyway; the explicit gate is
1963
- belt-and-braces for future chrome-bearing embedders. */}
1964
- {!navCustomization.embedded && <CloudFunnelButton />}
1849
+ <CloudFunnelButton />
1965
1850
 
1966
1851
  {/* Local terminal */}
1967
1852
  {capabilities.localTerminal && (
@@ -1976,50 +1861,28 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
1976
1861
  </Tooltip>
1977
1862
  )}
1978
1863
 
1979
- {/* Theme toggle — hidden in embedded mode. Host apps (e.g. Radar
1980
- Cloud) own the user-theme preference and mount their own picker
1981
- in the account menu; a second toggle in Radar's topbar would
1982
- fight them (one writes to Radar's localStorage key, the other
1983
- to the host's cookie/backend) and the user would see the theme
1984
- bounce on every navigation between host routes and /c/:id. */}
1985
- {!navCustomization.embedded && (
1986
- <div className="hidden @min-[920px]:flex items-center">
1987
- <ThemeToggle />
1988
- </div>
1989
- )}
1990
-
1991
- {/* Help + Report-a-bug — standalone only (the left rail owns chrome;
1992
- embedded hosts provide their own help/support). These replace the
1993
- old floating bottom-right pair. Settings moved to the rail bottom. */}
1994
- {showNavRail && (
1995
- <>
1996
- <Tooltip content="Keyboard shortcuts (?)">
1997
- <button
1998
- onClick={() => setShowHelp(true)}
1999
- aria-label="Show keyboard shortcuts"
2000
- className="p-1.5 rounded-md bg-theme-elevated hover:bg-theme-hover text-theme-text-secondary hover:text-theme-text-primary transition-colors"
2001
- >
2002
- <HelpCircle className="w-4 h-4" />
2003
- </button>
2004
- </Tooltip>
2005
- <Tooltip content="Report a bug / Diagnostics">
2006
- <button
2007
- onClick={() => setShowDiagnostics(true)}
2008
- aria-label="Open diagnostics"
2009
- className="p-1.5 rounded-md bg-theme-elevated hover:bg-theme-hover text-theme-text-secondary hover:text-theme-text-primary transition-colors"
2010
- >
2011
- <Bug className="w-4 h-4" />
2012
- </button>
2013
- </Tooltip>
2014
- </>
2015
- )}
2016
-
2017
- {/* Account moved to the rail bottom (standalone). Embedded never showed
2018
- Radar's UserMenu — the host provides its own via rightExtras. */}
1864
+ <div className="hidden @min-[920px]:flex items-center">
1865
+ <ThemeToggle />
1866
+ </div>
2019
1867
 
2020
- {/* Consumer-provided extras (e.g. Radar Hub's Install button +
2021
- avatar menu) appended to the right of the action bar. */}
2022
- {navCustomization.rightExtras}
1868
+ <Tooltip content="Keyboard shortcuts (?)">
1869
+ <button
1870
+ onClick={() => setShowHelp(true)}
1871
+ aria-label="Show keyboard shortcuts"
1872
+ className="p-1.5 rounded-md bg-theme-elevated hover:bg-theme-hover text-theme-text-secondary hover:text-theme-text-primary transition-colors"
1873
+ >
1874
+ <HelpCircle className="w-4 h-4" />
1875
+ </button>
1876
+ </Tooltip>
1877
+ <Tooltip content="Report a bug / Diagnostics">
1878
+ <button
1879
+ onClick={() => setShowDiagnostics(true)}
1880
+ aria-label="Open diagnostics"
1881
+ className="p-1.5 rounded-md bg-theme-elevated hover:bg-theme-hover text-theme-text-secondary hover:text-theme-text-primary transition-colors"
1882
+ >
1883
+ <Bug className="w-4 h-4" />
1884
+ </button>
1885
+ </Tooltip>
2023
1886
  </div>
2024
1887
  </header>
2025
1888
  )}
@@ -2454,7 +2317,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
2454
2317
  initialTab={drawerInitialTab}
2455
2318
  // No Radar header in chromeless embeds (Radar Hub) — anchor the drawer
2456
2319
  // to the top of the content area instead of leaving a 49px gap.
2457
- headerHeight={chromeless ? 0 : undefined}
2320
+ headerHeight={embedded ? 0 : undefined}
2458
2321
  rightInset={contentGutter}
2459
2322
  isOpen={resourceDrawer.isOpen}
2460
2323
  expanded={drawerExpandedProp}
@@ -2523,7 +2386,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
2523
2386
  maximized = fills the frame. */}
2524
2387
  {diagnoseOpen && (
2525
2388
  <DiagnoseSurface
2526
- topInset={chromeless ? 0 : APP_HEADER_HEIGHT}
2389
+ topInset={embedded ? 0 : APP_HEADER_HEIGHT}
2527
2390
  onBrowseIssues={() => setMainView('issues')}
2528
2391
  onOpenResource={(ref, investigationRunID) => {
2529
2392
  const resource: SelectedResource = {
@@ -2579,55 +2442,9 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
2579
2442
  {/* Spacer for dock */}
2580
2443
  <DockSpacer />
2581
2444
 
2582
- {/* Floating action buttons — embedded only, and not in chromeless (the
2583
- host owns help/diagnostics chrome). Standalone moved help + bug to
2584
- visible top-bar icons (the rail owns chrome). */}
2585
- {!showNavRail && !chromeless && (
2586
- <FloatingButtons showHelp={showHelp} showCommandPalette={showCommandPalette} showDiagnostics={showDiagnostics} onHelp={() => setShowHelp(true)} onBugReport={() => setShowDiagnostics(true)} />
2587
- )}
2588
-
2589
2445
  {/* Keyboard shortcut help overlay */}
2590
2446
  {helpOverlay.shouldRender && <ShortcutHelpOverlay isOpen={helpOverlay.isOpen} onClose={() => setShowHelp(false)} currentView={mainView} />}
2591
2447
 
2592
- {/* Command palette */}
2593
- {commandPaletteAnim.shouldRender && (
2594
- <CommandPalette
2595
- isOpen={commandPaletteAnim.isOpen}
2596
- onClose={() => setShowCommandPalette(false)}
2597
- onNavigateView={(view) => setMainView(view)}
2598
- onNavigateKind={(kind, group) => {
2599
- const params = new URLSearchParams(searchParams)
2600
- params.delete('kind')
2601
- if (group) params.set('apiGroup', group)
2602
- else params.delete('apiGroup')
2603
- params.delete('resource')
2604
- params.delete('full')
2605
- params.delete('tab')
2606
- navigate({ pathname: `/resources/${kind}`, search: params.toString() })
2607
- // Focus the table search after navigation — the user came from ⌘K
2608
- // (keyboard flow) and expects to type a resource name immediately.
2609
- setTimeout(() => {
2610
- (document.querySelector('input[placeholder="Search... (press /)"]') as HTMLInputElement)?.focus()
2611
- }, 100)
2612
- }}
2613
- onSwitchContext={(name) => switchContext.mutate(
2614
- { name },
2615
- // Namespace filter from the previous context may not exist in the
2616
- // new one — clear it so resource lists don't silently go empty.
2617
- // The server clears all per-user picks on context switch already;
2618
- // local state mirrors that via the namespace-scope effect.
2619
- { onSettled: () => setNamespaces([]) },
2620
- )}
2621
- onSetNamespaces={(ns) => {
2622
- if (namespaceScope?.cacheScoped && ns.length !== 1) return
2623
- setNamespaces(ns)
2624
- setActiveNamespace.mutate({ namespaces: ns })
2625
- }}
2626
- onToggleTheme={toggleTheme}
2627
- onShowDiagnostics={() => setShowDiagnostics(true)}
2628
- />
2629
- )}
2630
-
2631
2448
  {/* Diagnostics overlay */}
2632
2449
  {diagnosticsOverlay.shouldRender && <DiagnosticsOverlay isOpen={diagnosticsOverlay.isOpen} onClose={() => setShowDiagnostics(false)} />}
2633
2450
 
@@ -2666,29 +2483,6 @@ function DockSpacer() {
2666
2483
  )
2667
2484
  }
2668
2485
 
2669
- // Floating action buttons that position themselves above the dock
2670
- function FloatingButtons({ showHelp, showCommandPalette, showDiagnostics, onHelp, onBugReport }: { showHelp: boolean; showCommandPalette: boolean; showDiagnostics: boolean; onHelp: () => void; onBugReport: () => void }) {
2671
- const { tabs } = useDock()
2672
- if (showHelp || showCommandPalette || showDiagnostics) return null
2673
- // When dock tab bar is visible (36px), shift the buttons up above it
2674
- const bottom = tabs.length > 0 ? 'bottom-10' : 'bottom-2'
2675
- const btnClass = 'w-7 h-7 flex items-center justify-center rounded-full bg-theme-elevated/80 hover:bg-theme-hover border border-theme-border-light text-theme-text-tertiary hover:text-theme-text-secondary text-xs font-medium shadow-sm backdrop-blur-sm transition-all'
2676
- return (
2677
- <div className={`fixed ${bottom} right-4 z-40 flex items-center gap-1.5`}>
2678
- <Tooltip content="Report bug / Diagnostics" position="top">
2679
- <button onClick={onBugReport} aria-label="Open diagnostics" className={btnClass}>
2680
- <Bug className="w-3.5 h-3.5" />
2681
- </button>
2682
- </Tooltip>
2683
- <Tooltip content="Keyboard shortcuts (?)" position="top">
2684
- <button onClick={onHelp} aria-label="Show keyboard shortcuts" className={btnClass}>
2685
- ?
2686
- </button>
2687
- </Tooltip>
2688
- </div>
2689
- )
2690
- }
2691
-
2692
2486
  // Main App component wrapped with providers
2693
2487
  function App({ manageDocumentTitle = false, documentTitleSuffix, onClusterLoadStateChange }: AppProps) {
2694
2488
  return (
@@ -2710,37 +2504,6 @@ function App({ manageDocumentTitle = false, documentTitleSuffix, onClusterLoadSt
2710
2504
  )
2711
2505
  }
2712
2506
 
2713
- // Header brand: emerald-square radar icon + stacked "Radar" / "by Skyhook"
2714
- // wordmark. Shares its visual shape with the radar-hub-web shell so the
2715
- // standalone OSS app and the embedded Cloud experience read as the same
2716
- // product, and is narrow enough to leave room for the cluster switcher
2717
- // and nav block on standard laptop viewports.
2718
- function Logo() {
2719
- return (
2720
- <div className="flex items-center gap-2.5">
2721
- <div className="relative w-7 h-7 rounded-lg overflow-hidden flex-shrink-0 bg-emerald-500/10 border border-emerald-500/20">
2722
- <img
2723
- src={radarLogoUrl}
2724
- alt=""
2725
- aria-hidden
2726
- className="w-full h-full p-0.5"
2727
- // Fail loud on a missing/blocked asset rather than rendering an
2728
- // empty emerald square next to the wordmark — the latter reads
2729
- // as broken chrome with no diagnostics. Most likely cause is a
2730
- // build/deploy path mismatch.
2731
- onError={(e) =>
2732
- console.error('Radar logo asset failed to load:', (e.currentTarget as HTMLImageElement).src)
2733
- }
2734
- />
2735
- </div>
2736
- <div className="flex flex-col leading-none">
2737
- <span className="font-semibold text-[15px] tracking-tight text-theme-text-primary">Radar</span>
2738
- <span className="text-[9px] mt-0.5 tracking-wide uppercase text-theme-text-tertiary">by Skyhook</span>
2739
- </div>
2740
- </div>
2741
- )
2742
- }
2743
-
2744
2507
  // GitHub star button with live star count + programmatic starring via gh CLI
2745
2508
  // Shows a callout popover when the backend says shouldPrompt is true (synced with CLI state)
2746
2509
  function GitHubStarButton() {