@skyhook-io/radar-app 1.14.5 → 1.15.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/README.md +2 -9
- package/package.json +6 -6
- package/src/App.tsx +142 -334
- package/src/RadarApp.tsx +3 -8
- package/src/api/client.admission.test.ts +34 -0
- package/src/api/client.jobset.test.ts +18 -0
- package/src/api/client.resource-identity.test.ts +44 -0
- package/src/api/client.rightsizing.test.ts +7 -3
- package/src/api/client.ts +267 -78
- package/src/api/preferences.test.ts +54 -0
- package/src/api/preferences.ts +38 -0
- package/src/api/usage-data.fixtures.ts +33 -0
- package/src/api/usage-data.ts +165 -0
- package/src/components/CloudFunnelButton.test.tsx +155 -0
- package/src/components/CloudFunnelButton.tsx +113 -63
- package/src/components/ContextSwitcher.tsx +13 -132
- package/src/components/SelfManagedStart.tsx +96 -0
- package/src/components/applications/ApplicationsView.identity.test.ts +39 -0
- package/src/components/applications/ApplicationsView.tsx +5 -3
- package/src/components/audit/AuditSettingsDialog.tsx +15 -11
- package/src/components/cloudConnectHandoff.test.ts +1 -1
- package/src/components/cloudConnectHandoff.ts +1 -1
- package/src/components/diagnose/ActivityTurn.tsx +3 -2
- package/src/components/diagnose/AgentControls.tsx +26 -3
- package/src/components/diagnose/ApplyDialog.tsx +4 -2
- package/src/components/diagnose/DiagnoseContext.tsx +1 -0
- package/src/components/diagnose/agentCatalog.ts +6 -0
- package/src/components/diagnose/launch.test.ts +23 -0
- package/src/components/diagnose/launch.ts +4 -0
- package/src/components/diagnose/parts.test.tsx +18 -0
- package/src/components/dialog-names.test.ts +69 -0
- package/src/components/dock/NodeTerminalTab.test.tsx +34 -0
- package/src/components/dock/NodeTerminalTab.tsx +5 -3
- package/src/components/dock/WorkloadLogsTab.test.tsx +66 -0
- package/src/components/dock/WorkloadLogsTab.tsx +2 -1
- package/src/components/execution/BatchExecutionView.render.test.tsx +2 -1
- package/src/components/execution/BatchExecutionView.test.ts +52 -1
- package/src/components/execution/BatchExecutionView.tsx +324 -65
- package/src/components/execution/JobSetAdmission.test.tsx +165 -0
- package/src/components/execution/JobSetAdmission.tsx +26 -0
- package/src/components/execution/JobSetMemberComparison.test.tsx +30 -0
- package/src/components/execution/JobSetMemberComparison.tsx +88 -0
- package/src/components/execution/batch-run-actions.test.ts +1 -0
- package/src/components/execution/batch-timeline.test.ts +3 -0
- package/src/components/execution/execution-definition.test.ts +15 -1
- package/src/components/execution/execution-definition.ts +4 -0
- package/src/components/execution/member-collection.render.test.tsx +142 -0
- package/src/components/gitops/GitOpsView.tsx +33 -2
- package/src/components/gitops/destination-toast.test.ts +33 -0
- package/src/components/gitops/destination-toast.ts +46 -0
- package/src/components/gitops/useDestinationCluster.ts +30 -0
- package/src/components/helm/TrackChartSourceDialog.tsx +25 -14
- package/src/components/home/HomeView.tsx +6 -2
- package/src/components/home/MCPSetupDialog.tsx +10 -0
- package/src/components/home/RadarVersionLine.test.tsx +14 -0
- package/src/components/home/RadarVersionLine.tsx +28 -3
- package/src/components/logs/ScheduledWorkloadLogsViewer.tsx +75 -22
- package/src/components/logs/WorkloadLogsViewer.tsx +7 -4
- package/src/components/nav/PrimaryNavRail.test.tsx +10 -1
- package/src/components/nav/PrimaryNavRail.tsx +27 -8
- package/src/components/resource/PVCUsageBar.render.test.tsx +168 -0
- package/src/components/resource/PVCUsageBar.tsx +62 -16
- package/src/components/resource/PrometheusChartsGrid.render.test.tsx +1 -1
- package/src/components/resources/ResourcesView.tsx +3 -3
- package/src/components/resources/renderers/CAPIClusterRenderer.tsx +8 -0
- package/src/components/resources/renderers/JobAdmissionRenderers.test.tsx +18 -0
- package/src/components/resources/renderers/JobAdmissionRenderers.tsx +18 -0
- package/src/components/resources/renderers/KueueWorkloadRenderer.test.tsx +39 -0
- package/src/components/resources/renderers/KueueWorkloadRenderer.tsx +37 -0
- package/src/components/resources/renderers/RayClusterRenderer.test.tsx +18 -0
- package/src/components/resources/renderers/RayClusterRenderer.tsx +17 -0
- package/src/components/resources/renderers/RayJobRenderer.test.tsx +40 -0
- package/src/components/resources/renderers/RayJobRenderer.tsx +45 -0
- package/src/components/resources/renderers/RayServiceRenderer.test.tsx +69 -0
- package/src/components/resources/renderers/RayServiceRenderer.tsx +38 -0
- package/src/components/rightsizing/RightsizingScanView.tsx +34 -22
- package/src/components/rightsizing/copy.test.ts +7 -0
- package/src/components/rightsizing/model.test.ts +2 -1
- package/src/components/rightsizing/notices.test.tsx +4 -3
- package/src/components/settings/OperatorManagedNotice.tsx +17 -0
- package/src/components/settings/PrivacySection.test.tsx +84 -0
- package/src/components/settings/PrivacySection.tsx +140 -0
- package/src/components/settings/SettingsDialog.tsx +168 -92
- package/src/components/settings/controls.tsx +55 -0
- package/src/components/settings/settings-state.test.ts +18 -0
- package/src/components/settings/settings-state.ts +19 -0
- package/src/components/timeline/TimelineView.tsx +9 -3
- package/src/components/timeline/TimelineView.urlparams.test.ts +18 -1
- package/src/components/ui/ErrorBoundary.tsx +5 -0
- package/src/components/ui/Omnibar.tsx +16 -0
- package/src/components/ui/UpdateNotification.tsx +13 -9
- package/src/components/ui/command-items.ts +14 -3
- package/src/components/usage-data/UsageDataAsk.test.tsx +41 -0
- package/src/components/usage-data/UsageDataAsk.tsx +106 -0
- package/src/components/usage-data/UsageDataPrompt.test.ts +53 -0
- package/src/components/usage-data/UsageDataPrompt.tsx +148 -0
- package/src/components/useContextSwitchFlow.tsx +147 -0
- package/src/components/whats-new/WhatsNew.test.ts +110 -0
- package/src/components/whats-new/WhatsNew.tsx +423 -0
- package/src/components/whats-new/WhatsNewDialog.test.tsx +229 -0
- package/src/components/whats-new/check-whats-new.test.ts +65 -0
- package/src/components/whats-new/releaseNotes.ts +121 -0
- package/src/components/workload/WorkloadView.test.ts +17 -1
- package/src/components/workload/WorkloadView.tsx +47 -19
- package/src/context/ContextSwitchContext.tsx +18 -2
- package/src/context/NavCustomization.tsx +4 -58
- package/src/context/ThemeContext.tsx +3 -11
- package/src/hooks/useClusterLoadState.ts +2 -2
- package/src/hooks/useFavorites.ts +3 -5
- package/src/k8s-ui-exports.test.ts +73 -0
- package/src/utils/auditBadges.ts +1 -1
- package/src/utils/navigation.test.ts +34 -1
- package/src/utils/navigation.ts +28 -5
- package/src/utils/topology-selection.test.ts +56 -0
- package/src/utils/topology-selection.ts +9 -9
- package/src/utils/version.ts +11 -0
- 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
|
|
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'
|
|
@@ -48,8 +49,10 @@ import { CapabilitiesProvider, useCapabilitiesContext } from './contexts/Capabil
|
|
|
48
49
|
import { UserMenu } from './components/UserMenu'
|
|
49
50
|
import { ErrorBoundary } from './components/ui/ErrorBoundary'
|
|
50
51
|
import { UpdateNotification } from './components/ui/UpdateNotification'
|
|
52
|
+
import { openWhatsNew, useWhatsNewStatus, WhatsNew } from './components/whats-new/WhatsNew'
|
|
53
|
+
import { useUsageData, useUsageRecording } from './api/usage-data'
|
|
54
|
+
import { UsageDataPrompt } from './components/usage-data/UsageDataPrompt'
|
|
51
55
|
import { ShortcutHelpOverlay } from './components/ui/ShortcutHelpOverlay'
|
|
52
|
-
import { CommandPalette } from './components/ui/CommandPalette'
|
|
53
56
|
import { DiagnosticsOverlay } from './components/ui/DiagnosticsOverlay'
|
|
54
57
|
import { useEventSource } from './hooks/useEventSource'
|
|
55
58
|
import { debugNamespaceLog, useNamespaces, useNamespaceScope, useSetActiveNamespace, useSwitchContext, useAuthMe, useAudit } from './api/client'
|
|
@@ -61,20 +64,18 @@ import { useAnimatedUnmount } from './hooks/useAnimatedUnmount'
|
|
|
61
64
|
import { useDocumentTitle } from './hooks/useDocumentTitle'
|
|
62
65
|
import type { ClusterLoadState } from './types/clusterLoadState'
|
|
63
66
|
import { useClusterLoadState } from './hooks/useClusterLoadState'
|
|
64
|
-
import { Network,
|
|
67
|
+
import { Network, Sun, Moon, Star, Bug, SquareTerminal, HelpCircle, Loader2, RefreshCw } from 'lucide-react'
|
|
65
68
|
import { useTheme } from './context/ThemeContext'
|
|
66
69
|
import { Tooltip } from './components/ui/Tooltip'
|
|
67
70
|
import { LargeClusterNamespacePicker } from './components/shared/LargeClusterNamespacePicker'
|
|
68
71
|
import { SettingsDialog, type SettingsSectionId } from './components/settings/SettingsDialog'
|
|
69
72
|
import type { APIResource, TopologyNode, GroupingMode, MainView, SelectedResource, SelectedHelmRelease, NodeKind, TopologyMode, Topology, K8sEvent } from './types'
|
|
70
|
-
import { kindToPluralWithGroup, pluralToKind, openExternal, apiVersionToGroup, relatedResourcePath, searchHitToSelectedResource } from './utils/navigation'
|
|
73
|
+
import { kindToPluralWithGroup, pluralToKind, openExternal, apiVersionToGroup, relatedResourcePath, searchHitToSelectedResource, withCrossViewParams } from './utils/navigation'
|
|
71
74
|
import { findSelectedTopologyNode } from './utils/topology-selection'
|
|
72
75
|
import { type OmnibarHandle } from './components/ui/Omnibar'
|
|
73
76
|
import { RadarOmnibar } from './components/ui/RadarOmnibar'
|
|
74
77
|
import type { ContextSwitcherHandle } from './components/ContextSwitcher'
|
|
75
78
|
|
|
76
|
-
const radarLogoUrl = assetUrl('/images/radar/radar-icon.svg')
|
|
77
|
-
|
|
78
79
|
// All possible node kinds (core + GitOps)
|
|
79
80
|
const ALL_NODE_KINDS: NodeKind[] = [
|
|
80
81
|
'Internet', 'Ingress', 'Gateway', 'HTTPRoute', 'GRPCRoute', 'TCPRoute', 'TLSRoute',
|
|
@@ -146,6 +147,28 @@ function getViewFromPath(pathname: string): ExtendedMainView {
|
|
|
146
147
|
return 'home'
|
|
147
148
|
}
|
|
148
149
|
|
|
150
|
+
// The usage-data name for the current screen: a resource list names its
|
|
151
|
+
// kind's plural ("resources:deployments"); the server keeps built-in kinds
|
|
152
|
+
// only, so a custom resource's name never leaves.
|
|
153
|
+
function usageView(pathname: string, view: ExtendedMainView, upgrade: boolean): string {
|
|
154
|
+
if (upgrade) return 'upgrade'
|
|
155
|
+
if (view === 'resources') {
|
|
156
|
+
const plural = pathname.match(/^\/resources\/([^/]+)/)?.[1]
|
|
157
|
+
if (plural) return `resources:${plural.toLowerCase()}`
|
|
158
|
+
}
|
|
159
|
+
return view
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// The screen a crash is counted under. Fixed names, because release builds
|
|
163
|
+
// shorten component names and the component stack can't say which screen.
|
|
164
|
+
const CRASH_LABELS: Record<ExtendedMainView, string> = {
|
|
165
|
+
home: 'Home', topology: 'Topology', resources: 'Resources', timeline: 'Timeline',
|
|
166
|
+
issues: 'Issues', helm: 'Helm', helmCompare: 'HelmCompare', traffic: 'Traffic',
|
|
167
|
+
cost: 'Cost', capacity: 'Capacity', checks: 'Checks', gitops: 'GitOps',
|
|
168
|
+
applications: 'Applications', workload: 'Workload', compare: 'Compare',
|
|
169
|
+
investigations: 'Investigations',
|
|
170
|
+
}
|
|
171
|
+
|
|
149
172
|
// The namespace scope filter is meaningful only on namespaced surfaces. On
|
|
150
173
|
// cluster-scoped views it does nothing, so we disable it with an explanation
|
|
151
174
|
// rather than leaving a dead control that silently ignores the pick:
|
|
@@ -343,30 +366,23 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
343
366
|
[navCustomization],
|
|
344
367
|
)
|
|
345
368
|
// Resolve every host-takeover URL ONCE (memoized on navCustomization) so the
|
|
346
|
-
// setMainView intercept, redirect effect,
|
|
347
|
-
//
|
|
369
|
+
// setMainView intercept, redirect effect, inline-view gating, and the cert
|
|
370
|
+
// click handler all consume the SAME value — host
|
|
348
371
|
// callbacks aren't guaranteed idempotent (scope / flags / signed URLs can
|
|
349
372
|
// shift between calls). undefined = not taken over → Radar renders the view
|
|
350
|
-
// itself.
|
|
351
|
-
// 'checks' target for back-compat.
|
|
373
|
+
// itself.
|
|
352
374
|
const takeover: Record<FleetTakeoverTarget, string | undefined> = useMemo(
|
|
353
375
|
() => ({
|
|
354
376
|
issues: navCustomization.fleetTakeoverHref?.('issues'),
|
|
355
377
|
gitops: navCustomization.fleetTakeoverHref?.('gitops'),
|
|
356
|
-
checks: navCustomization.fleetTakeoverHref?.('checks')
|
|
378
|
+
checks: navCustomization.fleetTakeoverHref?.('checks'),
|
|
357
379
|
certs: navCustomization.fleetTakeoverHref?.('certs'),
|
|
358
380
|
}),
|
|
359
381
|
[navCustomization],
|
|
360
382
|
)
|
|
361
383
|
const { pinned: navRailPinned, togglePinned: toggleNavRailPinned } = useNavRailPinned()
|
|
362
|
-
|
|
363
|
-
|
|
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'
|
|
384
|
+
const embedded = navCustomization.embedded === true
|
|
385
|
+
const showNavRail = !embedded
|
|
370
386
|
// Force the slim rail on narrow windows: a pinned 176px rail needs viewport
|
|
371
387
|
// ≥976 to keep content above its ~800px floor (collapsed needs only ≥856).
|
|
372
388
|
// Below 976 we render collapsed regardless of the pin preference — a
|
|
@@ -426,6 +442,11 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
426
442
|
const mainView = getViewFromPath(location.pathname)
|
|
427
443
|
const upgradeReadinessRoute = location.pathname.startsWith('/checks/upgrade')
|
|
428
444
|
|
|
445
|
+
// Opt-in usage data. Embedded hosts own their own consent, so Radar never asks there.
|
|
446
|
+
const usageData = useUsageData(!navCustomization.embedded)
|
|
447
|
+
// A status cached by another screen must not start recording inside a host.
|
|
448
|
+
useUsageRecording(usageView(location.pathname, mainView, upgradeReadinessRoute), navCustomization.embedded ? undefined : usageData.data)
|
|
449
|
+
|
|
429
450
|
// Initialize the kind→plural discovery map app-wide (not just on ResourcesView
|
|
430
451
|
// mount) so the omnibar can open a CRD hit with an irregular plural from any
|
|
431
452
|
// view — kindToPlural would otherwise English-guess the route before a
|
|
@@ -501,6 +522,12 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
501
522
|
navigate({ pathname: path, search: newParams.toString() })
|
|
502
523
|
}, [location.search, navigate, takeover, goHost])
|
|
503
524
|
|
|
525
|
+
const whatsNewStatus = useWhatsNewStatus()
|
|
526
|
+
|
|
527
|
+
const navigateToPath = useCallback((path: string) => {
|
|
528
|
+
navigate(withCrossViewParams(path, location.search))
|
|
529
|
+
}, [location.search, navigate])
|
|
530
|
+
|
|
504
531
|
// The standalone rail expresses intent to leave the full-width investigation
|
|
505
532
|
// workspace. Close it before routing so the destination is immediately visible;
|
|
506
533
|
// docked investigations stay open across views as a persistent side panel.
|
|
@@ -513,15 +540,14 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
513
540
|
}, [diagnoseOpen, diagnoseMaximized, dismissDiagnoseForNavigation, setMainView])
|
|
514
541
|
|
|
515
542
|
// Cloud (embedded) takes over the "fleet-shaped" per-cluster views with its
|
|
516
|
-
// own fleet pages scoped to this cluster
|
|
517
|
-
// Radar drops the matching pills (see the nav below). In-app nav hands off in
|
|
543
|
+
// own fleet pages scoped to this cluster. In-app navigation hands off in
|
|
518
544
|
// setMainView (above); direct /<view> URL entry funnels through the redirect
|
|
519
545
|
// effect below. Both consume the memoized `takeover` resolved above. Standalone
|
|
520
546
|
// OSS (no fleetTakeoverHref) is unaffected and renders the in-app view.
|
|
521
547
|
//
|
|
522
548
|
// Has the host claimed this view? View-shaped targets only ('certs' has no
|
|
523
|
-
// Radar view — only its Home card consults `takeover`). Used to
|
|
524
|
-
//
|
|
549
|
+
// Radar view — only its Home card consults `takeover`). Used to gate the
|
|
550
|
+
// inline view render in favor of the "Opening…" splash.
|
|
525
551
|
const isViewTakenOver = (view: ExtendedMainView): boolean =>
|
|
526
552
|
(view === 'issues' || view === 'gitops' || view === 'checks') && !!takeover[view]
|
|
527
553
|
// The host's URL for the CURRENT view, if taken over. Drives the redirect
|
|
@@ -565,9 +591,6 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
565
591
|
// Help overlay state
|
|
566
592
|
const [showHelp, setShowHelp] = useState(false)
|
|
567
593
|
|
|
568
|
-
// Command palette state
|
|
569
|
-
const [showCommandPalette, setShowCommandPalette] = useState(false)
|
|
570
|
-
|
|
571
594
|
// Settings dialog state
|
|
572
595
|
const [showSettings, setShowSettings] = useState(false)
|
|
573
596
|
const [settingsSection, setSettingsSection] = useState<SettingsSectionId>('overview')
|
|
@@ -683,7 +706,6 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
683
706
|
// Dialog-kind overlays wait their exit duration (shorter than the entrance);
|
|
684
707
|
// drawers keep the symmetric slide.
|
|
685
708
|
const helpOverlay = useAnimatedUnmount(showHelp, overlayExitMs('dialog'))
|
|
686
|
-
const commandPaletteAnim = useAnimatedUnmount(showCommandPalette, overlayExitMs('dialog'))
|
|
687
709
|
const diagnosticsOverlay = useAnimatedUnmount(showDiagnostics, overlayExitMs('dialog'))
|
|
688
710
|
|
|
689
711
|
// Hold last valid values so drawers can animate out before data disappears
|
|
@@ -791,13 +813,14 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
791
813
|
metadata: { namespace: resource.namespace ?? '', name: resource.name },
|
|
792
814
|
})
|
|
793
815
|
if (gitOpsPath) {
|
|
794
|
-
const destination = new URL(gitOpsPath, window.location.origin)
|
|
795
|
-
if (investigationRunID) destination.searchParams.
|
|
816
|
+
const destination = new URL(withCrossViewParams(gitOpsPath, searchParams.toString()), window.location.origin)
|
|
817
|
+
if (investigationRunID === null) destination.searchParams.delete('ai-run')
|
|
818
|
+
else if (investigationRunID) destination.searchParams.set('ai-run', investigationRunID)
|
|
796
819
|
navigate(`${destination.pathname}${destination.search}${destination.hash}`)
|
|
797
820
|
return
|
|
798
821
|
}
|
|
799
822
|
navigateToResourceList(resource, investigationRunID)
|
|
800
|
-
}, [navigate, navigateToHelmRelease, navigateToResourceList])
|
|
823
|
+
}, [navigate, navigateToHelmRelease, navigateToResourceList, searchParams])
|
|
801
824
|
|
|
802
825
|
// Collapse the over-list fullscreen back to the drawer = drop ?full=1 (and the
|
|
803
826
|
// resource-scoped ?tab) in place. The button means "collapse THIS to a drawer"
|
|
@@ -830,7 +853,6 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
830
853
|
// Theme toggle for keyboard shortcut
|
|
831
854
|
const { toggleTheme } = useTheme()
|
|
832
855
|
|
|
833
|
-
// Context switching for command palette
|
|
834
856
|
const switchContext = useSwitchContext()
|
|
835
857
|
|
|
836
858
|
// Refs for dropdown components to trigger them via shortcuts
|
|
@@ -909,10 +931,8 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
909
931
|
category: 'General' as const,
|
|
910
932
|
scope: 'global' as const,
|
|
911
933
|
allowInInputs: true,
|
|
912
|
-
//
|
|
913
|
-
|
|
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) },
|
|
934
|
+
// Radar Hub owns its own omnibar.
|
|
935
|
+
handler: () => { if (!embedded) omnibarRef.current?.focus() },
|
|
916
936
|
},
|
|
917
937
|
{
|
|
918
938
|
id: 'diagnostics',
|
|
@@ -981,7 +1001,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
981
1001
|
const { data: namespaceScope } = useNamespaceScope()
|
|
982
1002
|
|
|
983
1003
|
// Context switch state
|
|
984
|
-
const { isSwitching, targetContext, progressMessage, updateProgress, endSwitch } = useContextSwitch()
|
|
1004
|
+
const { isSwitching, targetContext, progressMessage, updateProgress, endSwitch, takeOpenAfterSwitch } = useContextSwitch()
|
|
985
1005
|
|
|
986
1006
|
// Connection state (for graceful startup)
|
|
987
1007
|
const { connection, retry: retryConnection, isRetrying, updateFromSSE: updateConnectionFromSSE } = useConnection()
|
|
@@ -1008,7 +1028,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
1008
1028
|
const { clusterLoadState, showHomeClusterLoadFallback, clusterLoadInitial } = useClusterLoadState({
|
|
1009
1029
|
namespaces,
|
|
1010
1030
|
mainView,
|
|
1011
|
-
chromeless,
|
|
1031
|
+
chromeless: embedded,
|
|
1012
1032
|
contentReady,
|
|
1013
1033
|
onClusterLoadStateChange,
|
|
1014
1034
|
})
|
|
@@ -1158,7 +1178,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
1158
1178
|
const { topology, connected: eventStreamConnected, connecting: eventStreamConnecting, reconnect: reconnectEventStream } = useEventSource(namespaces, sseMode as 'resources' | 'traffic', {
|
|
1159
1179
|
onContextSwitchComplete: endSwitch,
|
|
1160
1180
|
onContextSwitchProgress: updateProgress,
|
|
1161
|
-
onContextChanged: () => {
|
|
1181
|
+
onContextChanged: (context) => {
|
|
1162
1182
|
// Clear all React Query caches when cluster context changes
|
|
1163
1183
|
// This ensures helm releases, resources, etc. are refetched from the new cluster
|
|
1164
1184
|
// removeQueries clears cached data, invalidateQueries triggers refetch
|
|
@@ -1181,7 +1201,16 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
1181
1201
|
// Reset resource-specific params while retaining the durable investigation
|
|
1182
1202
|
// focus. Diagnose resolves runs by id and owns whether the focused run is
|
|
1183
1203
|
// still readable after the context switch.
|
|
1184
|
-
|
|
1204
|
+
const openAfter = takeOpenAfterSwitch(context)
|
|
1205
|
+
if (openAfter) {
|
|
1206
|
+
// The switch was made to open this resource on the new cluster; the
|
|
1207
|
+
// page it was requested from belongs to the old one.
|
|
1208
|
+
setSelectedResource(openAfter)
|
|
1209
|
+
const params = new URLSearchParams()
|
|
1210
|
+
params.set('resource', openAfter.namespace ? `${openAfter.namespace}/${openAfter.name}` : openAfter.name)
|
|
1211
|
+
if (openAfter.group) params.set('apiGroup', openAfter.group)
|
|
1212
|
+
navigate({ pathname: `/resources/${openAfter.kind}`, search: params.toString() }, { replace: true })
|
|
1213
|
+
} else if (isInvestigationWorkspacePath(location.pathname)) {
|
|
1185
1214
|
navigate(
|
|
1186
1215
|
{
|
|
1187
1216
|
pathname: location.pathname,
|
|
@@ -1321,6 +1350,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
1321
1350
|
if (node.kind === 'Internet') return
|
|
1322
1351
|
|
|
1323
1352
|
const nodeGroup = apiVersionToGroup(node.data.apiVersion as string | undefined)
|
|
1353
|
+
const resourceKind = topologyNodeResourceKind(node)
|
|
1324
1354
|
// Radar's topology PodGroup is a virtual pod aggregate. A Kubernetes
|
|
1325
1355
|
// scheduling.k8s.io PodGroup carries apiVersion and is a real resource.
|
|
1326
1356
|
if (node.kind === 'PodGroup' && !nodeGroup) return
|
|
@@ -1330,14 +1360,14 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
1330
1360
|
// detail page with tree + insights + ops that the drawer can't reproduce.
|
|
1331
1361
|
// Route there from the main topology when the node is one of those kinds;
|
|
1332
1362
|
// everything else falls back to the drawer.
|
|
1333
|
-
const gitOpsPath = gitOpsRouteForKind(
|
|
1363
|
+
const gitOpsPath = gitOpsRouteForKind(resourceKind, namespace, node.name, nodeGroup, true)
|
|
1334
1364
|
if (gitOpsPath) {
|
|
1335
1365
|
navigate(gitOpsPath)
|
|
1336
1366
|
return
|
|
1337
1367
|
}
|
|
1338
1368
|
|
|
1339
1369
|
navigateToResource({
|
|
1340
|
-
kind: kindToPluralWithGroup(
|
|
1370
|
+
kind: kindToPluralWithGroup(resourceKind, nodeGroup),
|
|
1341
1371
|
namespace,
|
|
1342
1372
|
name: node.name,
|
|
1343
1373
|
group: nodeGroup,
|
|
@@ -1739,6 +1769,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
1739
1769
|
showPinToggle={!railForcedSlim}
|
|
1740
1770
|
onOpenSettings={() => openSettings()}
|
|
1741
1771
|
accountSlot={<UserMenu variant="rail" pinned={navRailEffectivePinned} />}
|
|
1772
|
+
whatsNew={whatsNewStatus.available ? { unread: whatsNewStatus.unread, onOpen: openWhatsNew } : undefined}
|
|
1742
1773
|
/>
|
|
1743
1774
|
)}
|
|
1744
1775
|
{/* `relative` makes this column the containing block for the absolute
|
|
@@ -1752,46 +1783,20 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
1752
1783
|
− nav rail), not the viewport, so it collapses gracefully on narrow windows.
|
|
1753
1784
|
The AI panel docks BELOW the navbar and pushes only the content region, so the
|
|
1754
1785
|
navbar is never squeezed by it — these thresholds react to real window width. */}
|
|
1755
|
-
{!
|
|
1786
|
+
{!embedded && (
|
|
1756
1787
|
<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
|
-
{/*
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
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
|
-
)}
|
|
1788
|
+
{/* Fixed width keeps the omnibar steady as cluster and namespace names change. */}
|
|
1789
|
+
<div className="flex items-center gap-4 shrink-0 w-[492px]">
|
|
1790
|
+
<div className="flex items-center gap-2 min-w-0 flex-1">
|
|
1791
|
+
<ScopePill>
|
|
1792
|
+
<ContextSwitcher ref={contextSwitcherRef} variant="segment" />
|
|
1793
|
+
<NamespaceSwitcher
|
|
1794
|
+
ref={namespaceSwitcherRef}
|
|
1795
|
+
variant="segment"
|
|
1796
|
+
disabled={namespaceFilter.disabled}
|
|
1797
|
+
disabledTooltip={namespaceFilter.tooltip}
|
|
1798
|
+
/>
|
|
1799
|
+
</ScopePill>
|
|
1795
1800
|
{/* Connection status — a fixed-size dot (state in the tooltip), an
|
|
1796
1801
|
optional reconnect button, and when the header is wide enough
|
|
1797
1802
|
(xl+), a label. The label is nowrap and unbounded: it overflows
|
|
@@ -1817,7 +1822,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
1817
1822
|
}`}
|
|
1818
1823
|
/>
|
|
1819
1824
|
</Tooltip>
|
|
1820
|
-
{
|
|
1825
|
+
{headerConnectionDisplayLabel && (
|
|
1821
1826
|
<span className="hidden xl:flex items-center gap-1.5 whitespace-nowrap text-[11px] text-theme-text-tertiary">
|
|
1822
1827
|
{showClusterWarmupLabel && <Loader2 className="w-3 h-3 animate-spin" />}
|
|
1823
1828
|
{headerConnectionDisplayLabel}
|
|
@@ -1842,65 +1847,6 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
1842
1847
|
</div>
|
|
1843
1848
|
</div>
|
|
1844
1849
|
|
|
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
1850
|
{/* Center: omnibar — standalone search + command surface (the ⌘K entry).
|
|
1905
1851
|
Its container is one of three EQUAL flex-1 columns (see the left/right
|
|
1906
1852
|
groups): equal side columns keep this middle column — and the search
|
|
@@ -1908,60 +1854,38 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
1908
1854
|
chrome gets (cluster name, namespace label, "Discovering…" /
|
|
1909
1855
|
"Disconnected" text). Overflowing side content truncates instead of
|
|
1910
1856
|
dragging the box. Same pattern as Radar Hub's ClusterTopBar. */}
|
|
1911
|
-
|
|
1912
|
-
<
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
)}
|
|
1857
|
+
<div className="hidden @min-[720px]:flex flex-1 justify-center min-w-0 px-3">
|
|
1858
|
+
<RadarOmnibar
|
|
1859
|
+
ref={omnibarRef}
|
|
1860
|
+
onNavigateView={(view) => setMainView(view)}
|
|
1861
|
+
onNavigateKind={(kind, group) => {
|
|
1862
|
+
const params = new URLSearchParams(searchParams)
|
|
1863
|
+
params.delete('kind')
|
|
1864
|
+
if (group) params.set('apiGroup', group); else params.delete('apiGroup')
|
|
1865
|
+
params.delete('resource')
|
|
1866
|
+
params.delete('full')
|
|
1867
|
+
params.delete('tab')
|
|
1868
|
+
navigate({ pathname: `/resources/${kind}`, search: params.toString() })
|
|
1869
|
+
}}
|
|
1870
|
+
onSwitchContext={(name) => switchContext.mutate({ name }, { onSettled: () => setNamespaces([]) })}
|
|
1871
|
+
onSetNamespaces={(ns) => { setNamespaces(ns); setActiveNamespace.mutate({ namespaces: ns }) }}
|
|
1872
|
+
onToggleTheme={toggleTheme}
|
|
1873
|
+
onShowDiagnostics={() => setShowDiagnostics(true)}
|
|
1874
|
+
onShowWhatsNew={whatsNewStatus.available ? openWhatsNew : undefined}
|
|
1875
|
+
onOpenResource={(hit) => navigateToResourceList(searchHitToSelectedResource(hit))}
|
|
1876
|
+
/>
|
|
1877
|
+
</div>
|
|
1933
1878
|
|
|
1934
1879
|
{/* Right: Controls */}
|
|
1935
1880
|
<div className="flex items-center gap-3 shrink-0">
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
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
|
-
)}
|
|
1881
|
+
<div className="hidden @min-[1100px]:block">
|
|
1882
|
+
<GitHubStarButton />
|
|
1883
|
+
</div>
|
|
1957
1884
|
|
|
1958
1885
|
{/* AI investigations (self-hides when no agent CLI is present) */}
|
|
1959
1886
|
<GlobalDiagnoseButton />
|
|
1960
1887
|
|
|
1961
|
-
|
|
1962
|
-
Cloud embeds render chromeless anyway; the explicit gate is
|
|
1963
|
-
belt-and-braces for future chrome-bearing embedders. */}
|
|
1964
|
-
{!navCustomization.embedded && <CloudFunnelButton />}
|
|
1888
|
+
<CloudFunnelButton />
|
|
1965
1889
|
|
|
1966
1890
|
{/* Local terminal */}
|
|
1967
1891
|
{capabilities.localTerminal && (
|
|
@@ -1976,50 +1900,28 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
1976
1900
|
</Tooltip>
|
|
1977
1901
|
)}
|
|
1978
1902
|
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
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. */}
|
|
1903
|
+
<div className="hidden @min-[920px]:flex items-center">
|
|
1904
|
+
<ThemeToggle />
|
|
1905
|
+
</div>
|
|
2019
1906
|
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
1907
|
+
<Tooltip content="Keyboard shortcuts (?)">
|
|
1908
|
+
<button
|
|
1909
|
+
onClick={() => setShowHelp(true)}
|
|
1910
|
+
aria-label="Show keyboard shortcuts"
|
|
1911
|
+
className="p-1.5 rounded-md bg-theme-elevated hover:bg-theme-hover text-theme-text-secondary hover:text-theme-text-primary transition-colors"
|
|
1912
|
+
>
|
|
1913
|
+
<HelpCircle className="w-4 h-4" />
|
|
1914
|
+
</button>
|
|
1915
|
+
</Tooltip>
|
|
1916
|
+
<Tooltip content="Report a bug / Diagnostics">
|
|
1917
|
+
<button
|
|
1918
|
+
onClick={() => setShowDiagnostics(true)}
|
|
1919
|
+
aria-label="Open diagnostics"
|
|
1920
|
+
className="p-1.5 rounded-md bg-theme-elevated hover:bg-theme-hover text-theme-text-secondary hover:text-theme-text-primary transition-colors"
|
|
1921
|
+
>
|
|
1922
|
+
<Bug className="w-4 h-4" />
|
|
1923
|
+
</button>
|
|
1924
|
+
</Tooltip>
|
|
2023
1925
|
</div>
|
|
2024
1926
|
</header>
|
|
2025
1927
|
)}
|
|
@@ -2114,7 +2016,10 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
2114
2016
|
{/* Search included, not just the path: selection inside a view rides in
|
|
2115
2017
|
the query (?resource=, ?release=), so a path-only key would still
|
|
2116
2018
|
strand a crash on the view that produced it. */}
|
|
2117
|
-
<ErrorBoundary
|
|
2019
|
+
<ErrorBoundary
|
|
2020
|
+
resetKey={location.pathname + location.search}
|
|
2021
|
+
usageLabel={upgradeReadinessRoute ? 'Upgrade' : CRASH_LABELS[mainView]}
|
|
2022
|
+
>
|
|
2118
2023
|
{/* Initial sync in progress: views that need the full cluster dataset
|
|
2119
2024
|
show per-kind progress instead; resource views work as kinds sync. */}
|
|
2120
2025
|
{viewsSyncGated && connection.syncStatus && (
|
|
@@ -2131,7 +2036,8 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
2131
2036
|
fallbackClusterLoadState={showHomeClusterLoadFallback ? clusterLoadState : undefined}
|
|
2132
2037
|
onNavigateToView={setMainView}
|
|
2133
2038
|
onNavigateToHelmRelease={navCustomization.embedded ? undefined : navigateToHelmRelease}
|
|
2134
|
-
onNavigateToManagerPath={navCustomization.embedded || takeover.gitops ? undefined :
|
|
2039
|
+
onNavigateToManagerPath={navCustomization.embedded || takeover.gitops ? undefined : navigateToPath}
|
|
2040
|
+
onShowWhatsNew={!navCustomization.embedded && whatsNewStatus.available ? openWhatsNew : undefined}
|
|
2135
2041
|
// Upgrade impact lives under /checks, which a Cloud host takes
|
|
2136
2042
|
// over wholesale — its fleet pages have no upgrade sub-route, so
|
|
2137
2043
|
// the version line stays plain text there.
|
|
@@ -2454,7 +2360,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
2454
2360
|
initialTab={drawerInitialTab}
|
|
2455
2361
|
// No Radar header in chromeless embeds (Radar Hub) — anchor the drawer
|
|
2456
2362
|
// to the top of the content area instead of leaving a 49px gap.
|
|
2457
|
-
headerHeight={
|
|
2363
|
+
headerHeight={embedded ? 0 : undefined}
|
|
2458
2364
|
rightInset={contentGutter}
|
|
2459
2365
|
isOpen={resourceDrawer.isOpen}
|
|
2460
2366
|
expanded={drawerExpandedProp}
|
|
@@ -2523,7 +2429,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
2523
2429
|
maximized = fills the frame. */}
|
|
2524
2430
|
{diagnoseOpen && (
|
|
2525
2431
|
<DiagnoseSurface
|
|
2526
|
-
topInset={
|
|
2432
|
+
topInset={embedded ? 0 : APP_HEADER_HEIGHT}
|
|
2527
2433
|
onBrowseIssues={() => setMainView('issues')}
|
|
2528
2434
|
onOpenResource={(ref, investigationRunID) => {
|
|
2529
2435
|
const resource: SelectedResource = {
|
|
@@ -2572,6 +2478,8 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
2572
2478
|
|
|
2573
2479
|
{/* Update notification — hidden in embedded mode (OSS download nudge). */}
|
|
2574
2480
|
{!navCustomization.embedded && <UpdateNotification />}
|
|
2481
|
+
{!navCustomization.embedded && <WhatsNew onNavigate={navigateToPath} usageData={usageData.data} />}
|
|
2482
|
+
{!navCustomization.embedded && <UsageDataPrompt status={usageData.data} />}
|
|
2575
2483
|
|
|
2576
2484
|
{/* Bottom Dock for Terminal/Logs */}
|
|
2577
2485
|
<BottomDock />
|
|
@@ -2579,55 +2487,9 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
2579
2487
|
{/* Spacer for dock */}
|
|
2580
2488
|
<DockSpacer />
|
|
2581
2489
|
|
|
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
2490
|
{/* Keyboard shortcut help overlay */}
|
|
2590
2491
|
{helpOverlay.shouldRender && <ShortcutHelpOverlay isOpen={helpOverlay.isOpen} onClose={() => setShowHelp(false)} currentView={mainView} />}
|
|
2591
2492
|
|
|
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
2493
|
{/* Diagnostics overlay */}
|
|
2632
2494
|
{diagnosticsOverlay.shouldRender && <DiagnosticsOverlay isOpen={diagnosticsOverlay.isOpen} onClose={() => setShowDiagnostics(false)} />}
|
|
2633
2495
|
|
|
@@ -2666,29 +2528,6 @@ function DockSpacer() {
|
|
|
2666
2528
|
)
|
|
2667
2529
|
}
|
|
2668
2530
|
|
|
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
2531
|
// Main App component wrapped with providers
|
|
2693
2532
|
function App({ manageDocumentTitle = false, documentTitleSuffix, onClusterLoadStateChange }: AppProps) {
|
|
2694
2533
|
return (
|
|
@@ -2710,37 +2549,6 @@ function App({ manageDocumentTitle = false, documentTitleSuffix, onClusterLoadSt
|
|
|
2710
2549
|
)
|
|
2711
2550
|
}
|
|
2712
2551
|
|
|
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
2552
|
// GitHub star button with live star count + programmatic starring via gh CLI
|
|
2745
2553
|
// Shows a callout popover when the backend says shouldPrompt is true (synced with CLI state)
|
|
2746
2554
|
function GitHubStarButton() {
|