@skyhook-io/radar-app 1.8.5 → 1.8.7
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 +11 -9
- package/src/App.tsx +229 -183
- package/src/RadarApp.tsx +91 -20
- package/src/api/client.delta.test.ts +89 -0
- package/src/api/client.deltaSync.test.ts +216 -0
- package/src/api/client.metrics.test.ts +106 -0
- package/src/api/client.ts +377 -48
- package/src/api/diagnose.ts +288 -0
- package/src/api/timelineSource.test.ts +217 -0
- package/src/api/timelineSource.ts +580 -0
- package/src/components/ConnectionErrorView.tsx +174 -70
- package/src/components/ContextSwitcher.tsx +13 -5
- package/src/components/applications/ApplicationsView.tsx +336 -32
- package/src/components/audit/AuditSettingsDialog.tsx +2 -2
- package/src/components/curl/ServiceCurlButton.tsx +2 -2
- package/src/components/diagnose/AISettings.tsx +121 -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/helm/ChartBrowser.tsx +2 -3
- 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/TrackChartSourceDialog.tsx +48 -4
- package/src/components/helm/ValuesDiffPreview.tsx +15 -4
- package/src/components/home/HomeView.tsx +22 -18
- 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.tsx +0 -3
- 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/NodeRenderer.tsx +10 -4
- package/src/components/resources/renderers/PodRenderer.tsx +10 -4
- package/src/components/resources/renderers/index.ts +1 -0
- package/src/components/settings/SettingsDialog.tsx +80 -23
- 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 +51 -10
- package/src/components/timeline/TimelineView.tsx +701 -15
- package/src/components/timeline/TimelineView.urlparams.test.ts +294 -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 +319 -35
- 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 +157 -0
- package/src/index.ts +12 -0
- 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, 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,16 @@ 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
64
|
import { MyPermissionsDialog } from './components/settings/MyPermissionsDialog'
|
|
61
65
|
import type { TopologyNode, GroupingMode, MainView, SelectedResource, SelectedHelmRelease, NodeKind, TopologyMode, Topology, K8sEvent } from './types'
|
|
62
|
-
import { kindToPlural, pluralToKind, openExternal, apiVersionToGroup,
|
|
66
|
+
import { kindToPlural, pluralToKind, openExternal, apiVersionToGroup, relatedResourcePath, searchHitToSelectedResource } from './utils/navigation'
|
|
63
67
|
import { type OmnibarHandle } from './components/ui/Omnibar'
|
|
64
68
|
import { RadarOmnibar } from './components/ui/RadarOmnibar'
|
|
65
69
|
import type { ContextSwitcherHandle } from './components/ContextSwitcher'
|
|
@@ -68,7 +72,7 @@ import type { ContextSwitcherHandle } from './components/ContextSwitcher'
|
|
|
68
72
|
const ALL_NODE_KINDS: NodeKind[] = [
|
|
69
73
|
'Internet', 'Ingress', 'Gateway', 'HTTPRoute', 'GRPCRoute', 'TCPRoute', 'TLSRoute',
|
|
70
74
|
'Service', 'Deployment', 'Rollout', 'DaemonSet', 'StatefulSet',
|
|
71
|
-
'ReplicaSet', 'Pod', 'PodGroup', 'ConfigMap', 'Secret', 'HorizontalPodAutoscaler', 'Job', 'CronJob', 'PersistentVolumeClaim', 'Namespace',
|
|
75
|
+
'ReplicaSet', 'Pod', 'PodGroup', 'ConfigMap', 'Secret', 'ServiceAccount', 'SealedSecret', 'ServiceMonitor', 'PodMonitor', 'HorizontalPodAutoscaler', 'Job', 'CronJob', 'PersistentVolumeClaim', 'Namespace',
|
|
72
76
|
'Application', 'Kustomization', 'HelmRelease', 'GitRepository',
|
|
73
77
|
'KnativeService', 'KnativeConfiguration', 'KnativeRevision', 'KnativeRoute',
|
|
74
78
|
'Broker', 'Trigger', 'PingSource', 'ApiServerSource', 'ContainerSource', 'SinkBinding', 'Channel',
|
|
@@ -86,6 +90,12 @@ const DEFAULT_VISIBLE_KINDS = ALL_NODE_KINDS.filter(k => k !== 'ReplicaSet')
|
|
|
86
90
|
// Users can re-enable via the filter sidebar.
|
|
87
91
|
const CRD_HIDDEN_BY_DEFAULT = new Set(['GatewayClass', 'IngressClass', 'NodePool', 'NodeClaim', 'NodeClass'])
|
|
88
92
|
|
|
93
|
+
// Top-bar height in px. The body frame's right-side surfaces (AI panel, resource +
|
|
94
|
+
// Helm drawers) all inset their top by this so they sit BELOW the header. 0 in
|
|
95
|
+
// chromeless embeds (the host owns the chrome, no Radar header). Keep in sync with
|
|
96
|
+
// the <header> py/line-height; the drawers historically hardcoded the same 49.
|
|
97
|
+
const APP_HEADER_HEIGHT = 49
|
|
98
|
+
|
|
89
99
|
// CAPI kinds shown in Fleet topology mode (+ Node for Machine→Node edges)
|
|
90
100
|
// Includes core CAPI kinds and all infrastructure provider kinds
|
|
91
101
|
const FLEET_MODE_KINDS = new Set<NodeKind>([
|
|
@@ -226,27 +236,10 @@ function AuthBarrier({ authMode }: { authMode: string }) {
|
|
|
226
236
|
|
|
227
237
|
if (authMode === 'oidc') {
|
|
228
238
|
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>
|
|
239
|
+
<PaneLoader
|
|
240
|
+
label="Redirecting to login…"
|
|
241
|
+
className="flex-1 min-h-0 bg-theme-base"
|
|
242
|
+
/>
|
|
250
243
|
)
|
|
251
244
|
}
|
|
252
245
|
|
|
@@ -279,7 +272,13 @@ function peekOwnerKey(pathname: string, search: string): string {
|
|
|
279
272
|
return `${pathname}\n${new URLSearchParams(search).get('app') ?? ''}`
|
|
280
273
|
}
|
|
281
274
|
|
|
282
|
-
|
|
275
|
+
interface AppProps {
|
|
276
|
+
manageDocumentTitle?: boolean
|
|
277
|
+
documentTitleSuffix?: string
|
|
278
|
+
onClusterLoadStateChange?: (state: ClusterLoadState) => void
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterLoadStateChange }: AppProps) {
|
|
283
282
|
const navigate = useNavigate()
|
|
284
283
|
const location = useLocation()
|
|
285
284
|
const navigationType = useNavigationType()
|
|
@@ -287,6 +286,10 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
287
286
|
const capabilities = useCapabilitiesContext()
|
|
288
287
|
const openLocalTerminal = useOpenLocalTerminal()
|
|
289
288
|
const navCustomization = useNavCustomization()
|
|
289
|
+
// The AI panel is an absolute slot in the body frame (the column under the header):
|
|
290
|
+
// it reserves a right gutter on the CONTENT only, so the navbar + nav rail stay
|
|
291
|
+
// static. contentGutter is the docked panel width (0 when closed/overlay/maximized).
|
|
292
|
+
const { open: diagnoseOpen, contentGutter } = useDiagnoseLayout()
|
|
290
293
|
// Hand off to a host-owned URL. The host's `onHostNavigate` (Radar Cloud's
|
|
291
294
|
// cross-tree swap) navigates same-document so the chrome morphs instead of
|
|
292
295
|
// cold-booting; without it we fall back to a hard `window.location` nav.
|
|
@@ -507,6 +510,18 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
507
510
|
return () => window.removeEventListener('radar:open-settings', handler)
|
|
508
511
|
}, [])
|
|
509
512
|
|
|
513
|
+
// Listen for "open-local-terminal" DOM event — the AI surface is portaled above
|
|
514
|
+
// the DockProvider, so it can't call useOpenLocalTerminal directly; it dispatches
|
|
515
|
+
// this instead (mirrors the open-settings pattern).
|
|
516
|
+
useEffect(() => {
|
|
517
|
+
const handler = (e: Event) => {
|
|
518
|
+
const { command, title } = (e as CustomEvent).detail ?? {}
|
|
519
|
+
openLocalTerminal({ initialCommand: command, title })
|
|
520
|
+
}
|
|
521
|
+
window.addEventListener('radar:open-local-terminal', handler)
|
|
522
|
+
return () => window.removeEventListener('radar:open-local-terminal', handler)
|
|
523
|
+
}, [openLocalTerminal])
|
|
524
|
+
|
|
510
525
|
// Diagnostics overlay state
|
|
511
526
|
const [showDiagnostics, setShowDiagnostics] = useState(false)
|
|
512
527
|
|
|
@@ -895,6 +910,18 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
895
910
|
const contentReady = !isSwitching && !authMePending &&
|
|
896
911
|
!(authMe?.authEnabled && !authMe?.username) && connection.state === 'connected'
|
|
897
912
|
|
|
913
|
+
const { clusterLoadState, showHomeClusterLoadFallback, clusterLoadInitial } = useClusterLoadState({
|
|
914
|
+
namespaces,
|
|
915
|
+
mainView,
|
|
916
|
+
chromeless,
|
|
917
|
+
contentReady,
|
|
918
|
+
onClusterLoadStateChange,
|
|
919
|
+
})
|
|
920
|
+
// Suppress the topbar warmup label during the initial dashboard fetch only on
|
|
921
|
+
// Home, where the center "Loading dashboard…" splash already covers it. Off
|
|
922
|
+
// Home there's no splash, so keep the label as the only text cue.
|
|
923
|
+
const showClusterWarmupLabel = clusterLoadState.loading && !(clusterLoadInitial && mainView === 'home')
|
|
924
|
+
|
|
898
925
|
// Query client for cache invalidation
|
|
899
926
|
const queryClient = useQueryClient()
|
|
900
927
|
|
|
@@ -923,8 +950,22 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
923
950
|
updatedKinds: Set<string> // update-only churn → throttled list + dashboard
|
|
924
951
|
timer: number | null
|
|
925
952
|
}>({ updatedKinds: new Set(), timer: null })
|
|
953
|
+
const timelineInvalidationRef = useRef<{ timer: number | null }>({ timer: null })
|
|
926
954
|
|
|
927
955
|
const handleK8sEvent = useCallback((event: K8sEvent) => {
|
|
956
|
+
// The timeline consumes every frame — including the K8s Event kind the
|
|
957
|
+
// resource tiers skip below (warnings like BackOff are timeline content).
|
|
958
|
+
// Its own trailing throttle keeps the live view fresh within seconds
|
|
959
|
+
// while batching bursts into one refetch; the 60s poll on useChanges
|
|
960
|
+
// remains the no-SSE fallback.
|
|
961
|
+
const tl = timelineInvalidationRef.current
|
|
962
|
+
if (tl.timer === null) {
|
|
963
|
+
tl.timer = window.setTimeout(() => {
|
|
964
|
+
queryClient.invalidateQueries({ queryKey: ['changes'] })
|
|
965
|
+
timelineInvalidationRef.current = { timer: null }
|
|
966
|
+
}, 5000)
|
|
967
|
+
}
|
|
968
|
+
|
|
928
969
|
// Skip K8s Event kind — informational, not resource mutations
|
|
929
970
|
if (event.kind === 'Event') return
|
|
930
971
|
|
|
@@ -985,15 +1026,17 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
985
1026
|
useEffect(() => () => {
|
|
986
1027
|
if (fastInvalidationRef.current.timer !== null) clearTimeout(fastInvalidationRef.current.timer)
|
|
987
1028
|
if (slowInvalidationRef.current.timer !== null) clearTimeout(slowInvalidationRef.current.timer)
|
|
1029
|
+
if (timelineInvalidationRef.current.timer !== null) clearTimeout(timelineInvalidationRef.current.timer)
|
|
988
1030
|
fastInvalidationRef.current = { changedKinds: new Set(), structuralKinds: new Set(), secretsChanged: false, timer: null }
|
|
989
1031
|
slowInvalidationRef.current = { updatedKinds: new Set(), timer: null }
|
|
1032
|
+
timelineInvalidationRef.current = { timer: null }
|
|
990
1033
|
}, [])
|
|
991
1034
|
|
|
992
1035
|
// SSE connection for real-time updates — no namespace filter for small/medium clusters (frontend filters).
|
|
993
1036
|
// forceNamespaceFilter is only set for large clusters that require server-side filtering.
|
|
994
1037
|
// Fleet mode uses 'resources' topology on the backend — filtering is client-side
|
|
995
1038
|
const sseMode = topologyMode === 'fleet' ? 'resources' : topologyMode
|
|
996
|
-
const { topology } = useEventSource(namespaces, sseMode as 'resources' | 'traffic', {
|
|
1039
|
+
const { topology, connected: eventStreamConnected, connecting: eventStreamConnecting, reconnect: reconnectEventStream } = useEventSource(namespaces, sseMode as 'resources' | 'traffic', {
|
|
997
1040
|
onContextSwitchComplete: endSwitch,
|
|
998
1041
|
onContextSwitchProgress: updateProgress,
|
|
999
1042
|
onContextChanged: () => {
|
|
@@ -1006,8 +1049,10 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
1006
1049
|
// Cancel any pending SSE-driven invalidation — old cluster's events are irrelevant
|
|
1007
1050
|
if (fastInvalidationRef.current.timer !== null) clearTimeout(fastInvalidationRef.current.timer)
|
|
1008
1051
|
if (slowInvalidationRef.current.timer !== null) clearTimeout(slowInvalidationRef.current.timer)
|
|
1052
|
+
if (timelineInvalidationRef.current.timer !== null) clearTimeout(timelineInvalidationRef.current.timer)
|
|
1009
1053
|
fastInvalidationRef.current = { changedKinds: new Set(), structuralKinds: new Set(), secretsChanged: false, timer: null }
|
|
1010
1054
|
slowInvalidationRef.current = { updatedKinds: new Set(), timer: null }
|
|
1055
|
+
timelineInvalidationRef.current = { timer: null }
|
|
1011
1056
|
|
|
1012
1057
|
// Close any open drawers/overlays — old cluster's resources don't exist on the new one
|
|
1013
1058
|
// (?full=1 is cleared by the URL reset below).
|
|
@@ -1030,8 +1075,6 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
1030
1075
|
},
|
|
1031
1076
|
onK8sEvent: handleK8sEvent,
|
|
1032
1077
|
}, forceNamespaceFilter, showPolicyEffect)
|
|
1033
|
-
const clusterConnected = connection.state === 'connected'
|
|
1034
|
-
|
|
1035
1078
|
// On large clusters (where the server requires namespace filtering), keep
|
|
1036
1079
|
// SSE's server-side filter in lockstep with the user's namespace pick.
|
|
1037
1080
|
// Without this, header switches and deep-link loads can leave SSE filtered
|
|
@@ -1076,6 +1119,28 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
1076
1119
|
// Track CRD discovery status from topology (more direct than cluster-info)
|
|
1077
1120
|
// When discovery completes, topology will auto-update via SSE with new CRD nodes
|
|
1078
1121
|
const crdDiscoveryStatus = topology?.crdDiscoveryStatus
|
|
1122
|
+
const clusterConnectionState = connection.state
|
|
1123
|
+
const clusterConnected = clusterConnectionState === 'connected'
|
|
1124
|
+
const liveUpdatesDisconnected = clusterConnected && !eventStreamConnected && !eventStreamConnecting
|
|
1125
|
+
const headerConnectionLabel =
|
|
1126
|
+
clusterConnectionState === 'disconnected' ? 'Disconnected' :
|
|
1127
|
+
clusterConnectionState === 'connecting' ? 'Connecting' :
|
|
1128
|
+
liveUpdatesDisconnected ? 'Live updates disconnected' :
|
|
1129
|
+
clusterLoadState.loading ? `Connected — ${clusterLoadState.message}` :
|
|
1130
|
+
crdDiscoveryStatus === 'discovering' ? 'Connected — discovering Custom Resources...' :
|
|
1131
|
+
'Connected'
|
|
1132
|
+
const headerConnectionDisplayLabel =
|
|
1133
|
+
clusterConnectionState === 'disconnected' ? 'Disconnected' :
|
|
1134
|
+
clusterConnectionState === 'connecting' ? 'Connecting' :
|
|
1135
|
+
liveUpdatesDisconnected ? 'Live updates disconnected' :
|
|
1136
|
+
showClusterWarmupLabel ? clusterLoadState.message :
|
|
1137
|
+
crdDiscoveryStatus === 'discovering' ? 'Discovering Custom Resources…' :
|
|
1138
|
+
''
|
|
1139
|
+
const showHeaderReconnect =
|
|
1140
|
+
clusterConnectionState === 'disconnected' ||
|
|
1141
|
+
liveUpdatesDisconnected
|
|
1142
|
+
const headerReconnect = clusterConnectionState === 'disconnected' ? retryConnection : reconnectEventStream
|
|
1143
|
+
const headerReconnectPending = clusterConnectionState === 'disconnected' ? isRetrying : false
|
|
1079
1144
|
|
|
1080
1145
|
// Debug: log discovery status changes
|
|
1081
1146
|
useEffect(() => {
|
|
@@ -1516,7 +1581,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
1516
1581
|
content column (min-w-0, shrinkable) would fall below the old desktop
|
|
1517
1582
|
floor at small windows. Embedded mode has no rail → plain 800. */}
|
|
1518
1583
|
<div
|
|
1519
|
-
className=
|
|
1584
|
+
className={`relative flex bg-theme-base ${navCustomization.embedded ? 'h-full min-h-0' : 'h-screen'}`}
|
|
1520
1585
|
style={{ minWidth: 800 + (showNavRail ? (navRailEffectivePinned ? 176 : 56) : 0) }}
|
|
1521
1586
|
>
|
|
1522
1587
|
{showNavRail && (
|
|
@@ -1535,9 +1600,13 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
1535
1600
|
span the content area AFTER the rail rather than the full viewport
|
|
1536
1601
|
under it. `fixed` splashes (connecting/switching) are unaffected. */}
|
|
1537
1602
|
<div className="relative flex flex-col flex-1 min-w-0 h-full">
|
|
1538
|
-
{/* Header — suppressed in chromeless embed; the host owns the chrome.
|
|
1603
|
+
{/* Header — suppressed in chromeless embed; the host owns the chrome.
|
|
1604
|
+
@container: the header's responsive layout keys off its OWN width (≈ viewport
|
|
1605
|
+
− nav rail), not the viewport, so it collapses gracefully on narrow windows.
|
|
1606
|
+
The AI panel docks BELOW the navbar and pushes only the content region, so the
|
|
1607
|
+
navbar is never squeezed by it — these thresholds react to real window width. */}
|
|
1539
1608
|
{!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">
|
|
1609
|
+
<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
1610
|
{/* Left: Logo + Cluster info. In the standalone (nav-rail) layout this
|
|
1542
1611
|
is a FIXED-WIDTH column so the omnibar after it is force-pinned: the
|
|
1543
1612
|
scope pill + status dot can change width (cluster/namespace value)
|
|
@@ -1576,49 +1645,49 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
1576
1645
|
/>
|
|
1577
1646
|
</ScopePill>
|
|
1578
1647
|
)}
|
|
1579
|
-
{/* Connection status — a fixed-size dot (state in the tooltip
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1648
|
+
{/* Connection status — a fixed-size dot (state in the tooltip), an
|
|
1649
|
+
optional reconnect button, and when the header is wide enough
|
|
1650
|
+
(xl+), a label. The label is nowrap and unbounded: it overflows
|
|
1651
|
+
the fixed left column into the empty gap before the centered
|
|
1652
|
+
search box rather than shifting anything (the dot + pill are
|
|
1653
|
+
shrink-0, so layout stays put — the search box never moves).
|
|
1654
|
+
Where the gap is smaller than the label, its tail tucks under
|
|
1655
|
+
the omnibar's solid background. Below xl it's the dot alone
|
|
1656
|
+
unless reconnect is available. */}
|
|
1588
1657
|
<div className="ml-1 flex items-center gap-1.5 shrink-0">
|
|
1589
1658
|
<Tooltip
|
|
1590
|
-
content={
|
|
1591
|
-
!clusterConnected
|
|
1592
|
-
? 'Cluster disconnected — click to reconnect'
|
|
1593
|
-
: crdDiscoveryStatus === 'discovering'
|
|
1594
|
-
? 'Connected — discovering Custom Resources...'
|
|
1595
|
-
: 'Connected'
|
|
1596
|
-
}
|
|
1659
|
+
content={headerConnectionLabel}
|
|
1597
1660
|
delay={100}
|
|
1598
1661
|
position="bottom"
|
|
1599
1662
|
>
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1663
|
+
<span
|
|
1664
|
+
className={`block w-2.5 h-2.5 shrink-0 rounded-full ${
|
|
1665
|
+
clusterConnectionState === 'disconnected' || liveUpdatesDisconnected
|
|
1666
|
+
? 'bg-red-500'
|
|
1667
|
+
: clusterConnectionState === 'connecting' || crdDiscoveryStatus === 'discovering' || clusterLoadState.loading
|
|
1668
|
+
? 'bg-amber-400 animate-pulse'
|
|
1669
|
+
: 'bg-green-500'
|
|
1670
|
+
}`}
|
|
1671
|
+
/>
|
|
1672
|
+
</Tooltip>
|
|
1673
|
+
{showNavRail && headerConnectionDisplayLabel && (
|
|
1674
|
+
<span className="hidden xl:flex items-center gap-1.5 whitespace-nowrap text-[11px] text-theme-text-tertiary">
|
|
1675
|
+
{showClusterWarmupLabel && <Loader2 className="w-3 h-3 animate-spin" />}
|
|
1676
|
+
{headerConnectionDisplayLabel}
|
|
1677
|
+
</span>
|
|
1678
|
+
)}
|
|
1679
|
+
{showHeaderReconnect && (
|
|
1680
|
+
<Tooltip content="Reconnect" delay={100} position="bottom">
|
|
1607
1681
|
<button
|
|
1608
1682
|
type="button"
|
|
1609
|
-
onClick={
|
|
1610
|
-
disabled={
|
|
1611
|
-
aria-label=
|
|
1612
|
-
className="
|
|
1683
|
+
onClick={headerReconnect}
|
|
1684
|
+
disabled={headerReconnectPending}
|
|
1685
|
+
aria-label={clusterConnectionState === 'disconnected' ? 'Reconnect cluster' : 'Reconnect live updates'}
|
|
1686
|
+
className="p-1 text-theme-text-secondary hover:text-theme-text-primary disabled:opacity-50 disabled:pointer-events-none"
|
|
1613
1687
|
>
|
|
1614
|
-
<
|
|
1688
|
+
<RefreshCw className={`w-3 h-3 ${headerReconnectPending ? 'animate-spin' : ''}`} />
|
|
1615
1689
|
</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>
|
|
1690
|
+
</Tooltip>
|
|
1622
1691
|
)}
|
|
1623
1692
|
</div>
|
|
1624
1693
|
{/* Port forwards indicator — shown only when sessions exist */}
|
|
@@ -1630,7 +1699,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
1630
1699
|
navigates via the left rail (showNavRail), so the pill bar is
|
|
1631
1700
|
suppressed there to avoid a duplicate primary nav. */}
|
|
1632
1701
|
{!showNavRail && (
|
|
1633
|
-
<div className="
|
|
1702
|
+
<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
1703
|
{([
|
|
1635
1704
|
{ view: 'home' as const, icon: Home, label: 'Home' },
|
|
1636
1705
|
{ view: 'topology' as const, icon: Network, label: 'Topology' },
|
|
@@ -1677,7 +1746,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
1677
1746
|
off-system breakpoint chosen by measurement at the time
|
|
1678
1747
|
of this PR — recompute if the cluster switcher cap or
|
|
1679
1748
|
other left-section chrome changes appreciably. */}
|
|
1680
|
-
<span className="hidden min-[
|
|
1749
|
+
<span className="hidden @min-[1264px]:inline">{label}</span>
|
|
1681
1750
|
</button>
|
|
1682
1751
|
</Tooltip>
|
|
1683
1752
|
))}
|
|
@@ -1692,7 +1761,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
1692
1761
|
"Disconnected" text). Overflowing side content truncates instead of
|
|
1693
1762
|
dragging the box. Same pattern as Radar Hub's ClusterTopBar. */}
|
|
1694
1763
|
{showNavRail && (
|
|
1695
|
-
<div className="hidden
|
|
1764
|
+
<div className="hidden @min-[720px]:flex flex-1 justify-center min-w-0 px-3">
|
|
1696
1765
|
<RadarOmnibar
|
|
1697
1766
|
ref={omnibarRef}
|
|
1698
1767
|
onNavigateView={(view) => setMainView(view)}
|
|
@@ -1721,6 +1790,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
1721
1790
|
{!showNavRail && (
|
|
1722
1791
|
<button
|
|
1723
1792
|
onClick={() => setShowCommandPalette(true)}
|
|
1793
|
+
aria-label="Open command palette"
|
|
1724
1794
|
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"
|
|
1725
1795
|
>
|
|
1726
1796
|
<Search className="w-3.5 h-3.5" />
|
|
@@ -1732,16 +1802,20 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
1732
1802
|
|
|
1733
1803
|
{/* GitHub star — hidden in embedded mode (not OSS-distribution chrome). */}
|
|
1734
1804
|
{!navCustomization.embedded && (
|
|
1735
|
-
<div className="hidden
|
|
1805
|
+
<div className="hidden @min-[1100px]:block">
|
|
1736
1806
|
<GitHubStarButton />
|
|
1737
1807
|
</div>
|
|
1738
1808
|
)}
|
|
1739
1809
|
|
|
1810
|
+
{/* AI investigations (self-hides when no agent CLI is present) */}
|
|
1811
|
+
<GlobalDiagnoseButton />
|
|
1812
|
+
|
|
1740
1813
|
{/* Local terminal */}
|
|
1741
1814
|
{capabilities.localTerminal && (
|
|
1742
1815
|
<Tooltip content="Open local terminal">
|
|
1743
1816
|
<button
|
|
1744
1817
|
onClick={() => openLocalTerminal()}
|
|
1818
|
+
aria-label="Open local terminal"
|
|
1745
1819
|
className="p-1.5 rounded-md bg-theme-elevated hover:bg-theme-hover text-theme-text-secondary hover:text-theme-text-primary transition-colors"
|
|
1746
1820
|
>
|
|
1747
1821
|
<SquareTerminal className="w-4 h-4" />
|
|
@@ -1756,7 +1830,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
1756
1830
|
to the host's cookie/backend) and the user would see the theme
|
|
1757
1831
|
bounce on every navigation between host routes and /c/:id. */}
|
|
1758
1832
|
{!navCustomization.embedded && (
|
|
1759
|
-
<div className="hidden
|
|
1833
|
+
<div className="hidden @min-[920px]:flex items-center">
|
|
1760
1834
|
<ThemeToggle />
|
|
1761
1835
|
</div>
|
|
1762
1836
|
)}
|
|
@@ -1769,6 +1843,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
1769
1843
|
<Tooltip content="Keyboard shortcuts (?)">
|
|
1770
1844
|
<button
|
|
1771
1845
|
onClick={() => setShowHelp(true)}
|
|
1846
|
+
aria-label="Show keyboard shortcuts"
|
|
1772
1847
|
className="p-1.5 rounded-md bg-theme-elevated hover:bg-theme-hover text-theme-text-secondary hover:text-theme-text-primary transition-colors"
|
|
1773
1848
|
>
|
|
1774
1849
|
<HelpCircle className="w-4 h-4" />
|
|
@@ -1777,6 +1852,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
1777
1852
|
<Tooltip content="Report a bug / Diagnostics">
|
|
1778
1853
|
<button
|
|
1779
1854
|
onClick={() => setShowDiagnostics(true)}
|
|
1855
|
+
aria-label="Open diagnostics"
|
|
1780
1856
|
className="p-1.5 rounded-md bg-theme-elevated hover:bg-theme-hover text-theme-text-secondary hover:text-theme-text-primary transition-colors"
|
|
1781
1857
|
>
|
|
1782
1858
|
<Bug className="w-4 h-4" />
|
|
@@ -1795,6 +1871,11 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
1795
1871
|
</header>
|
|
1796
1872
|
)}
|
|
1797
1873
|
|
|
1874
|
+
{/* Body frame — every content state lives here and reflows left of the docked
|
|
1875
|
+
AI panel (an absolute slot in this column). The header + nav rail are OUTSIDE
|
|
1876
|
+
this wrapper, so they never move when the panel opens. */}
|
|
1877
|
+
<div className="relative flex flex-1 flex-col min-h-0" style={{ paddingRight: contentGutter, transition: 'padding-right 0.2s ease' }}>
|
|
1878
|
+
|
|
1798
1879
|
{/* Auth barrier - show when auth is enabled but user is not authenticated */}
|
|
1799
1880
|
{authMe?.authEnabled && !authMe?.username && authMe.authMode === 'proxy' && (
|
|
1800
1881
|
<AuthBarrier authMode="proxy" />
|
|
@@ -1816,95 +1897,60 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
1816
1897
|
Icon is pane-anchored so its screen position matches the
|
|
1817
1898
|
host hub splash across cross-document transitions. */}
|
|
1818
1899
|
{!isSwitching && !(authMe?.authEnabled && !authMe?.username) && connection.state === 'connecting' && (
|
|
1819
|
-
<
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
<div
|
|
1835
|
-
className="absolute left-1/2 -translate-x-1/2 text-center"
|
|
1836
|
-
style={{ top: 'calc(50% + 34px)' }}
|
|
1837
|
-
>
|
|
1838
|
-
{/* 17px semibold matches the other splash surfaces so font
|
|
1839
|
-
weight doesn't visibly swap during hub → cluster
|
|
1840
|
-
transitions. Subtitles below stay smaller/dimmer. */}
|
|
1841
|
-
<p className="whitespace-nowrap text-[17px] font-semibold tracking-tight text-theme-text-primary">
|
|
1842
|
-
Connecting to cluster
|
|
1843
|
-
</p>
|
|
1844
|
-
{connection.context && (
|
|
1845
|
-
<p className="text-sm text-theme-text-secondary mt-1">{connection.context}</p>
|
|
1846
|
-
)}
|
|
1847
|
-
{connection.progressMessage && (
|
|
1848
|
-
<p className="text-xs text-theme-text-tertiary animate-pulse mt-3">
|
|
1849
|
-
{connection.progressMessage}
|
|
1850
|
-
</p>
|
|
1851
|
-
)}
|
|
1852
|
-
</div>
|
|
1853
|
-
</div>
|
|
1854
|
-
</div>
|
|
1900
|
+
<PaneLoader
|
|
1901
|
+
label="Connecting to cluster"
|
|
1902
|
+
className="flex-1 min-h-0 bg-theme-base"
|
|
1903
|
+
>
|
|
1904
|
+
{connection.context && (
|
|
1905
|
+
<span className="mt-1 block text-sm font-normal tracking-normal text-theme-text-secondary">
|
|
1906
|
+
{connection.context}
|
|
1907
|
+
</span>
|
|
1908
|
+
)}
|
|
1909
|
+
{connection.progressMessage && (
|
|
1910
|
+
<span className="mt-3 block text-xs font-normal tracking-normal text-theme-text-tertiary animate-pulse">
|
|
1911
|
+
{connection.progressMessage}
|
|
1912
|
+
</span>
|
|
1913
|
+
)}
|
|
1914
|
+
</PaneLoader>
|
|
1855
1915
|
)}
|
|
1856
1916
|
|
|
1857
|
-
{/* Context switching overlay
|
|
1917
|
+
{/* Context switching overlay */}
|
|
1858
1918
|
{isSwitching && (
|
|
1859
|
-
<
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
<div
|
|
1871
|
-
className="absolute left-1/2 -translate-x-1/2 text-center"
|
|
1872
|
-
style={{ top: 'calc(50% + 34px)' }}
|
|
1873
|
-
>
|
|
1874
|
-
<div className="whitespace-nowrap text-[17px] font-semibold tracking-tight text-theme-text-primary">Switching context</div>
|
|
1875
|
-
{targetContext && (
|
|
1876
|
-
<div className="text-xs mt-2 text-theme-text-tertiary">
|
|
1877
|
-
{targetContext.provider ? (
|
|
1878
|
-
<span className="flex items-center justify-center gap-1.5">
|
|
1879
|
-
<span className="text-blue-400 font-medium">{targetContext.provider}</span>
|
|
1880
|
-
{targetContext.account && (
|
|
1881
|
-
<>
|
|
1882
|
-
<span className="text-theme-text-tertiary/50">•</span>
|
|
1883
|
-
<span>{targetContext.account}</span>
|
|
1884
|
-
</>
|
|
1885
|
-
)}
|
|
1886
|
-
{targetContext.region && (
|
|
1887
|
-
<>
|
|
1888
|
-
<span className="text-theme-text-tertiary/50">•</span>
|
|
1889
|
-
<span>{targetContext.region}</span>
|
|
1890
|
-
</>
|
|
1891
|
-
)}
|
|
1919
|
+
<PaneLoader
|
|
1920
|
+
label="Switching context"
|
|
1921
|
+
className="flex-1 min-h-0 bg-theme-base"
|
|
1922
|
+
>
|
|
1923
|
+
{targetContext && (
|
|
1924
|
+
<span className="mt-2 block text-xs font-normal tracking-normal text-theme-text-tertiary">
|
|
1925
|
+
{targetContext.provider ? (
|
|
1926
|
+
<span className="flex items-center justify-center gap-1.5">
|
|
1927
|
+
<span className="text-blue-400 font-medium">{targetContext.provider}</span>
|
|
1928
|
+
{targetContext.account && (
|
|
1929
|
+
<>
|
|
1892
1930
|
<span className="text-theme-text-tertiary/50">•</span>
|
|
1893
|
-
<span
|
|
1894
|
-
|
|
1895
|
-
) : (
|
|
1896
|
-
<span>{targetContext.raw}</span>
|
|
1931
|
+
<span>{targetContext.account}</span>
|
|
1932
|
+
</>
|
|
1897
1933
|
)}
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1934
|
+
{targetContext.region && (
|
|
1935
|
+
<>
|
|
1936
|
+
<span className="text-theme-text-tertiary/50">•</span>
|
|
1937
|
+
<span>{targetContext.region}</span>
|
|
1938
|
+
</>
|
|
1939
|
+
)}
|
|
1940
|
+
<span className="text-theme-text-tertiary/50">•</span>
|
|
1941
|
+
<span className="text-theme-text-secondary font-medium">{targetContext.clusterName}</span>
|
|
1942
|
+
</span>
|
|
1943
|
+
) : (
|
|
1944
|
+
<span>{targetContext.raw}</span>
|
|
1904
1945
|
)}
|
|
1905
|
-
</
|
|
1906
|
-
|
|
1907
|
-
|
|
1946
|
+
</span>
|
|
1947
|
+
)}
|
|
1948
|
+
{progressMessage && (
|
|
1949
|
+
<span className="mt-3 block text-xs font-normal tracking-normal text-theme-text-tertiary animate-pulse">
|
|
1950
|
+
{progressMessage}
|
|
1951
|
+
</span>
|
|
1952
|
+
)}
|
|
1953
|
+
</PaneLoader>
|
|
1908
1954
|
)}
|
|
1909
1955
|
|
|
1910
1956
|
{/* Main content - only show when connected and authenticated */}
|
|
@@ -1918,6 +1964,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
1918
1964
|
<HomeView
|
|
1919
1965
|
namespaces={namespaces}
|
|
1920
1966
|
topology={topology}
|
|
1967
|
+
fallbackClusterLoadState={showHomeClusterLoadFallback ? clusterLoadState : undefined}
|
|
1921
1968
|
onNavigateToView={setMainView}
|
|
1922
1969
|
onNavigateToResourceKind={(kind, apiGroup, filters) => {
|
|
1923
1970
|
// Navigate to resources view with kind in URL path
|
|
@@ -2086,7 +2133,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
2086
2133
|
<TimelineView
|
|
2087
2134
|
namespaces={namespaces}
|
|
2088
2135
|
onResourceClick={(resource) => {
|
|
2089
|
-
navigate(
|
|
2136
|
+
navigate(relatedResourcePath(resource))
|
|
2090
2137
|
}}
|
|
2091
2138
|
initialViewMode={(searchParams.get('view') as 'list' | 'swimlane') || undefined}
|
|
2092
2139
|
initialFilter={(searchParams.get('filter') as 'all' | 'changes' | 'k8s_events' | 'warnings' | 'unhealthy') || undefined}
|
|
@@ -2137,9 +2184,10 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
2137
2184
|
// detail panel) sits behind the peek. Search-only change keeps the
|
|
2138
2185
|
// pathname — and thus the peek's owner-path — intact.
|
|
2139
2186
|
const params = new URLSearchParams(window.location.search)
|
|
2140
|
-
if (params.has('workload') || params.has('tab')) {
|
|
2187
|
+
if (params.has('workload') || params.has('tab') || params.has('run')) {
|
|
2141
2188
|
params.delete('workload')
|
|
2142
2189
|
params.delete('tab')
|
|
2190
|
+
params.delete('run')
|
|
2143
2191
|
navigate({ pathname: window.location.pathname, search: params.toString() }, { replace: true })
|
|
2144
2192
|
}
|
|
2145
2193
|
navigateToResource(resource)
|
|
@@ -2163,27 +2211,10 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
2163
2211
|
own fetches) while the cross-document nav lands. Covers checks /
|
|
2164
2212
|
issues / gitops with one block since only one view is active. */}
|
|
2165
2213
|
{viewTakeoverHref && (
|
|
2166
|
-
<
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
<img
|
|
2171
|
-
src={radarLoadingIcon}
|
|
2172
|
-
alt=""
|
|
2173
|
-
aria-hidden
|
|
2174
|
-
className="absolute w-11 h-11"
|
|
2175
|
-
style={{ left: 'calc(50% - 22px)', top: 'calc(50% - 22px)' }}
|
|
2176
|
-
/>
|
|
2177
|
-
<div
|
|
2178
|
-
className="absolute left-1/2 -translate-x-1/2 text-center"
|
|
2179
|
-
style={{ top: 'calc(50% + 34px)' }}
|
|
2180
|
-
>
|
|
2181
|
-
<p className="whitespace-nowrap text-[17px] font-semibold tracking-tight text-theme-text-primary">
|
|
2182
|
-
Opening…
|
|
2183
|
-
</p>
|
|
2184
|
-
</div>
|
|
2185
|
-
</div>
|
|
2186
|
-
</div>
|
|
2214
|
+
<PaneLoader
|
|
2215
|
+
label="Opening…"
|
|
2216
|
+
className="flex-1 min-h-0 bg-theme-base"
|
|
2217
|
+
/>
|
|
2187
2218
|
)}
|
|
2188
2219
|
|
|
2189
2220
|
{/* Best practices detail view (inline only when the host hasn't taken
|
|
@@ -2213,7 +2244,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
2213
2244
|
{mainView === 'workload' && (
|
|
2214
2245
|
<WorkloadViewRoute
|
|
2215
2246
|
onNavigateToResource={(resource) => {
|
|
2216
|
-
navigate(
|
|
2247
|
+
navigate(relatedResourcePath(resource))
|
|
2217
2248
|
}}
|
|
2218
2249
|
/>
|
|
2219
2250
|
)}
|
|
@@ -2223,6 +2254,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
2223
2254
|
|
|
2224
2255
|
</ErrorBoundary>
|
|
2225
2256
|
</div>}
|
|
2257
|
+
</div>{/* /body frame */}
|
|
2226
2258
|
|
|
2227
2259
|
{/* Resource detail drawer — stays mounted, expands to full-screen WorkloadView.
|
|
2228
2260
|
Gated on contentReady so it never renders over the connecting/switching
|
|
@@ -2234,6 +2266,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
2234
2266
|
// No Radar header in chromeless embeds (Radar Hub) — anchor the drawer
|
|
2235
2267
|
// to the top of the content area instead of leaving a 49px gap.
|
|
2236
2268
|
headerHeight={chromeless ? 0 : undefined}
|
|
2269
|
+
rightInset={contentGutter}
|
|
2237
2270
|
isOpen={resourceDrawer.isOpen}
|
|
2238
2271
|
expanded={drawerExpandedProp}
|
|
2239
2272
|
onClose={closeDrawer}
|
|
@@ -2275,6 +2308,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
2275
2308
|
<HelmReleaseDrawer
|
|
2276
2309
|
release={drawerHelmRelease}
|
|
2277
2310
|
isOpen={helmDrawer.isOpen}
|
|
2311
|
+
rightInset={contentGutter}
|
|
2278
2312
|
onClose={() => {
|
|
2279
2313
|
setSelectedHelmRelease(null)
|
|
2280
2314
|
const params = new URLSearchParams(window.location.search)
|
|
@@ -2294,6 +2328,12 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix }: { manage
|
|
|
2294
2328
|
/>
|
|
2295
2329
|
)}
|
|
2296
2330
|
|
|
2331
|
+
{/* AI investigation panel — an absolute slot in this column (the body frame),
|
|
2332
|
+
below the header and right of the nav rail, sharing the frame with the
|
|
2333
|
+
drawers above. Docked = right slot (pushes content via contentGutter);
|
|
2334
|
+
maximized = fills the frame. */}
|
|
2335
|
+
{diagnoseOpen && <DiagnoseSurface topInset={chromeless ? 0 : APP_HEADER_HEIGHT} />}
|
|
2336
|
+
|
|
2297
2337
|
{/* Port Forward floating panel (indicator lives in header) */}
|
|
2298
2338
|
<PortForwardPanel />
|
|
2299
2339
|
|
|
@@ -2406,12 +2446,12 @@ function FloatingButtons({ showHelp, showCommandPalette, showDiagnostics, onHelp
|
|
|
2406
2446
|
return (
|
|
2407
2447
|
<div className={`fixed ${bottom} right-4 z-40 flex items-center gap-1.5`}>
|
|
2408
2448
|
<Tooltip content="Report bug / Diagnostics" position="top">
|
|
2409
|
-
<button onClick={onBugReport} className={btnClass}>
|
|
2449
|
+
<button onClick={onBugReport} aria-label="Open diagnostics" className={btnClass}>
|
|
2410
2450
|
<Bug className="w-3.5 h-3.5" />
|
|
2411
2451
|
</button>
|
|
2412
2452
|
</Tooltip>
|
|
2413
2453
|
<Tooltip content="Keyboard shortcuts (?)" position="top">
|
|
2414
|
-
<button onClick={onHelp} className={btnClass}>
|
|
2454
|
+
<button onClick={onHelp} aria-label="Show keyboard shortcuts" className={btnClass}>
|
|
2415
2455
|
?
|
|
2416
2456
|
</button>
|
|
2417
2457
|
</Tooltip>
|
|
@@ -2420,14 +2460,18 @@ function FloatingButtons({ showHelp, showCommandPalette, showDiagnostics, onHelp
|
|
|
2420
2460
|
}
|
|
2421
2461
|
|
|
2422
2462
|
// Main App component wrapped with providers
|
|
2423
|
-
function App({ manageDocumentTitle = false, documentTitleSuffix }:
|
|
2463
|
+
function App({ manageDocumentTitle = false, documentTitleSuffix, onClusterLoadStateChange }: AppProps) {
|
|
2424
2464
|
return (
|
|
2425
2465
|
<ConnectionProvider>
|
|
2426
2466
|
<CapabilitiesProvider>
|
|
2427
2467
|
<ContextSwitchProvider>
|
|
2428
2468
|
<DockProvider>
|
|
2429
2469
|
<KeyboardShortcutProvider>
|
|
2430
|
-
<AppInner
|
|
2470
|
+
<AppInner
|
|
2471
|
+
manageDocumentTitle={manageDocumentTitle}
|
|
2472
|
+
documentTitleSuffix={documentTitleSuffix}
|
|
2473
|
+
onClusterLoadStateChange={onClusterLoadStateChange}
|
|
2474
|
+
/>
|
|
2431
2475
|
</KeyboardShortcutProvider>
|
|
2432
2476
|
</DockProvider>
|
|
2433
2477
|
</ContextSwitchProvider>
|
|
@@ -2563,6 +2607,7 @@ function GitHubStarButton() {
|
|
|
2563
2607
|
target="_blank"
|
|
2564
2608
|
rel="noopener noreferrer"
|
|
2565
2609
|
onClick={handleClick}
|
|
2610
|
+
aria-label={starred ? 'Open Radar on GitHub' : 'Star Radar on GitHub'}
|
|
2566
2611
|
className="flex items-center gap-1.5 h-7 px-2 rounded-md transition-colors bg-theme-elevated hover:bg-theme-hover text-theme-text-secondary hover:text-theme-text-primary"
|
|
2567
2612
|
>
|
|
2568
2613
|
<svg className="w-4 h-4" viewBox="0 0 16 16" fill="currentColor"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>
|
|
@@ -2618,6 +2663,7 @@ function ThemeToggle() {
|
|
|
2618
2663
|
<Tooltip content={`Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`}>
|
|
2619
2664
|
<button
|
|
2620
2665
|
onClick={toggleTheme}
|
|
2666
|
+
aria-label={`Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`}
|
|
2621
2667
|
className="p-1.5 rounded-md bg-theme-elevated hover:bg-theme-hover text-theme-text-secondary hover:text-theme-text-primary transition-colors"
|
|
2622
2668
|
>
|
|
2623
2669
|
{theme === 'dark' ? (
|