@skyhook-io/radar-app 1.8.6 → 1.8.8
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 +7 -1
- package/package.json +12 -10
- package/src/App.tsx +278 -230
- package/src/RadarApp.tsx +91 -20
- package/src/api/client.argoResourceSync.test.ts +69 -0
- package/src/api/client.delta.test.ts +89 -0
- package/src/api/client.deltaSync.test.ts +216 -0
- package/src/api/client.rightsizing.test.ts +32 -0
- package/src/api/client.ts +1462 -246
- package/src/api/diagnose.ts +288 -0
- package/src/api/timelineSource.test.ts +217 -0
- package/src/api/timelineSource.ts +582 -0
- package/src/components/ConnectionErrorView.tsx +174 -70
- package/src/components/ContextSwitcher.tsx +13 -5
- package/src/components/applications/ApplicationsView.tsx +775 -80
- package/src/components/audit/AuditSettingsDialog.tsx +2 -2
- package/src/components/cost/ApplicationCostTab.test.ts +204 -0
- package/src/components/cost/ApplicationCostTab.tsx +571 -0
- package/src/components/cost/CostTrendChart.tsx +103 -72
- package/src/components/cost/CostView.test.ts +12 -0
- package/src/components/cost/CostView.tsx +494 -229
- package/src/components/cost/CostViewTabs.test.tsx +21 -0
- package/src/components/cost/CostViewTabs.tsx +40 -0
- package/src/components/cost/CurrentAllocationUse.test.ts +21 -0
- package/src/components/cost/CurrentAllocationUse.tsx +126 -0
- package/src/components/cost/WorkloadCostTab.test.ts +153 -0
- package/src/components/cost/WorkloadCostTab.tsx +372 -0
- package/src/components/cost/cloud-console.test.ts +39 -0
- package/src/components/cost/cloud-console.ts +81 -0
- package/src/components/cost/errors.ts +8 -0
- package/src/components/cost/format.test.ts +27 -0
- package/src/components/cost/format.ts +46 -0
- package/src/components/cost/kinds.ts +5 -0
- package/src/components/curl/ServiceCurlButton.tsx +2 -2
- package/src/components/diagnose/AISettings.tsx +116 -0
- package/src/components/diagnose/DiagnoseContext.tsx +491 -0
- package/src/components/diagnose/DiagnoseSurface.tsx +385 -0
- package/src/components/diagnose/Home.tsx +163 -0
- package/src/components/diagnose/InvestigationView.tsx +604 -0
- package/src/components/diagnose/LocalDiagnoseAction.tsx +150 -0
- package/src/components/diagnose/launch.ts +65 -0
- package/src/components/diagnose/parts.tsx +1689 -0
- package/src/components/dock/BottomDock.tsx +2 -3
- package/src/components/dock/WorkloadLogsTab.tsx +21 -5
- package/src/components/execution/BatchExecutionView.test.ts +170 -0
- package/src/components/execution/BatchExecutionView.tsx +1329 -0
- package/src/components/execution/batch-run-actions.test.ts +48 -0
- package/src/components/execution/batch-run-actions.ts +24 -0
- package/src/components/execution/batch-timeline.test.ts +57 -0
- package/src/components/execution/batch-timeline.ts +46 -0
- package/src/components/execution/execution-definition.test.ts +208 -0
- package/src/components/execution/execution-definition.ts +245 -0
- package/src/components/gitops/ArgoResourceDiffLoader.tsx +23 -0
- package/src/components/gitops/GitOpsView.tsx +81 -14
- package/src/components/gitops/RevisionMetaChip.tsx +63 -0
- package/src/components/helm/ChartBrowser.tsx +2 -3
- package/src/components/helm/HelmCompareRoute.tsx +1 -2
- package/src/components/helm/HelmReleaseDrawer.test.ts +17 -0
- package/src/components/helm/HelmReleaseDrawer.tsx +376 -43
- package/src/components/helm/HelmView.tsx +2 -3
- package/src/components/helm/InstallWizard.tsx +3 -5
- package/src/components/helm/ManifestDiffViewer.tsx +1 -31
- package/src/components/helm/TrackChartSourceDialog.tsx +48 -4
- package/src/components/helm/ValuesDiffPreview.tsx +17 -7
- package/src/components/home/CostCard.tsx +21 -36
- package/src/components/home/HomeView.tsx +17 -15
- package/src/components/home/MCPSetupDialog.tsx +1 -1
- package/src/components/home/mcpToolCatalog.ts +2 -2
- package/src/components/issues/IssuesPane.tsx +9 -1
- package/src/components/logs/ScheduledWorkloadLogsViewer.tsx +135 -0
- package/src/components/portforward/PortForwardButton.tsx +2 -2
- package/src/components/portforward/PortForwardManager.tsx +19 -14
- package/src/components/resource/PrometheusChartsGrid.tsx +6 -80
- package/src/components/resource/RightsizingStrip.test.ts +109 -0
- package/src/components/resource/RightsizingStrip.tsx +320 -127
- package/src/components/resources/ImageFilesystemModal.tsx +2 -2
- package/src/components/resources/PodFilesystemModal.tsx +2 -3
- package/src/components/resources/ResourceDetailDrawer.tsx +2 -0
- package/src/components/resources/ResourcesView.tsx +13 -4
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +1 -0
- package/src/components/resources/renderers/index.ts +1 -0
- package/src/components/rightsizing/RightsizingScanView.tsx +938 -0
- package/src/components/rightsizing/copy.test.ts +56 -0
- package/src/components/rightsizing/model.test.ts +227 -0
- package/src/components/rightsizing/model.ts +158 -0
- package/src/components/rightsizing/presentation.test.ts +104 -0
- package/src/components/rightsizing/presentation.ts +94 -0
- package/src/components/settings/MyPermissionsDialog.tsx +66 -116
- package/src/components/settings/SettingsDialog.tsx +1290 -283
- package/src/components/shared/LargeClusterNamespacePicker.tsx +2 -2
- package/src/components/timeline/LocalTimelineScrubber.tsx +212 -0
- package/src/components/timeline/RetainedTimelineScrubber.tsx +311 -0
- package/src/components/timeline/TimelineList.tsx +81 -13
- package/src/components/timeline/TimelineView.tsx +842 -26
- package/src/components/timeline/TimelineView.urlparams.test.ts +335 -0
- package/src/components/traffic/TrafficFilterSidebar.tsx +2 -2
- package/src/components/traffic/TrafficView.tsx +8 -4
- package/src/components/ui/CommandPalette.tsx +2 -2
- package/src/components/workload/WorkloadView.tsx +912 -245
- package/src/context/ConnectionContext.tsx +109 -11
- package/src/context/DiagnoseCustomization.tsx +42 -0
- package/src/context/TimelineSource.tsx +50 -0
- package/src/hooks/useClusterLoadState.ts +73 -0
- package/src/hooks/useEventSource.ts +6 -0
- package/src/index.css +162 -1
- package/src/index.ts +12 -0
- package/src/main.tsx +1 -1
- package/src/types/clusterLoadState.ts +33 -0
- package/src/utils/navigation.ts +10 -0
package/src/App.tsx
CHANGED
|
@@ -6,7 +6,10 @@ import { useQueryClient } from '@tanstack/react-query'
|
|
|
6
6
|
import { useNavigate, useLocation, useSearchParams, useNavigationType, NavigationType } from 'react-router-dom'
|
|
7
7
|
import { HomeView } from './components/home/HomeView'
|
|
8
8
|
import { DebugOverlay } from './components/DebugOverlay'
|
|
9
|
-
import {
|
|
9
|
+
import { GlobalDiagnoseButton } from './components/diagnose/LocalDiagnoseAction'
|
|
10
|
+
import { useDiagnoseLayout } from './components/diagnose/DiagnoseContext'
|
|
11
|
+
import { DiagnoseSurface } from './components/diagnose/DiagnoseSurface'
|
|
12
|
+
import { TopologyGraph, TopologySearch, TopologyBreadcrumb, TopologyFilterSidebar, TopologyControls, FreshnessControl, gitOpsRouteForKind, gitOpsRouteForResource, ScopePill, PaneLoader } from '@skyhook-io/k8s-ui'
|
|
10
13
|
import { initNavigationMap } from '@skyhook-io/k8s-ui/utils/navigation'
|
|
11
14
|
import { useAPIResources, CORE_RESOURCES } from './api/apiResources'
|
|
12
15
|
import { TimelineView } from './components/timeline/TimelineView'
|
|
@@ -51,15 +54,15 @@ import { routePath, apiUrl, getAuthHeaders, getCredentialsMode } from './api/con
|
|
|
51
54
|
import { KeyboardShortcutProvider, useRegisterShortcut, useRegisterShortcuts, useSuppressBaseShortcuts } from './hooks/useKeyboardShortcuts'
|
|
52
55
|
import { useAnimatedUnmount } from './hooks/useAnimatedUnmount'
|
|
53
56
|
import { useDocumentTitle } from './hooks/useDocumentTitle'
|
|
54
|
-
import
|
|
55
|
-
import {
|
|
57
|
+
import type { ClusterLoadState } from './types/clusterLoadState'
|
|
58
|
+
import { useClusterLoadState } from './hooks/useClusterLoadState'
|
|
59
|
+
import { Network, List, Clock, Package, Sun, Moon, Activity, Home, Star, Search, Bug, SquareTerminal, ShieldCheck, GitBranch, HelpCircle, Loader2, RefreshCw } from 'lucide-react'
|
|
56
60
|
import { useTheme } from './context/ThemeContext'
|
|
57
61
|
import { Tooltip } from './components/ui/Tooltip'
|
|
58
62
|
import { LargeClusterNamespacePicker } from './components/shared/LargeClusterNamespacePicker'
|
|
59
63
|
import { SettingsDialog } from './components/settings/SettingsDialog'
|
|
60
|
-
import { MyPermissionsDialog } from './components/settings/MyPermissionsDialog'
|
|
61
64
|
import type { TopologyNode, GroupingMode, MainView, SelectedResource, SelectedHelmRelease, NodeKind, TopologyMode, Topology, K8sEvent } from './types'
|
|
62
|
-
import { kindToPlural, pluralToKind, openExternal, apiVersionToGroup,
|
|
65
|
+
import { kindToPlural, pluralToKind, openExternal, apiVersionToGroup, relatedResourcePath, searchHitToSelectedResource } from './utils/navigation'
|
|
63
66
|
import { type OmnibarHandle } from './components/ui/Omnibar'
|
|
64
67
|
import { RadarOmnibar } from './components/ui/RadarOmnibar'
|
|
65
68
|
import type { ContextSwitcherHandle } from './components/ContextSwitcher'
|
|
@@ -68,7 +71,7 @@ import type { ContextSwitcherHandle } from './components/ContextSwitcher'
|
|
|
68
71
|
const ALL_NODE_KINDS: NodeKind[] = [
|
|
69
72
|
'Internet', 'Ingress', 'Gateway', 'HTTPRoute', 'GRPCRoute', 'TCPRoute', 'TLSRoute',
|
|
70
73
|
'Service', 'Deployment', 'Rollout', 'DaemonSet', 'StatefulSet',
|
|
71
|
-
'ReplicaSet', 'Pod', 'PodGroup', 'ConfigMap', 'Secret', 'HorizontalPodAutoscaler', 'Job', 'CronJob', 'PersistentVolumeClaim', 'Namespace',
|
|
74
|
+
'ReplicaSet', 'Pod', 'PodGroup', 'ConfigMap', 'Secret', 'ServiceAccount', 'SealedSecret', 'ServiceMonitor', 'PodMonitor', 'HorizontalPodAutoscaler', 'Job', 'CronJob', 'PersistentVolumeClaim', 'Namespace',
|
|
72
75
|
'Application', 'Kustomization', 'HelmRelease', 'GitRepository',
|
|
73
76
|
'KnativeService', 'KnativeConfiguration', 'KnativeRevision', 'KnativeRoute',
|
|
74
77
|
'Broker', 'Trigger', 'PingSource', 'ApiServerSource', 'ContainerSource', 'SinkBinding', 'Channel',
|
|
@@ -86,6 +89,12 @@ const DEFAULT_VISIBLE_KINDS = ALL_NODE_KINDS.filter(k => k !== 'ReplicaSet')
|
|
|
86
89
|
// Users can re-enable via the filter sidebar.
|
|
87
90
|
const CRD_HIDDEN_BY_DEFAULT = new Set(['GatewayClass', 'IngressClass', 'NodePool', 'NodeClaim', 'NodeClass'])
|
|
88
91
|
|
|
92
|
+
// Top-bar height in px. The body frame's right-side surfaces (AI panel, resource +
|
|
93
|
+
// Helm drawers) all inset their top by this so they sit BELOW the header. 0 in
|
|
94
|
+
// chromeless embeds (the host owns the chrome, no Radar header). Keep in sync with
|
|
95
|
+
// the <header> py/line-height; the drawers historically hardcoded the same 49.
|
|
96
|
+
const APP_HEADER_HEIGHT = 49
|
|
97
|
+
|
|
89
98
|
// CAPI kinds shown in Fleet topology mode (+ Node for Machine→Node edges)
|
|
90
99
|
// Includes core CAPI kinds and all infrastructure provider kinds
|
|
91
100
|
const FLEET_MODE_KINDS = new Set<NodeKind>([
|
|
@@ -143,7 +152,10 @@ function namespaceFilterDisabled(
|
|
|
143
152
|
pathname: string,
|
|
144
153
|
apiResources?: { name: string; kind: string; namespaced: boolean }[],
|
|
145
154
|
): { disabled: boolean; tooltip?: string } {
|
|
146
|
-
if (
|
|
155
|
+
if (
|
|
156
|
+
view === 'cost' &&
|
|
157
|
+
!pathname.startsWith('/cost/rightsizing')
|
|
158
|
+
) {
|
|
147
159
|
return {
|
|
148
160
|
disabled: true,
|
|
149
161
|
tooltip: 'Cost is reported per namespace across the whole cluster — the namespace filter doesn’t apply here.',
|
|
@@ -226,27 +238,10 @@ function AuthBarrier({ authMode }: { authMode: string }) {
|
|
|
226
238
|
|
|
227
239
|
if (authMode === 'oidc') {
|
|
228
240
|
return (
|
|
229
|
-
<
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
alt=""
|
|
234
|
-
aria-hidden
|
|
235
|
-
// Integer offset (50% − 22) — matches the Connecting/Opening splashes;
|
|
236
|
-
// avoids sub-pixel jitter from translate(-50%) on odd-width viewports.
|
|
237
|
-
className="absolute w-11 h-11"
|
|
238
|
-
style={{ left: 'calc(50% - 22px)', top: 'calc(50% - 22px)' }}
|
|
239
|
-
/>
|
|
240
|
-
<div
|
|
241
|
-
className="absolute left-1/2 -translate-x-1/2 text-center"
|
|
242
|
-
style={{ top: 'calc(50% + 34px)' }}
|
|
243
|
-
>
|
|
244
|
-
<p className="whitespace-nowrap text-[17px] font-semibold tracking-tight text-theme-text-primary">
|
|
245
|
-
Redirecting to login…
|
|
246
|
-
</p>
|
|
247
|
-
</div>
|
|
248
|
-
</div>
|
|
249
|
-
</div>
|
|
241
|
+
<PaneLoader
|
|
242
|
+
label="Redirecting to login…"
|
|
243
|
+
className="flex-1 min-h-0 bg-theme-base"
|
|
244
|
+
/>
|
|
250
245
|
)
|
|
251
246
|
}
|
|
252
247
|
|
|
@@ -279,7 +274,13 @@ function peekOwnerKey(pathname: string, search: string): string {
|
|
|
279
274
|
return `${pathname}\n${new URLSearchParams(search).get('app') ?? ''}`
|
|
280
275
|
}
|
|
281
276
|
|
|
282
|
-
|
|
277
|
+
interface AppProps {
|
|
278
|
+
manageDocumentTitle?: boolean
|
|
279
|
+
documentTitleSuffix?: string
|
|
280
|
+
onClusterLoadStateChange?: (state: ClusterLoadState) => void
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterLoadStateChange }: AppProps) {
|
|
283
284
|
const navigate = useNavigate()
|
|
284
285
|
const location = useLocation()
|
|
285
286
|
const navigationType = useNavigationType()
|
|
@@ -287,6 +288,10 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
287
288
|
const capabilities = useCapabilitiesContext()
|
|
288
289
|
const openLocalTerminal = useOpenLocalTerminal()
|
|
289
290
|
const navCustomization = useNavCustomization()
|
|
291
|
+
// The AI panel is an absolute slot in the body frame (the column under the header):
|
|
292
|
+
// it reserves a right gutter on the CONTENT only, so the navbar + nav rail stay
|
|
293
|
+
// static. contentGutter is the docked panel width (0 when closed/overlay/maximized).
|
|
294
|
+
const { open: diagnoseOpen, contentGutter } = useDiagnoseLayout()
|
|
290
295
|
// Hand off to a host-owned URL. The host's `onHostNavigate` (Radar Cloud's
|
|
291
296
|
// cross-tree swap) navigates same-document so the chrome morphs instead of
|
|
292
297
|
// cold-booting; without it we fall back to a hard `window.location` nav.
|
|
@@ -482,6 +487,10 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
482
487
|
const [filterSidebarCollapsed, setFilterSidebarCollapsed] = useState(false)
|
|
483
488
|
// Topology node-search → canvas focus request (nonce lets the same node re-focus)
|
|
484
489
|
const [topologyFocus, setTopologyFocus] = useState<{ id: string; nonce: number } | null>(null)
|
|
490
|
+
// The topology pane element — the search overlay portals into it so its
|
|
491
|
+
// backdrop dims only the pane (not the app) and stays clickable. Callback-ref
|
|
492
|
+
// state so it updates once the pane mounts.
|
|
493
|
+
const [topologyPane, setTopologyPane] = useState<HTMLDivElement | null>(null)
|
|
485
494
|
// Track CRD kinds that have been auto-added to visibleKinds so we don't override user toggles
|
|
486
495
|
const seededCRDKindsRef = useRef<Set<string>>(new Set())
|
|
487
496
|
|
|
@@ -498,7 +507,6 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
498
507
|
|
|
499
508
|
// Settings dialog state
|
|
500
509
|
const [showSettings, setShowSettings] = useState(false)
|
|
501
|
-
const [showMyPermissions, setShowMyPermissions] = useState(false)
|
|
502
510
|
|
|
503
511
|
// Listen for "open-settings" DOM event (used by MCPSetupDialog etc.)
|
|
504
512
|
useEffect(() => {
|
|
@@ -507,6 +515,18 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
507
515
|
return () => window.removeEventListener('radar:open-settings', handler)
|
|
508
516
|
}, [])
|
|
509
517
|
|
|
518
|
+
// Listen for "open-local-terminal" DOM event — the AI surface is portaled above
|
|
519
|
+
// the DockProvider, so it can't call useOpenLocalTerminal directly; it dispatches
|
|
520
|
+
// this instead (mirrors the open-settings pattern).
|
|
521
|
+
useEffect(() => {
|
|
522
|
+
const handler = (e: Event) => {
|
|
523
|
+
const { command, title } = (e as CustomEvent).detail ?? {}
|
|
524
|
+
openLocalTerminal({ initialCommand: command, title })
|
|
525
|
+
}
|
|
526
|
+
window.addEventListener('radar:open-local-terminal', handler)
|
|
527
|
+
return () => window.removeEventListener('radar:open-local-terminal', handler)
|
|
528
|
+
}, [openLocalTerminal])
|
|
529
|
+
|
|
510
530
|
// Diagnostics overlay state
|
|
511
531
|
const [showDiagnostics, setShowDiagnostics] = useState(false)
|
|
512
532
|
|
|
@@ -895,6 +915,18 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
895
915
|
const contentReady = !isSwitching && !authMePending &&
|
|
896
916
|
!(authMe?.authEnabled && !authMe?.username) && connection.state === 'connected'
|
|
897
917
|
|
|
918
|
+
const { clusterLoadState, showHomeClusterLoadFallback, clusterLoadInitial } = useClusterLoadState({
|
|
919
|
+
namespaces,
|
|
920
|
+
mainView,
|
|
921
|
+
chromeless,
|
|
922
|
+
contentReady,
|
|
923
|
+
onClusterLoadStateChange,
|
|
924
|
+
})
|
|
925
|
+
// Suppress the topbar warmup label during the initial dashboard fetch only on
|
|
926
|
+
// Home, where the center "Loading dashboard…" splash already covers it. Off
|
|
927
|
+
// Home there's no splash, so keep the label as the only text cue.
|
|
928
|
+
const showClusterWarmupLabel = clusterLoadState.loading && !(clusterLoadInitial && mainView === 'home')
|
|
929
|
+
|
|
898
930
|
// Query client for cache invalidation
|
|
899
931
|
const queryClient = useQueryClient()
|
|
900
932
|
|
|
@@ -923,8 +955,22 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
923
955
|
updatedKinds: Set<string> // update-only churn → throttled list + dashboard
|
|
924
956
|
timer: number | null
|
|
925
957
|
}>({ updatedKinds: new Set(), timer: null })
|
|
958
|
+
const timelineInvalidationRef = useRef<{ timer: number | null }>({ timer: null })
|
|
926
959
|
|
|
927
960
|
const handleK8sEvent = useCallback((event: K8sEvent) => {
|
|
961
|
+
// The timeline consumes every frame — including the K8s Event kind the
|
|
962
|
+
// resource tiers skip below (warnings like BackOff are timeline content).
|
|
963
|
+
// Its own trailing throttle keeps the live view fresh within seconds
|
|
964
|
+
// while batching bursts into one refetch; the 60s poll on useChanges
|
|
965
|
+
// remains the no-SSE fallback.
|
|
966
|
+
const tl = timelineInvalidationRef.current
|
|
967
|
+
if (tl.timer === null) {
|
|
968
|
+
tl.timer = window.setTimeout(() => {
|
|
969
|
+
queryClient.invalidateQueries({ queryKey: ['changes'] })
|
|
970
|
+
timelineInvalidationRef.current = { timer: null }
|
|
971
|
+
}, 5000)
|
|
972
|
+
}
|
|
973
|
+
|
|
928
974
|
// Skip K8s Event kind — informational, not resource mutations
|
|
929
975
|
if (event.kind === 'Event') return
|
|
930
976
|
|
|
@@ -985,15 +1031,17 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
985
1031
|
useEffect(() => () => {
|
|
986
1032
|
if (fastInvalidationRef.current.timer !== null) clearTimeout(fastInvalidationRef.current.timer)
|
|
987
1033
|
if (slowInvalidationRef.current.timer !== null) clearTimeout(slowInvalidationRef.current.timer)
|
|
1034
|
+
if (timelineInvalidationRef.current.timer !== null) clearTimeout(timelineInvalidationRef.current.timer)
|
|
988
1035
|
fastInvalidationRef.current = { changedKinds: new Set(), structuralKinds: new Set(), secretsChanged: false, timer: null }
|
|
989
1036
|
slowInvalidationRef.current = { updatedKinds: new Set(), timer: null }
|
|
1037
|
+
timelineInvalidationRef.current = { timer: null }
|
|
990
1038
|
}, [])
|
|
991
1039
|
|
|
992
1040
|
// SSE connection for real-time updates — no namespace filter for small/medium clusters (frontend filters).
|
|
993
1041
|
// forceNamespaceFilter is only set for large clusters that require server-side filtering.
|
|
994
1042
|
// Fleet mode uses 'resources' topology on the backend — filtering is client-side
|
|
995
1043
|
const sseMode = topologyMode === 'fleet' ? 'resources' : topologyMode
|
|
996
|
-
const { topology } = useEventSource(namespaces, sseMode as 'resources' | 'traffic', {
|
|
1044
|
+
const { topology, connected: eventStreamConnected, connecting: eventStreamConnecting, reconnect: reconnectEventStream } = useEventSource(namespaces, sseMode as 'resources' | 'traffic', {
|
|
997
1045
|
onContextSwitchComplete: endSwitch,
|
|
998
1046
|
onContextSwitchProgress: updateProgress,
|
|
999
1047
|
onContextChanged: () => {
|
|
@@ -1006,8 +1054,10 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
1006
1054
|
// Cancel any pending SSE-driven invalidation — old cluster's events are irrelevant
|
|
1007
1055
|
if (fastInvalidationRef.current.timer !== null) clearTimeout(fastInvalidationRef.current.timer)
|
|
1008
1056
|
if (slowInvalidationRef.current.timer !== null) clearTimeout(slowInvalidationRef.current.timer)
|
|
1057
|
+
if (timelineInvalidationRef.current.timer !== null) clearTimeout(timelineInvalidationRef.current.timer)
|
|
1009
1058
|
fastInvalidationRef.current = { changedKinds: new Set(), structuralKinds: new Set(), secretsChanged: false, timer: null }
|
|
1010
1059
|
slowInvalidationRef.current = { updatedKinds: new Set(), timer: null }
|
|
1060
|
+
timelineInvalidationRef.current = { timer: null }
|
|
1011
1061
|
|
|
1012
1062
|
// Close any open drawers/overlays — old cluster's resources don't exist on the new one
|
|
1013
1063
|
// (?full=1 is cleared by the URL reset below).
|
|
@@ -1030,8 +1080,6 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
1030
1080
|
},
|
|
1031
1081
|
onK8sEvent: handleK8sEvent,
|
|
1032
1082
|
}, forceNamespaceFilter, showPolicyEffect)
|
|
1033
|
-
const clusterConnected = connection.state === 'connected'
|
|
1034
|
-
|
|
1035
1083
|
// On large clusters (where the server requires namespace filtering), keep
|
|
1036
1084
|
// SSE's server-side filter in lockstep with the user's namespace pick.
|
|
1037
1085
|
// Without this, header switches and deep-link loads can leave SSE filtered
|
|
@@ -1076,6 +1124,28 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
1076
1124
|
// Track CRD discovery status from topology (more direct than cluster-info)
|
|
1077
1125
|
// When discovery completes, topology will auto-update via SSE with new CRD nodes
|
|
1078
1126
|
const crdDiscoveryStatus = topology?.crdDiscoveryStatus
|
|
1127
|
+
const clusterConnectionState = connection.state
|
|
1128
|
+
const clusterConnected = clusterConnectionState === 'connected'
|
|
1129
|
+
const liveUpdatesDisconnected = clusterConnected && !eventStreamConnected && !eventStreamConnecting
|
|
1130
|
+
const headerConnectionLabel =
|
|
1131
|
+
clusterConnectionState === 'disconnected' ? 'Disconnected' :
|
|
1132
|
+
clusterConnectionState === 'connecting' ? 'Connecting' :
|
|
1133
|
+
liveUpdatesDisconnected ? 'Live updates disconnected' :
|
|
1134
|
+
clusterLoadState.loading ? `Connected — ${clusterLoadState.message}` :
|
|
1135
|
+
crdDiscoveryStatus === 'discovering' ? 'Connected — discovering Custom Resources...' :
|
|
1136
|
+
'Connected'
|
|
1137
|
+
const headerConnectionDisplayLabel =
|
|
1138
|
+
clusterConnectionState === 'disconnected' ? 'Disconnected' :
|
|
1139
|
+
clusterConnectionState === 'connecting' ? 'Connecting' :
|
|
1140
|
+
liveUpdatesDisconnected ? 'Live updates disconnected' :
|
|
1141
|
+
showClusterWarmupLabel ? clusterLoadState.message :
|
|
1142
|
+
crdDiscoveryStatus === 'discovering' ? 'Discovering Custom Resources…' :
|
|
1143
|
+
''
|
|
1144
|
+
const showHeaderReconnect =
|
|
1145
|
+
clusterConnectionState === 'disconnected' ||
|
|
1146
|
+
liveUpdatesDisconnected
|
|
1147
|
+
const headerReconnect = clusterConnectionState === 'disconnected' ? retryConnection : reconnectEventStream
|
|
1148
|
+
const headerReconnectPending = clusterConnectionState === 'disconnected' ? isRetrying : false
|
|
1079
1149
|
|
|
1080
1150
|
// Debug: log discovery status changes
|
|
1081
1151
|
useEffect(() => {
|
|
@@ -1516,7 +1586,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
1516
1586
|
content column (min-w-0, shrinkable) would fall below the old desktop
|
|
1517
1587
|
floor at small windows. Embedded mode has no rail → plain 800. */}
|
|
1518
1588
|
<div
|
|
1519
|
-
className=
|
|
1589
|
+
className={`relative flex bg-theme-base ${navCustomization.embedded ? 'h-full min-h-0' : 'h-screen'}`}
|
|
1520
1590
|
style={{ minWidth: 800 + (showNavRail ? (navRailEffectivePinned ? 176 : 56) : 0) }}
|
|
1521
1591
|
>
|
|
1522
1592
|
{showNavRail && (
|
|
@@ -1535,9 +1605,13 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
1535
1605
|
span the content area AFTER the rail rather than the full viewport
|
|
1536
1606
|
under it. `fixed` splashes (connecting/switching) are unaffected. */}
|
|
1537
1607
|
<div className="relative flex flex-col flex-1 min-w-0 h-full">
|
|
1538
|
-
{/* Header — suppressed in chromeless embed; the host owns the chrome.
|
|
1608
|
+
{/* Header — suppressed in chromeless embed; the host owns the chrome.
|
|
1609
|
+
@container: the header's responsive layout keys off its OWN width (≈ viewport
|
|
1610
|
+
− nav rail), not the viewport, so it collapses gracefully on narrow windows.
|
|
1611
|
+
The AI panel docks BELOW the navbar and pushes only the content region, so the
|
|
1612
|
+
navbar is never squeezed by it — these thresholds react to real window width. */}
|
|
1539
1613
|
{!chromeless && (
|
|
1540
|
-
<header className="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">
|
|
1614
|
+
<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">
|
|
1541
1615
|
{/* Left: Logo + Cluster info. In the standalone (nav-rail) layout this
|
|
1542
1616
|
is a FIXED-WIDTH column so the omnibar after it is force-pinned: the
|
|
1543
1617
|
scope pill + status dot can change width (cluster/namespace value)
|
|
@@ -1576,49 +1650,49 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
1576
1650
|
/>
|
|
1577
1651
|
</ScopePill>
|
|
1578
1652
|
)}
|
|
1579
|
-
{/* Connection status — a fixed-size dot (state in the tooltip
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1653
|
+
{/* Connection status — a fixed-size dot (state in the tooltip), an
|
|
1654
|
+
optional reconnect button, and when the header is wide enough
|
|
1655
|
+
(xl+), a label. The label is nowrap and unbounded: it overflows
|
|
1656
|
+
the fixed left column into the empty gap before the centered
|
|
1657
|
+
search box rather than shifting anything (the dot + pill are
|
|
1658
|
+
shrink-0, so layout stays put — the search box never moves).
|
|
1659
|
+
Where the gap is smaller than the label, its tail tucks under
|
|
1660
|
+
the omnibar's solid background. Below xl it's the dot alone
|
|
1661
|
+
unless reconnect is available. */}
|
|
1588
1662
|
<div className="ml-1 flex items-center gap-1.5 shrink-0">
|
|
1589
1663
|
<Tooltip
|
|
1590
|
-
content={
|
|
1591
|
-
!clusterConnected
|
|
1592
|
-
? 'Cluster disconnected — click to reconnect'
|
|
1593
|
-
: crdDiscoveryStatus === 'discovering'
|
|
1594
|
-
? 'Connected — discovering Custom Resources...'
|
|
1595
|
-
: 'Connected'
|
|
1596
|
-
}
|
|
1664
|
+
content={headerConnectionLabel}
|
|
1597
1665
|
delay={100}
|
|
1598
1666
|
position="bottom"
|
|
1599
1667
|
>
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1668
|
+
<span
|
|
1669
|
+
className={`block w-2.5 h-2.5 shrink-0 rounded-full ${
|
|
1670
|
+
clusterConnectionState === 'disconnected' || liveUpdatesDisconnected
|
|
1671
|
+
? 'bg-red-500'
|
|
1672
|
+
: clusterConnectionState === 'connecting' || crdDiscoveryStatus === 'discovering' || clusterLoadState.loading
|
|
1673
|
+
? 'bg-amber-400 animate-pulse'
|
|
1674
|
+
: 'bg-green-500'
|
|
1675
|
+
}`}
|
|
1676
|
+
/>
|
|
1677
|
+
</Tooltip>
|
|
1678
|
+
{showNavRail && headerConnectionDisplayLabel && (
|
|
1679
|
+
<span className="hidden xl:flex items-center gap-1.5 whitespace-nowrap text-[11px] text-theme-text-tertiary">
|
|
1680
|
+
{showClusterWarmupLabel && <Loader2 className="w-3 h-3 animate-spin" />}
|
|
1681
|
+
{headerConnectionDisplayLabel}
|
|
1682
|
+
</span>
|
|
1683
|
+
)}
|
|
1684
|
+
{showHeaderReconnect && (
|
|
1685
|
+
<Tooltip content="Reconnect" delay={100} position="bottom">
|
|
1607
1686
|
<button
|
|
1608
1687
|
type="button"
|
|
1609
|
-
onClick={
|
|
1610
|
-
disabled={
|
|
1611
|
-
aria-label=
|
|
1612
|
-
className="
|
|
1688
|
+
onClick={headerReconnect}
|
|
1689
|
+
disabled={headerReconnectPending}
|
|
1690
|
+
aria-label={clusterConnectionState === 'disconnected' ? 'Reconnect cluster' : 'Reconnect live updates'}
|
|
1691
|
+
className="p-1 text-theme-text-secondary hover:text-theme-text-primary disabled:opacity-50 disabled:pointer-events-none"
|
|
1613
1692
|
>
|
|
1614
|
-
<
|
|
1693
|
+
<RefreshCw className={`w-3 h-3 ${headerReconnectPending ? 'animate-spin' : ''}`} />
|
|
1615
1694
|
</button>
|
|
1616
|
-
|
|
1617
|
-
</Tooltip>
|
|
1618
|
-
{showNavRail && (!clusterConnected || crdDiscoveryStatus === 'discovering') && (
|
|
1619
|
-
<span className="hidden xl:block whitespace-nowrap text-[11px] text-theme-text-tertiary">
|
|
1620
|
-
{!clusterConnected ? 'Disconnected' : 'Discovering Custom Resources…'}
|
|
1621
|
-
</span>
|
|
1695
|
+
</Tooltip>
|
|
1622
1696
|
)}
|
|
1623
1697
|
</div>
|
|
1624
1698
|
{/* Port forwards indicator — shown only when sessions exist */}
|
|
@@ -1630,7 +1704,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
1630
1704
|
navigates via the left rail (showNavRail), so the pill bar is
|
|
1631
1705
|
suppressed there to avoid a duplicate primary nav. */}
|
|
1632
1706
|
{!showNavRail && (
|
|
1633
|
-
<div className="
|
|
1707
|
+
<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">
|
|
1634
1708
|
{([
|
|
1635
1709
|
{ view: 'home' as const, icon: Home, label: 'Home' },
|
|
1636
1710
|
{ view: 'topology' as const, icon: Network, label: 'Topology' },
|
|
@@ -1677,7 +1751,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
1677
1751
|
off-system breakpoint chosen by measurement at the time
|
|
1678
1752
|
of this PR — recompute if the cluster switcher cap or
|
|
1679
1753
|
other left-section chrome changes appreciably. */}
|
|
1680
|
-
<span className="hidden min-[
|
|
1754
|
+
<span className="hidden @min-[1264px]:inline">{label}</span>
|
|
1681
1755
|
</button>
|
|
1682
1756
|
</Tooltip>
|
|
1683
1757
|
))}
|
|
@@ -1692,7 +1766,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
1692
1766
|
"Disconnected" text). Overflowing side content truncates instead of
|
|
1693
1767
|
dragging the box. Same pattern as Radar Hub's ClusterTopBar. */}
|
|
1694
1768
|
{showNavRail && (
|
|
1695
|
-
<div className="hidden
|
|
1769
|
+
<div className="hidden @min-[720px]:flex flex-1 justify-center min-w-0 px-3">
|
|
1696
1770
|
<RadarOmnibar
|
|
1697
1771
|
ref={omnibarRef}
|
|
1698
1772
|
onNavigateView={(view) => setMainView(view)}
|
|
@@ -1733,11 +1807,14 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
1733
1807
|
|
|
1734
1808
|
{/* GitHub star — hidden in embedded mode (not OSS-distribution chrome). */}
|
|
1735
1809
|
{!navCustomization.embedded && (
|
|
1736
|
-
<div className="hidden
|
|
1810
|
+
<div className="hidden @min-[1100px]:block">
|
|
1737
1811
|
<GitHubStarButton />
|
|
1738
1812
|
</div>
|
|
1739
1813
|
)}
|
|
1740
1814
|
|
|
1815
|
+
{/* AI investigations (self-hides when no agent CLI is present) */}
|
|
1816
|
+
<GlobalDiagnoseButton />
|
|
1817
|
+
|
|
1741
1818
|
{/* Local terminal */}
|
|
1742
1819
|
{capabilities.localTerminal && (
|
|
1743
1820
|
<Tooltip content="Open local terminal">
|
|
@@ -1758,7 +1835,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
1758
1835
|
to the host's cookie/backend) and the user would see the theme
|
|
1759
1836
|
bounce on every navigation between host routes and /c/:id. */}
|
|
1760
1837
|
{!navCustomization.embedded && (
|
|
1761
|
-
<div className="hidden
|
|
1838
|
+
<div className="hidden @min-[920px]:flex items-center">
|
|
1762
1839
|
<ThemeToggle />
|
|
1763
1840
|
</div>
|
|
1764
1841
|
)}
|
|
@@ -1799,6 +1876,11 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
1799
1876
|
</header>
|
|
1800
1877
|
)}
|
|
1801
1878
|
|
|
1879
|
+
{/* Body frame — every content state lives here and reflows left of the docked
|
|
1880
|
+
AI panel (an absolute slot in this column). The header + nav rail are OUTSIDE
|
|
1881
|
+
this wrapper, so they never move when the panel opens. */}
|
|
1882
|
+
<div className="relative flex flex-1 flex-col min-h-0" style={{ paddingRight: contentGutter, transition: 'padding-right 0.2s ease' }}>
|
|
1883
|
+
|
|
1802
1884
|
{/* Auth barrier - show when auth is enabled but user is not authenticated */}
|
|
1803
1885
|
{authMe?.authEnabled && !authMe?.username && authMe.authMode === 'proxy' && (
|
|
1804
1886
|
<AuthBarrier authMode="proxy" />
|
|
@@ -1820,95 +1902,60 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
1820
1902
|
Icon is pane-anchored so its screen position matches the
|
|
1821
1903
|
host hub splash across cross-document transitions. */}
|
|
1822
1904
|
{!isSwitching && !(authMe?.authEnabled && !authMe?.username) && connection.state === 'connecting' && (
|
|
1823
|
-
<
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
<div
|
|
1839
|
-
className="absolute left-1/2 -translate-x-1/2 text-center"
|
|
1840
|
-
style={{ top: 'calc(50% + 34px)' }}
|
|
1841
|
-
>
|
|
1842
|
-
{/* 17px semibold matches the other splash surfaces so font
|
|
1843
|
-
weight doesn't visibly swap during hub → cluster
|
|
1844
|
-
transitions. Subtitles below stay smaller/dimmer. */}
|
|
1845
|
-
<p className="whitespace-nowrap text-[17px] font-semibold tracking-tight text-theme-text-primary">
|
|
1846
|
-
Connecting to cluster
|
|
1847
|
-
</p>
|
|
1848
|
-
{connection.context && (
|
|
1849
|
-
<p className="text-sm text-theme-text-secondary mt-1">{connection.context}</p>
|
|
1850
|
-
)}
|
|
1851
|
-
{connection.progressMessage && (
|
|
1852
|
-
<p className="text-xs text-theme-text-tertiary animate-pulse mt-3">
|
|
1853
|
-
{connection.progressMessage}
|
|
1854
|
-
</p>
|
|
1855
|
-
)}
|
|
1856
|
-
</div>
|
|
1857
|
-
</div>
|
|
1858
|
-
</div>
|
|
1905
|
+
<PaneLoader
|
|
1906
|
+
label="Connecting to cluster"
|
|
1907
|
+
className="flex-1 min-h-0 bg-theme-base"
|
|
1908
|
+
>
|
|
1909
|
+
{connection.context && (
|
|
1910
|
+
<span className="mt-1 block text-sm font-normal tracking-normal text-theme-text-secondary">
|
|
1911
|
+
{connection.context}
|
|
1912
|
+
</span>
|
|
1913
|
+
)}
|
|
1914
|
+
{connection.progressMessage && (
|
|
1915
|
+
<span className="mt-3 block text-xs font-normal tracking-normal text-theme-text-tertiary animate-pulse">
|
|
1916
|
+
{connection.progressMessage}
|
|
1917
|
+
</span>
|
|
1918
|
+
)}
|
|
1919
|
+
</PaneLoader>
|
|
1859
1920
|
)}
|
|
1860
1921
|
|
|
1861
|
-
{/* Context switching overlay
|
|
1922
|
+
{/* Context switching overlay */}
|
|
1862
1923
|
{isSwitching && (
|
|
1863
|
-
<
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
<div
|
|
1875
|
-
className="absolute left-1/2 -translate-x-1/2 text-center"
|
|
1876
|
-
style={{ top: 'calc(50% + 34px)' }}
|
|
1877
|
-
>
|
|
1878
|
-
<div className="whitespace-nowrap text-[17px] font-semibold tracking-tight text-theme-text-primary">Switching context</div>
|
|
1879
|
-
{targetContext && (
|
|
1880
|
-
<div className="text-xs mt-2 text-theme-text-tertiary">
|
|
1881
|
-
{targetContext.provider ? (
|
|
1882
|
-
<span className="flex items-center justify-center gap-1.5">
|
|
1883
|
-
<span className="text-blue-400 font-medium">{targetContext.provider}</span>
|
|
1884
|
-
{targetContext.account && (
|
|
1885
|
-
<>
|
|
1886
|
-
<span className="text-theme-text-tertiary/50">•</span>
|
|
1887
|
-
<span>{targetContext.account}</span>
|
|
1888
|
-
</>
|
|
1889
|
-
)}
|
|
1890
|
-
{targetContext.region && (
|
|
1891
|
-
<>
|
|
1892
|
-
<span className="text-theme-text-tertiary/50">•</span>
|
|
1893
|
-
<span>{targetContext.region}</span>
|
|
1894
|
-
</>
|
|
1895
|
-
)}
|
|
1924
|
+
<PaneLoader
|
|
1925
|
+
label="Switching context"
|
|
1926
|
+
className="flex-1 min-h-0 bg-theme-base"
|
|
1927
|
+
>
|
|
1928
|
+
{targetContext && (
|
|
1929
|
+
<span className="mt-2 block text-xs font-normal tracking-normal text-theme-text-tertiary">
|
|
1930
|
+
{targetContext.provider ? (
|
|
1931
|
+
<span className="flex items-center justify-center gap-1.5">
|
|
1932
|
+
<span className="text-blue-400 font-medium">{targetContext.provider}</span>
|
|
1933
|
+
{targetContext.account && (
|
|
1934
|
+
<>
|
|
1896
1935
|
<span className="text-theme-text-tertiary/50">•</span>
|
|
1897
|
-
<span
|
|
1898
|
-
|
|
1899
|
-
) : (
|
|
1900
|
-
<span>{targetContext.raw}</span>
|
|
1936
|
+
<span>{targetContext.account}</span>
|
|
1937
|
+
</>
|
|
1901
1938
|
)}
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1939
|
+
{targetContext.region && (
|
|
1940
|
+
<>
|
|
1941
|
+
<span className="text-theme-text-tertiary/50">•</span>
|
|
1942
|
+
<span>{targetContext.region}</span>
|
|
1943
|
+
</>
|
|
1944
|
+
)}
|
|
1945
|
+
<span className="text-theme-text-tertiary/50">•</span>
|
|
1946
|
+
<span className="text-theme-text-secondary font-medium">{targetContext.clusterName}</span>
|
|
1947
|
+
</span>
|
|
1948
|
+
) : (
|
|
1949
|
+
<span>{targetContext.raw}</span>
|
|
1908
1950
|
)}
|
|
1909
|
-
</
|
|
1910
|
-
|
|
1911
|
-
|
|
1951
|
+
</span>
|
|
1952
|
+
)}
|
|
1953
|
+
{progressMessage && (
|
|
1954
|
+
<span className="mt-3 block text-xs font-normal tracking-normal text-theme-text-tertiary animate-pulse">
|
|
1955
|
+
{progressMessage}
|
|
1956
|
+
</span>
|
|
1957
|
+
)}
|
|
1958
|
+
</PaneLoader>
|
|
1912
1959
|
)}
|
|
1913
1960
|
|
|
1914
1961
|
{/* Main content - only show when connected and authenticated */}
|
|
@@ -1922,6 +1969,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
1922
1969
|
<HomeView
|
|
1923
1970
|
namespaces={namespaces}
|
|
1924
1971
|
topology={topology}
|
|
1972
|
+
fallbackClusterLoadState={showHomeClusterLoadFallback ? clusterLoadState : undefined}
|
|
1925
1973
|
onNavigateToView={setMainView}
|
|
1926
1974
|
onNavigateToResourceKind={(kind, apiGroup, filters) => {
|
|
1927
1975
|
// Navigate to resources view with kind in URL path
|
|
@@ -2014,7 +2062,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
2014
2062
|
}}
|
|
2015
2063
|
/>
|
|
2016
2064
|
|
|
2017
|
-
<div className="flex-1 relative">
|
|
2065
|
+
<div ref={setTopologyPane} className="flex-1 relative">
|
|
2018
2066
|
<TopologyGraph
|
|
2019
2067
|
topology={topologyWithAudit}
|
|
2020
2068
|
viewMode={topologyMode}
|
|
@@ -2025,48 +2073,55 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
2025
2073
|
paused={topologyPaused}
|
|
2026
2074
|
onTogglePause={handleTogglePause}
|
|
2027
2075
|
onMaximizeNamespace={(ns) => setActiveNamespace.mutate({ namespaces: [ns] })}
|
|
2028
|
-
namespaceBreadcrumb={namespaces.length === 1 ? namespaces[0] : undefined}
|
|
2029
|
-
onClearNamespace={namespaces.length >= 1 ? () => setActiveNamespace.mutate({ namespaces: [] }) : undefined}
|
|
2030
2076
|
namespacesKey={namespaces.join(',')}
|
|
2031
2077
|
focusNodeId={topologyFocus?.id}
|
|
2032
2078
|
focusNonce={topologyFocus?.nonce}
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2079
|
+
>
|
|
2080
|
+
{/* Overlay row: left column (namespace breadcrumb over search)
|
|
2081
|
+
+ controls. items-start pins the controls to the top even
|
|
2082
|
+
when the breadcrumb grows the left column; w-full so
|
|
2083
|
+
justify-between spans the canvas. */}
|
|
2084
|
+
<div className="flex w-full items-start justify-between gap-2">
|
|
2085
|
+
<div className="flex flex-col items-start gap-2">
|
|
2086
|
+
{namespaces.length === 1 && (
|
|
2087
|
+
<TopologyBreadcrumb
|
|
2088
|
+
namespace={namespaces[0]}
|
|
2089
|
+
onClear={() => setActiveNamespace.mutate({ namespaces: [] })}
|
|
2090
|
+
/>
|
|
2091
|
+
)}
|
|
2092
|
+
<TopologySearch
|
|
2093
|
+
nodes={filteredTopology?.nodes ?? []}
|
|
2094
|
+
allNodes={topology?.nodes}
|
|
2095
|
+
viewModeLabel={topologyMode === 'fleet' ? 'Fleet' : topologyMode === 'traffic' ? 'Network Flow' : 'Resources'}
|
|
2096
|
+
onNodeSelect={handleNodeClick}
|
|
2097
|
+
onZoomToNode={(id) => setTopologyFocus((prev) => ({ id, nonce: (prev?.nonce ?? 0) + 1 }))}
|
|
2098
|
+
overlayContainer={topologyPane}
|
|
2099
|
+
/>
|
|
2100
|
+
</div>
|
|
2101
|
+
<TopologyControls
|
|
2102
|
+
viewMode={topologyMode}
|
|
2103
|
+
onViewModeChange={(mode) => {
|
|
2104
|
+
setTopologyMode(mode)
|
|
2105
|
+
// Fleet mode: namespace grouping for structure, but expanded (not collapsed chips)
|
|
2106
|
+
if (mode === 'fleet') setGroupingMode('namespace')
|
|
2107
|
+
}}
|
|
2108
|
+
groupingMode={groupingMode}
|
|
2109
|
+
onGroupingModeChange={setGroupingMode}
|
|
2110
|
+
showNoGrouping={hasNamespaceFilter}
|
|
2111
|
+
showPolicyEffect={showPolicyEffect}
|
|
2112
|
+
onShowPolicyEffectChange={setShowPolicyEffect}
|
|
2113
|
+
showFleetMode={displayedTopology?.nodes?.some(n => FLEET_MODE_KINDS.has(n.kind as NodeKind)) ?? false}
|
|
2114
|
+
onNavigateToTraffic={() => setMainView('traffic')}
|
|
2115
|
+
leadingSlot={
|
|
2116
|
+
<FreshnessControl
|
|
2117
|
+
mode="auto"
|
|
2118
|
+
paused={topologyPaused}
|
|
2119
|
+
connectionState={connection.state}
|
|
2120
|
+
/>
|
|
2121
|
+
}
|
|
2067
2122
|
/>
|
|
2068
|
-
|
|
2069
|
-
|
|
2123
|
+
</div>
|
|
2124
|
+
</TopologyGraph>
|
|
2070
2125
|
</div>
|
|
2071
2126
|
</>
|
|
2072
2127
|
)}
|
|
@@ -2090,7 +2145,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
2090
2145
|
<TimelineView
|
|
2091
2146
|
namespaces={namespaces}
|
|
2092
2147
|
onResourceClick={(resource) => {
|
|
2093
|
-
navigate(
|
|
2148
|
+
navigate(relatedResourcePath(resource))
|
|
2094
2149
|
}}
|
|
2095
2150
|
initialViewMode={(searchParams.get('view') as 'list' | 'swimlane') || undefined}
|
|
2096
2151
|
initialFilter={(searchParams.get('filter') as 'all' | 'changes' | 'k8s_events' | 'warnings' | 'unhealthy') || undefined}
|
|
@@ -2128,6 +2183,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
2128
2183
|
navigateToResource(resource)
|
|
2129
2184
|
}}
|
|
2130
2185
|
onClearNamespaces={clearAllNamespaces}
|
|
2186
|
+
onOpenSettings={() => setShowSettings(true)}
|
|
2131
2187
|
/>
|
|
2132
2188
|
)}
|
|
2133
2189
|
|
|
@@ -2141,9 +2197,10 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
2141
2197
|
// detail panel) sits behind the peek. Search-only change keeps the
|
|
2142
2198
|
// pathname — and thus the peek's owner-path — intact.
|
|
2143
2199
|
const params = new URLSearchParams(window.location.search)
|
|
2144
|
-
if (params.has('workload') || params.has('tab')) {
|
|
2200
|
+
if (params.has('workload') || params.has('tab') || params.has('run')) {
|
|
2145
2201
|
params.delete('workload')
|
|
2146
2202
|
params.delete('tab')
|
|
2203
|
+
params.delete('run')
|
|
2147
2204
|
navigate({ pathname: window.location.pathname, search: params.toString() }, { replace: true })
|
|
2148
2205
|
}
|
|
2149
2206
|
navigateToResource(resource)
|
|
@@ -2158,7 +2215,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
2158
2215
|
|
|
2159
2216
|
{/* Cost detail view */}
|
|
2160
2217
|
{mainView === 'cost' && (
|
|
2161
|
-
<CostView onBack={() => setMainView('home')} />
|
|
2218
|
+
<CostView namespaces={namespaces} onBack={() => setMainView('home')} onOpenResource={navigateToResource} />
|
|
2162
2219
|
)}
|
|
2163
2220
|
|
|
2164
2221
|
{/* Takeover splash. When the host claims the current view via
|
|
@@ -2167,27 +2224,10 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
2167
2224
|
own fetches) while the cross-document nav lands. Covers checks /
|
|
2168
2225
|
issues / gitops with one block since only one view is active. */}
|
|
2169
2226
|
{viewTakeoverHref && (
|
|
2170
|
-
<
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
<img
|
|
2175
|
-
src={radarLoadingIcon}
|
|
2176
|
-
alt=""
|
|
2177
|
-
aria-hidden
|
|
2178
|
-
className="absolute w-11 h-11"
|
|
2179
|
-
style={{ left: 'calc(50% - 22px)', top: 'calc(50% - 22px)' }}
|
|
2180
|
-
/>
|
|
2181
|
-
<div
|
|
2182
|
-
className="absolute left-1/2 -translate-x-1/2 text-center"
|
|
2183
|
-
style={{ top: 'calc(50% + 34px)' }}
|
|
2184
|
-
>
|
|
2185
|
-
<p className="whitespace-nowrap text-[17px] font-semibold tracking-tight text-theme-text-primary">
|
|
2186
|
-
Opening…
|
|
2187
|
-
</p>
|
|
2188
|
-
</div>
|
|
2189
|
-
</div>
|
|
2190
|
-
</div>
|
|
2227
|
+
<PaneLoader
|
|
2228
|
+
label="Opening…"
|
|
2229
|
+
className="flex-1 min-h-0 bg-theme-base"
|
|
2230
|
+
/>
|
|
2191
2231
|
)}
|
|
2192
2232
|
|
|
2193
2233
|
{/* Best practices detail view (inline only when the host hasn't taken
|
|
@@ -2217,7 +2257,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
2217
2257
|
{mainView === 'workload' && (
|
|
2218
2258
|
<WorkloadViewRoute
|
|
2219
2259
|
onNavigateToResource={(resource) => {
|
|
2220
|
-
navigate(
|
|
2260
|
+
navigate(relatedResourcePath(resource))
|
|
2221
2261
|
}}
|
|
2222
2262
|
/>
|
|
2223
2263
|
)}
|
|
@@ -2227,6 +2267,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
2227
2267
|
|
|
2228
2268
|
</ErrorBoundary>
|
|
2229
2269
|
</div>}
|
|
2270
|
+
</div>{/* /body frame */}
|
|
2230
2271
|
|
|
2231
2272
|
{/* Resource detail drawer — stays mounted, expands to full-screen WorkloadView.
|
|
2232
2273
|
Gated on contentReady so it never renders over the connecting/switching
|
|
@@ -2238,6 +2279,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
2238
2279
|
// No Radar header in chromeless embeds (Radar Hub) — anchor the drawer
|
|
2239
2280
|
// to the top of the content area instead of leaving a 49px gap.
|
|
2240
2281
|
headerHeight={chromeless ? 0 : undefined}
|
|
2282
|
+
rightInset={contentGutter}
|
|
2241
2283
|
isOpen={resourceDrawer.isOpen}
|
|
2242
2284
|
expanded={drawerExpandedProp}
|
|
2243
2285
|
onClose={closeDrawer}
|
|
@@ -2279,6 +2321,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
2279
2321
|
<HelmReleaseDrawer
|
|
2280
2322
|
release={drawerHelmRelease}
|
|
2281
2323
|
isOpen={helmDrawer.isOpen}
|
|
2324
|
+
rightInset={contentGutter}
|
|
2282
2325
|
onClose={() => {
|
|
2283
2326
|
setSelectedHelmRelease(null)
|
|
2284
2327
|
const params = new URLSearchParams(window.location.search)
|
|
@@ -2298,6 +2341,12 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
2298
2341
|
/>
|
|
2299
2342
|
)}
|
|
2300
2343
|
|
|
2344
|
+
{/* AI investigation panel — an absolute slot in this column (the body frame),
|
|
2345
|
+
below the header and right of the nav rail, sharing the frame with the
|
|
2346
|
+
drawers above. Docked = right slot (pushes content via contentGutter);
|
|
2347
|
+
maximized = fills the frame. */}
|
|
2348
|
+
{diagnoseOpen && <DiagnoseSurface topInset={chromeless ? 0 : APP_HEADER_HEIGHT} />}
|
|
2349
|
+
|
|
2301
2350
|
{/* Port Forward floating panel (indicator lives in header) */}
|
|
2302
2351
|
<PortForwardPanel />
|
|
2303
2352
|
|
|
@@ -2362,16 +2411,11 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
2362
2411
|
{/* Diagnostics overlay */}
|
|
2363
2412
|
{diagnosticsOverlay.shouldRender && <DiagnosticsOverlay isOpen={diagnosticsOverlay.isOpen} onClose={() => setShowDiagnostics(false)} />}
|
|
2364
2413
|
|
|
2365
|
-
{/* Settings dialog */}
|
|
2414
|
+
{/* Settings dialog — My permissions is rendered inline in its own section */}
|
|
2366
2415
|
<SettingsDialog
|
|
2367
2416
|
open={showSettings}
|
|
2368
2417
|
onClose={() => setShowSettings(false)}
|
|
2369
|
-
onShowMyPermissions={() => {
|
|
2370
|
-
setShowSettings(false)
|
|
2371
|
-
setShowMyPermissions(true)
|
|
2372
|
-
}}
|
|
2373
2418
|
/>
|
|
2374
|
-
<MyPermissionsDialog open={showMyPermissions} onClose={() => setShowMyPermissions(false)} />
|
|
2375
2419
|
|
|
2376
2420
|
{/* Debug overlay — dev mode, standalone only. Embedded hosts (Radar Hub)
|
|
2377
2421
|
own their own dev tooling; ours would collide with theirs bottom-right. */}
|
|
@@ -2424,14 +2468,18 @@ function FloatingButtons({ showHelp, showCommandPalette, showDiagnostics, onHelp
|
|
|
2424
2468
|
}
|
|
2425
2469
|
|
|
2426
2470
|
// Main App component wrapped with providers
|
|
2427
|
-
function App({ manageDocumentTitle = false, documentTitleSuffix }:
|
|
2471
|
+
function App({ manageDocumentTitle = false, documentTitleSuffix, onClusterLoadStateChange }: AppProps) {
|
|
2428
2472
|
return (
|
|
2429
2473
|
<ConnectionProvider>
|
|
2430
2474
|
<CapabilitiesProvider>
|
|
2431
2475
|
<ContextSwitchProvider>
|
|
2432
2476
|
<DockProvider>
|
|
2433
2477
|
<KeyboardShortcutProvider>
|
|
2434
|
-
<AppInner
|
|
2478
|
+
<AppInner
|
|
2479
|
+
manageDocumentTitle={manageDocumentTitle}
|
|
2480
|
+
documentTitleSuffix={documentTitleSuffix}
|
|
2481
|
+
onClusterLoadStateChange={onClusterLoadStateChange}
|
|
2482
|
+
/>
|
|
2435
2483
|
</KeyboardShortcutProvider>
|
|
2436
2484
|
</DockProvider>
|
|
2437
2485
|
</ContextSwitchProvider>
|