@skyhook-io/radar-app 1.16.2 → 1.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -5
- package/src/App.tsx +60 -12
- package/src/RadarApp.tsx +27 -17
- package/src/api/client.goneResource.test.ts +49 -0
- package/src/api/client.ts +168 -55
- package/src/api/cnpg.ts +95 -0
- package/src/api/diagnose.test.ts +40 -0
- package/src/api/diagnose.ts +12 -1
- package/src/api/radarFeatures.render.test.tsx +19 -1
- package/src/api/radarFeatures.ts +3 -0
- package/src/api/timelineSource.ts +1 -1
- package/src/api/traffic.sources.test.ts +55 -0
- package/src/api/traffic.ts +173 -14
- package/src/components/NoClusterAccessBanner.tsx +68 -0
- package/src/components/applications/ApplicationsView.tsx +3 -0
- package/src/components/capacity/CapacityPoolDetail.tsx +18 -7
- package/src/components/capacity/CapacityView.test.tsx +31 -1
- package/src/components/capacity/shared.tsx +12 -21
- package/src/components/cnpg/CNPGClusterActivity.tsx +49 -0
- package/src/components/cnpg/CNPGClusterLogs.tsx +62 -0
- package/src/components/cnpg/CNPGDeclarations.tsx +311 -0
- package/src/components/cnpg/CNPGDetailPage.tsx +228 -0
- package/src/components/cnpg/CNPGDrawerTrail.tsx +37 -0
- package/src/components/cnpg/CNPGOperator.tsx +234 -0
- package/src/components/cnpg/CNPGOverview.tsx +307 -0
- package/src/components/cnpg/CNPGPooling.tsx +116 -0
- package/src/components/cnpg/CNPGProtection.tsx +320 -0
- package/src/components/cnpg/CNPGSummaryHost.tsx +143 -0
- package/src/components/cnpg/CNPGView.tsx +156 -0
- package/src/components/cnpg/paths.ts +13 -0
- package/src/components/cnpg/routes.test.ts +60 -0
- package/src/components/cnpg/routes.ts +112 -0
- package/src/components/cnpg/shared.tsx +290 -0
- package/src/components/cnpg/useCNPGSidebarWorkspace.ts +88 -0
- package/src/components/diagnose/AgentSetupNotice.test.tsx +58 -0
- package/src/components/diagnose/AgentSetupNotice.tsx +142 -51
- package/src/components/diagnose/ApplyDialog.ack.test.tsx +47 -0
- package/src/components/diagnose/ApplyDialog.test.tsx +59 -6
- package/src/components/diagnose/ApplyDialog.tsx +26 -28
- package/src/components/diagnose/DiagnoseContext.test.ts +43 -0
- package/src/components/diagnose/DiagnoseContext.tsx +136 -54
- package/src/components/diagnose/DiagnoseSurface.tsx +16 -3
- package/src/components/diagnose/InvestigationView.tsx +39 -2
- package/src/components/diagnose/LocalDiagnoseAction.tsx +40 -20
- package/src/components/diagnose/agentCatalog.ts +2 -2
- package/src/components/diagnose/diagnoseEvidenceTypes.ts +2 -0
- package/src/components/dock/TrafficFlowListTab.tsx +27 -6
- package/src/components/execution/BatchExecutionView.tsx +11 -11
- package/src/components/helm/ChartBrowser.tsx +9 -10
- package/src/components/helm/HelmCompareRoute.tsx +108 -115
- package/src/components/helm/HelmReleaseDrawer.tsx +26 -44
- package/src/components/helm/InstallWizard.tsx +5 -2
- package/src/components/helm/RevisionHistory.tsx +3 -2
- package/src/components/helm/TrackChartSourceDialog.tsx +10 -2
- package/src/components/helm/ValuesViewer.tsx +1 -1
- package/src/components/home/ClusterHealthCard.tsx +7 -0
- package/src/components/home/mcpToolCatalog.ts +1 -1
- package/src/components/resources/ResourceDetailDrawer.tsx +6 -0
- package/src/components/resources/ResourcesView.tsx +6 -38
- package/src/components/resources/renderers/KyvernoPolicyQueued.tsx +2 -1
- package/src/components/resources/renderers/NodeRenderer.tsx +7 -1
- package/src/components/resources/renderers/ServiceRenderer.test.tsx +81 -0
- package/src/components/resources/renderers/ServiceRenderer.tsx +19 -14
- package/src/components/settings/SettingsDialog.tsx +122 -19
- package/src/components/shared/LargeClusterNamespacePicker.tsx +11 -5
- package/src/components/timeline/TimelineList.test.ts +47 -29
- package/src/components/timeline/TimelineList.tsx +60 -11
- package/src/components/timeline/TimelineRetentionNote.test.tsx +49 -0
- package/src/components/timeline/TimelineRetentionNote.tsx +20 -0
- package/src/components/timeline/TimelineView.tsx +16 -2
- package/src/components/timeline/TimelineView.urlparams.test.ts +6 -0
- package/src/components/traffic/TrafficConnectionsTable.tsx +144 -0
- package/src/components/traffic/TrafficFilterSidebar.tsx +22 -1
- package/src/components/traffic/TrafficFlowListContext.tsx +12 -4
- package/src/components/traffic/TrafficFocusSearch.tsx +172 -0
- package/src/components/traffic/TrafficGraph.tsx +109 -26
- package/src/components/traffic/TrafficGraphTooLarge.tsx +150 -0
- package/src/components/traffic/TrafficView.tsx +582 -149
- package/src/components/traffic/trafficFilters.test.ts +398 -1
- package/src/components/traffic/trafficFilters.ts +408 -0
- package/src/components/whats-new/WhatsNew.test.ts +107 -28
- package/src/components/whats-new/WhatsNew.tsx +146 -86
- package/src/components/whats-new/WhatsNewDialog.test.tsx +132 -10
- package/src/components/whats-new/releaseNotes.ts +155 -24
- package/src/components/workload/WorkloadView.test.ts +2 -1
- package/src/components/workload/WorkloadView.tsx +165 -294
- package/src/context/TimelineCustomization.tsx +15 -0
- package/src/contexts/CapabilitiesContext.tsx +9 -1
- package/src/hooks/useGitOpsWriteGuard.test.tsx +457 -0
- package/src/hooks/useGitOpsWriteGuard.ts +193 -0
- package/src/hooks/useResolvedGitOpsOwner.ts +256 -0
- package/src/hooks/useResourceCounts.ts +49 -0
- package/src/components/helm/RoleGatedPanel.tsx +0 -47
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skyhook-io/radar-app",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.17.0",
|
|
4
4
|
"description": "Radar's full web UI as a reusable React component. Used by Radar's own binary and by external consumers like Radar Cloud.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -36,13 +36,13 @@
|
|
|
36
36
|
"diff": "^9.0.0",
|
|
37
37
|
"monaco-editor": "0.55.1",
|
|
38
38
|
"react-markdown": "^10.1.0",
|
|
39
|
-
"react-virtuoso": "^4.18.
|
|
39
|
+
"react-virtuoso": "^4.18.16",
|
|
40
40
|
"remark-gfm": "^4.0.1",
|
|
41
41
|
"shiki": "^4.4.2",
|
|
42
42
|
"yaml": "^2.9.0"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
|
-
"@skyhook-io/k8s-ui": ">=1.
|
|
45
|
+
"@skyhook-io/k8s-ui": ">=1.17.0",
|
|
46
46
|
"@tanstack/react-query": ">=5",
|
|
47
47
|
"@xyflow/react": ">=12.0.0",
|
|
48
48
|
"clsx": ">=2",
|
|
@@ -73,14 +73,14 @@
|
|
|
73
73
|
"globals": "^17.11.0",
|
|
74
74
|
"lucide-react": "^1.48.0",
|
|
75
75
|
"postcss": "^8.5.28",
|
|
76
|
-
"prettier": "^3.9.
|
|
76
|
+
"prettier": "^3.9.9",
|
|
77
77
|
"react": "^19.3.0",
|
|
78
78
|
"react-dom": "^19.3.0",
|
|
79
79
|
"react-router-dom": "^7.18.4",
|
|
80
80
|
"tailwind-merge": "^3.7.0",
|
|
81
81
|
"tailwindcss": "^4.3.3",
|
|
82
82
|
"typescript": "^6.0.2",
|
|
83
|
-
"typescript-eslint": "^8.
|
|
83
|
+
"typescript-eslint": "^8.71.0",
|
|
84
84
|
"vite": "^8.3.1"
|
|
85
85
|
},
|
|
86
86
|
"sideEffects": [
|
package/src/App.tsx
CHANGED
|
@@ -38,6 +38,9 @@ import { CloudFunnelButton } from './components/CloudFunnelButton'
|
|
|
38
38
|
import { useNavCustomization } from './context/NavCustomization'
|
|
39
39
|
import type { FleetTakeoverTarget } from './context/NavCustomization'
|
|
40
40
|
import { PrimaryNavRail } from './components/nav/PrimaryNavRail'
|
|
41
|
+
import { CNPGView } from './components/cnpg/CNPGView'
|
|
42
|
+
import { CNPG_SCREENS, cnpgDetailKindFor, cnpgDetailPath, parseCNPGRoute } from './components/cnpg/routes'
|
|
43
|
+
import { currentPageLabel } from './components/cnpg/paths'
|
|
41
44
|
import { navigateFromPrimaryRail } from './components/nav/navigation'
|
|
42
45
|
import { useNavRailPinned } from './hooks/useNavRailPinned'
|
|
43
46
|
import { useMediaQuery } from './hooks/useMediaQuery'
|
|
@@ -45,6 +48,7 @@ import { ContextSwitchProvider, useContextSwitch } from './context/ContextSwitch
|
|
|
45
48
|
import { ConnectionProvider, useConnection } from './context/ConnectionContext'
|
|
46
49
|
import { ConnectionErrorView } from './components/ConnectionErrorView'
|
|
47
50
|
import { SyncProgressPanel } from './components/SyncProgressPanel'
|
|
51
|
+
import { NoClusterAccessBanner } from './components/NoClusterAccessBanner'
|
|
48
52
|
import { CapabilitiesProvider, useCapabilitiesContext } from './contexts/CapabilitiesContext'
|
|
49
53
|
import { RadarUpgradeProvider } from './context/RadarUpgrade'
|
|
50
54
|
import { UserMenu } from './components/UserMenu'
|
|
@@ -56,7 +60,7 @@ import { UsageDataPrompt } from './components/usage-data/UsageDataPrompt'
|
|
|
56
60
|
import { ShortcutHelpOverlay } from './components/ui/ShortcutHelpOverlay'
|
|
57
61
|
import { DiagnosticsOverlay } from './components/ui/DiagnosticsOverlay'
|
|
58
62
|
import { useEventSource } from './hooks/useEventSource'
|
|
59
|
-
import { debugNamespaceLog, useNamespaces, useNamespaceScope, useSetActiveNamespace, useSwitchContext, useAuthMe, useAudit } from './api/client'
|
|
63
|
+
import { debugNamespaceLog, refetchOnResourceEvents, resourceEventKey, useNamespaces, useNamespaceScope, useSetActiveNamespace, useSwitchContext, useAuthMe, useAudit } from './api/client'
|
|
60
64
|
import { buildAuditSeverityMap } from './utils/auditBadges'
|
|
61
65
|
import { isInNamespaceScope, scopeNodesToNamespaces } from './utils/topology-namespace'
|
|
62
66
|
import { routePath, apiUrl, getAuthHeaders, getCredentialsMode, stripBasename } from './api/config'
|
|
@@ -124,7 +128,7 @@ const FLEET_MODE_KINDS = new Set<NodeKind>([
|
|
|
124
128
|
|
|
125
129
|
// Convert API resource name back to topology node ID prefix
|
|
126
130
|
// Extended MainView type that includes traffic and cost
|
|
127
|
-
type ExtendedMainView = MainView | 'traffic' | 'cost' | 'capacity' | 'workload' | 'checks' | 'gitops' | 'compare' | 'helmCompare' | 'issues' | 'applications' | 'investigations'
|
|
131
|
+
type ExtendedMainView = MainView | 'traffic' | 'cost' | 'capacity' | 'cnpg' | 'workload' | 'checks' | 'gitops' | 'compare' | 'helmCompare' | 'issues' | 'applications' | 'investigations'
|
|
128
132
|
|
|
129
133
|
// Extract view from URL path
|
|
130
134
|
function getViewFromPath(pathname: string): ExtendedMainView {
|
|
@@ -138,6 +142,7 @@ function getViewFromPath(pathname: string): ExtendedMainView {
|
|
|
138
142
|
if (path === 'traffic') return 'traffic'
|
|
139
143
|
if (path === 'cost') return 'cost'
|
|
140
144
|
if (path === 'capacity') return 'capacity'
|
|
145
|
+
if (path === 'cnpg') return 'cnpg'
|
|
141
146
|
if (path === 'workload') return 'workload'
|
|
142
147
|
if (path === 'checks' || path === 'audit') return 'checks' // /audit = legacy → checks
|
|
143
148
|
if (path === 'gitops') return 'gitops'
|
|
@@ -165,7 +170,7 @@ function usageView(pathname: string, view: ExtendedMainView, upgrade: boolean):
|
|
|
165
170
|
const CRASH_LABELS: Record<ExtendedMainView, string> = {
|
|
166
171
|
home: 'Home', topology: 'Topology', resources: 'Resources', timeline: 'Timeline',
|
|
167
172
|
issues: 'Issues', helm: 'Helm', helmCompare: 'HelmCompare', traffic: 'Traffic',
|
|
168
|
-
cost: 'Cost', capacity: 'Capacity', checks: 'Checks', gitops: 'GitOps',
|
|
173
|
+
cost: 'Cost', capacity: 'Capacity', cnpg: 'CloudNativePG', checks: 'Checks', gitops: 'GitOps',
|
|
169
174
|
applications: 'Applications', workload: 'Workload', compare: 'Compare',
|
|
170
175
|
investigations: 'Investigations',
|
|
171
176
|
}
|
|
@@ -282,6 +287,12 @@ function radarPageTitle(pathname: string, search = '', apiResources?: APIResourc
|
|
|
282
287
|
if (pathSegments[1] === 'activity') return 'Capacity Activity'
|
|
283
288
|
}
|
|
284
289
|
|
|
290
|
+
if (view === 'cnpg') {
|
|
291
|
+
const route = parseCNPGRoute(pathname)
|
|
292
|
+
if (route.detail) return route.detail.name
|
|
293
|
+
const screen = CNPG_SCREENS.find((s) => s.id === route.screen)
|
|
294
|
+
return `CloudNativePG ${screen?.label ?? 'Overview'}`
|
|
295
|
+
}
|
|
285
296
|
if (view === 'home') return 'Overview'
|
|
286
297
|
// Every other view's label is its id capitalized — getViewFromPath has already
|
|
287
298
|
// normalized aliases (e.g. /audit → 'checks'), so no lookup table is needed.
|
|
@@ -875,7 +886,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
875
886
|
gitops: 'g o', checks: 'g u', cost: 'g c', capacity: 'g p',
|
|
876
887
|
// Non-rail views (reachable via deep links / actions, not the rail) get no
|
|
877
888
|
// dedicated mnemonic — listed for exhaustiveness so the type stays total.
|
|
878
|
-
workload: '', compare: '', helmCompare: '', investigations: '',
|
|
889
|
+
workload: '', compare: '', helmCompare: '', investigations: '', cnpg: '',
|
|
879
890
|
}
|
|
880
891
|
const views = Object.keys(VIEW_SHORTCUT_KEYS).filter(
|
|
881
892
|
(v): v is ExtendedMainView => VIEW_SHORTCUT_KEYS[v as ExtendedMainView] !== '',
|
|
@@ -1058,12 +1069,14 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
1058
1069
|
// update that arrived *after* the fast structural flush already ran.
|
|
1059
1070
|
const fastInvalidationRef = useRef<{
|
|
1060
1071
|
changedKinds: Set<string> // every changed kind (any op) → detail drawer
|
|
1072
|
+
changedObjects: Set<string> // resourceEventKey of every event → detail views that got a 404
|
|
1061
1073
|
structuralKinds: Set<string> // add/delete kinds → list membership + counts + dashboard
|
|
1062
1074
|
environmentNamespaces: Set<string>
|
|
1063
1075
|
environmentPods: Map<string, Set<string>>
|
|
1064
1076
|
secretsChanged: boolean
|
|
1065
1077
|
timer: number | null
|
|
1066
|
-
}>({ changedKinds: new Set(), structuralKinds: new Set(), environmentNamespaces: new Set(), environmentPods: new Map(), secretsChanged: false, timer: null })
|
|
1078
|
+
}>({ changedKinds: new Set(), changedObjects: new Set(), structuralKinds: new Set(), environmentNamespaces: new Set(), environmentPods: new Map(), secretsChanged: false, timer: null })
|
|
1079
|
+
const cnpgInvalidationPendingRef = useRef(false)
|
|
1067
1080
|
const slowInvalidationRef = useRef<{
|
|
1068
1081
|
updatedKinds: Set<string> // update-only churn → throttled list + dashboard
|
|
1069
1082
|
timer: number | null
|
|
@@ -1095,8 +1108,11 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
1095
1108
|
const structural = event.operation === 'add' || event.operation === 'delete'
|
|
1096
1109
|
const applicationWorkload = ['deployments', 'statefulsets', 'daemonsets', 'rollouts'].includes(kind)
|
|
1097
1110
|
|
|
1111
|
+
if (event.group?.endsWith('.cnpg.io')) cnpgInvalidationPendingRef.current = true
|
|
1112
|
+
|
|
1098
1113
|
const fast = fastInvalidationRef.current
|
|
1099
1114
|
fast.changedKinds.add(kind)
|
|
1115
|
+
fast.changedObjects.add(resourceEventKey(kind, event.namespace ?? '', event.name))
|
|
1100
1116
|
if (structural) fast.structuralKinds.add(kind)
|
|
1101
1117
|
if (kind === 'secrets') fast.secretsChanged = true
|
|
1102
1118
|
if ((kind === 'configmaps' || kind === 'secrets') && event.namespace) fast.environmentNamespaces.add(event.namespace)
|
|
@@ -1114,7 +1130,12 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
1114
1130
|
fast.timer = window.setTimeout(() => {
|
|
1115
1131
|
const f = fastInvalidationRef.current
|
|
1116
1132
|
for (const k of f.changedKinds) {
|
|
1117
|
-
|
|
1133
|
+
// Open detail drawers stay live; a deleted one is left alone unless
|
|
1134
|
+
// an event names it.
|
|
1135
|
+
queryClient.invalidateQueries({
|
|
1136
|
+
queryKey: ['resource', k],
|
|
1137
|
+
predicate: (q) => refetchOnResourceEvents(q, k, f.changedObjects),
|
|
1138
|
+
})
|
|
1118
1139
|
}
|
|
1119
1140
|
for (const k of f.structuralKinds) {
|
|
1120
1141
|
queryClient.invalidateQueries({ queryKey: ['resources', k] }) // list membership changed
|
|
@@ -1138,7 +1159,11 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
1138
1159
|
// GitOps view is mounted (Phase 2 will make this relevance-aware).
|
|
1139
1160
|
queryClient.invalidateQueries({ queryKey: ['gitops-tree'] })
|
|
1140
1161
|
queryClient.invalidateQueries({ queryKey: ['gitops-insights'] })
|
|
1141
|
-
|
|
1162
|
+
if (cnpgInvalidationPendingRef.current) {
|
|
1163
|
+
cnpgInvalidationPendingRef.current = false
|
|
1164
|
+
queryClient.invalidateQueries({ queryKey: ['cnpg', 'workspace'] })
|
|
1165
|
+
}
|
|
1166
|
+
fastInvalidationRef.current = { changedKinds: new Set(), changedObjects: new Set(), structuralKinds: new Set(), environmentNamespaces: new Set(), environmentPods: new Map(), secretsChanged: false, timer: null }
|
|
1142
1167
|
}, 3000)
|
|
1143
1168
|
}
|
|
1144
1169
|
|
|
@@ -1167,7 +1192,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
1167
1192
|
if (fastInvalidationRef.current.timer !== null) clearTimeout(fastInvalidationRef.current.timer)
|
|
1168
1193
|
if (slowInvalidationRef.current.timer !== null) clearTimeout(slowInvalidationRef.current.timer)
|
|
1169
1194
|
if (timelineInvalidationRef.current.timer !== null) clearTimeout(timelineInvalidationRef.current.timer)
|
|
1170
|
-
fastInvalidationRef.current = { changedKinds: new Set(), structuralKinds: new Set(), environmentNamespaces: new Set(), environmentPods: new Map(), secretsChanged: false, timer: null }
|
|
1195
|
+
fastInvalidationRef.current = { changedKinds: new Set(), changedObjects: new Set(), structuralKinds: new Set(), environmentNamespaces: new Set(), environmentPods: new Map(), secretsChanged: false, timer: null }
|
|
1171
1196
|
slowInvalidationRef.current = { updatedKinds: new Set(), timer: null }
|
|
1172
1197
|
timelineInvalidationRef.current = { timer: null }
|
|
1173
1198
|
}, [])
|
|
@@ -1190,7 +1215,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
1190
1215
|
if (fastInvalidationRef.current.timer !== null) clearTimeout(fastInvalidationRef.current.timer)
|
|
1191
1216
|
if (slowInvalidationRef.current.timer !== null) clearTimeout(slowInvalidationRef.current.timer)
|
|
1192
1217
|
if (timelineInvalidationRef.current.timer !== null) clearTimeout(timelineInvalidationRef.current.timer)
|
|
1193
|
-
fastInvalidationRef.current = { changedKinds: new Set(), structuralKinds: new Set(), environmentNamespaces: new Set(), environmentPods: new Map(), secretsChanged: false, timer: null }
|
|
1218
|
+
fastInvalidationRef.current = { changedKinds: new Set(), changedObjects: new Set(), structuralKinds: new Set(), environmentNamespaces: new Set(), environmentPods: new Map(), secretsChanged: false, timer: null }
|
|
1194
1219
|
slowInvalidationRef.current = { updatedKinds: new Set(), timer: null }
|
|
1195
1220
|
timelineInvalidationRef.current = { timer: null }
|
|
1196
1221
|
|
|
@@ -1223,6 +1248,10 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
1223
1248
|
const nextParams = new URLSearchParams()
|
|
1224
1249
|
const diagnoseRun = new URLSearchParams(location.search).get('ai-run')
|
|
1225
1250
|
if (diagnoseRun) nextParams.set('ai-run', diagnoseRun)
|
|
1251
|
+
// A CNPG detail keeps the context it belongs to, so it can say it is
|
|
1252
|
+
// not in the new one instead of loading a same-named object.
|
|
1253
|
+
const pinnedCtx = new URLSearchParams(location.search).get('ctx')
|
|
1254
|
+
if (pinnedCtx && location.pathname.startsWith('/cnpg/')) nextParams.set('ctx', pinnedCtx)
|
|
1226
1255
|
navigate(
|
|
1227
1256
|
{ pathname: location.pathname, search: nextParams.toString() },
|
|
1228
1257
|
{ replace: true, state: location.state },
|
|
@@ -1749,7 +1778,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
1749
1778
|
setVisibleKinds(new Set())
|
|
1750
1779
|
}, [])
|
|
1751
1780
|
|
|
1752
|
-
const navActiveView = mainView === 'helmCompare' ? 'helm' : mainView
|
|
1781
|
+
const navActiveView = mainView === 'helmCompare' ? 'helm' : mainView === 'cnpg' ? 'resources' : mainView
|
|
1753
1782
|
|
|
1754
1783
|
return (
|
|
1755
1784
|
<PortForwardProvider>
|
|
@@ -2010,6 +2039,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
2010
2039
|
</PaneLoader>
|
|
2011
2040
|
)}
|
|
2012
2041
|
|
|
2042
|
+
{contentReady && <NoClusterAccessBanner />}
|
|
2013
2043
|
{/* Main content - only show when connected and authenticated */}
|
|
2014
2044
|
{/* inert while a fullscreen detail overlay covers the views — keeps the
|
|
2015
2045
|
retained background list out of the focus order + a11y tree (the visual
|
|
@@ -2290,7 +2320,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
2290
2320
|
|
|
2291
2321
|
{/* Traffic view */}
|
|
2292
2322
|
{!viewsSyncGated && mainView === 'traffic' && (
|
|
2293
|
-
<TrafficView namespaces={namespaces} />
|
|
2323
|
+
<TrafficView namespaces={namespaces} onSetNamespaces={(ns) => { setNamespaces(ns); setActiveNamespace.mutate({ namespaces: ns }) }} />
|
|
2294
2324
|
)}
|
|
2295
2325
|
|
|
2296
2326
|
{/* Cost detail view */}
|
|
@@ -2302,6 +2332,16 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
2302
2332
|
<CapacityView onOpenResource={navigateToResource} />
|
|
2303
2333
|
)}
|
|
2304
2334
|
|
|
2335
|
+
{!viewsSyncGated && mainView === 'cnpg' && (
|
|
2336
|
+
<CNPGView
|
|
2337
|
+
namespaces={namespaces}
|
|
2338
|
+
selectedResource={routeSelectedResource}
|
|
2339
|
+
onOpenResource={navigateToResource}
|
|
2340
|
+
onCloseResource={() => setSelectedResource(null)}
|
|
2341
|
+
onClearNamespaces={clearAllNamespaces}
|
|
2342
|
+
/>
|
|
2343
|
+
)}
|
|
2344
|
+
|
|
2305
2345
|
{/* Takeover splash. When the host claims the current view via
|
|
2306
2346
|
fleetTakeoverHref, the redirect effect above is mid-flight — render a
|
|
2307
2347
|
brief splash instead of the inline view (which would flash + fire its
|
|
@@ -2369,7 +2409,15 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
2369
2409
|
onClose={closeDrawer}
|
|
2370
2410
|
onNavigate={(res) => navigateToResource(res)}
|
|
2371
2411
|
canCollapseToDrawer={!isMobile}
|
|
2372
|
-
onExpand={(
|
|
2412
|
+
onExpand={(res, opts) => {
|
|
2413
|
+
const cnpgPlural = cnpgDetailKindFor(res.kind, res.group)
|
|
2414
|
+
if (cnpgPlural) {
|
|
2415
|
+
navigate(
|
|
2416
|
+
cnpgDetailPath({ plural: cnpgPlural, namespace: res.namespace, name: res.name }, connection.context || undefined, opts?.yaml ? 'yaml' : undefined),
|
|
2417
|
+
{ state: { returnLabel: currentPageLabel(), returnCtx: connection.context } },
|
|
2418
|
+
)
|
|
2419
|
+
return
|
|
2420
|
+
}
|
|
2373
2421
|
// Grow the peek into a fullscreen overlay (?full=1, pushed so Back
|
|
2374
2422
|
// collapses) over whatever view is underneath — list, topology graph,
|
|
2375
2423
|
// GitOps, Applications — which stays mounted. Carry the YAML tab when
|
package/src/RadarApp.tsx
CHANGED
|
@@ -46,6 +46,7 @@ import type {
|
|
|
46
46
|
import type { ClusterLoadState } from "./types/clusterLoadState";
|
|
47
47
|
import { TimelineSourceProvider } from "./context/TimelineSource";
|
|
48
48
|
import type { TimelineSourceConfig } from "./api/timelineSource";
|
|
49
|
+
import { TimelineRetentionNoteProvider } from "./context/TimelineCustomization";
|
|
49
50
|
import { DiagnoseCustomizationProvider } from "./context/DiagnoseCustomization";
|
|
50
51
|
import type {
|
|
51
52
|
RenderDiagnoseAction,
|
|
@@ -180,6 +181,12 @@ export interface RadarAppProps {
|
|
|
180
181
|
* React hook-order violation). Set it once at mount when possible.
|
|
181
182
|
*/
|
|
182
183
|
timelineSource?: TimelineSourceConfig;
|
|
184
|
+
/**
|
|
185
|
+
* A note shown above the timeline and the Applications history, saying how
|
|
186
|
+
* far back history goes. Omitted → no note. Set it only when the timeline server
|
|
187
|
+
* actually limits history, or the note misstates what is shown.
|
|
188
|
+
*/
|
|
189
|
+
timelineRetentionNote?: React.ReactNode;
|
|
183
190
|
}
|
|
184
191
|
|
|
185
192
|
// Default QueryClient with the same shape Radar's standalone binary uses.
|
|
@@ -236,6 +243,7 @@ export function RadarApp({
|
|
|
236
243
|
onClusterLoadStateChange,
|
|
237
244
|
onInvestigationFocus,
|
|
238
245
|
timelineSource,
|
|
246
|
+
timelineRetentionNote,
|
|
239
247
|
}: RadarAppProps): React.ReactElement {
|
|
240
248
|
// Apply runtime config during render so module-level singletons are set
|
|
241
249
|
// before children construct URLs. getApiBase() / getAuthHeaders() /
|
|
@@ -266,24 +274,26 @@ export function RadarApp({
|
|
|
266
274
|
<RadarUpgradeHostProvider value={upgradeHost}>
|
|
267
275
|
<FilterLocationBridge>
|
|
268
276
|
<TimelineSourceProvider config={timelineSource}>
|
|
269
|
-
<
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
<DiagnoseProvider
|
|
276
|
-
browserURLState={router !== "memory"}
|
|
277
|
-
forceRouterURLState={router === "memory"}
|
|
278
|
-
onFocusedRun={onInvestigationFocus}
|
|
277
|
+
<TimelineRetentionNoteProvider value={timelineRetentionNote}>
|
|
278
|
+
<DiagnoseCustomizationProvider
|
|
279
|
+
value={renderDiagnoseAction ?? defaultDiagnoseAction}
|
|
280
|
+
consentCopy={diagnoseConsent}
|
|
281
|
+
renderRunActions={renderInvestigationRunActions}
|
|
282
|
+
renderRefusalAction={renderInvestigationRefusalAction}
|
|
279
283
|
>
|
|
280
|
-
<
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
284
|
+
<DiagnoseProvider
|
|
285
|
+
browserURLState={router !== "memory"}
|
|
286
|
+
forceRouterURLState={router === "memory"}
|
|
287
|
+
onFocusedRun={onInvestigationFocus}
|
|
288
|
+
>
|
|
289
|
+
<App
|
|
290
|
+
manageDocumentTitle={manageDocumentTitle}
|
|
291
|
+
documentTitleSuffix={documentTitleSuffix}
|
|
292
|
+
onClusterLoadStateChange={onClusterLoadStateChange}
|
|
293
|
+
/>
|
|
294
|
+
</DiagnoseProvider>
|
|
295
|
+
</DiagnoseCustomizationProvider>
|
|
296
|
+
</TimelineRetentionNoteProvider>
|
|
287
297
|
</TimelineSourceProvider>
|
|
288
298
|
</FilterLocationBridge>
|
|
289
299
|
</RadarUpgradeHostProvider>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { ApiError, goneRecheckInterval, refetchOnResourceEvents, resourceEventKey } from "./client";
|
|
3
|
+
|
|
4
|
+
const notFound = new ApiError('pods "web-1" not found', 404);
|
|
5
|
+
const detail = (namespace: string, name: string, error: unknown) => ({
|
|
6
|
+
queryKey: ["resource", "pods", namespace, name, undefined],
|
|
7
|
+
state: { error },
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
describe("refetchOnResourceEvents", () => {
|
|
11
|
+
it("refetches a live detail view on any change to its kind", () => {
|
|
12
|
+
expect(refetchOnResourceEvents(detail("shop", "web-1", null), "pods", new Set())).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("refetches a detail view that failed for a reason other than 404", () => {
|
|
16
|
+
const failed = detail("shop", "web-1", new ApiError("unavailable", 503));
|
|
17
|
+
expect(refetchOnResourceEvents(failed, "pods", new Set())).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("leaves a deleted object alone when other objects of its kind change", () => {
|
|
21
|
+
// The production shape: pods churning next to an open view of a deleted
|
|
22
|
+
// pod refetched it, 404 every time, for as long as the view stayed open.
|
|
23
|
+
const named = new Set([resourceEventKey("pods", "shop", "web-2")]);
|
|
24
|
+
expect(refetchOnResourceEvents(detail("shop", "web-1", notFound), "pods", named)).toBe(false);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("refetches a deleted object as soon as an event names it", () => {
|
|
28
|
+
// A StatefulSet recreates its pod under the same name.
|
|
29
|
+
const named = new Set([resourceEventKey("pods", "shop", "web-1")]);
|
|
30
|
+
expect(refetchOnResourceEvents(detail("shop", "web-1", notFound), "pods", named)).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("matches a cluster-scoped object, whose namespace is empty", () => {
|
|
34
|
+
const query = { queryKey: ["resource", "nodes", "", "node-a", undefined], state: { error: notFound } };
|
|
35
|
+
expect(refetchOnResourceEvents(query, "nodes", new Set([resourceEventKey("nodes", "", "node-a")]))).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("goneRecheckInterval", () => {
|
|
40
|
+
it("re-checks a deleted object once a minute", () => {
|
|
41
|
+
expect(goneRecheckInterval({ state: { error: notFound } }, undefined)).toBe(60_000);
|
|
42
|
+
expect(goneRecheckInterval({ state: { error: notFound } }, 5_000)).toBe(60_000);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("keeps the caller's interval otherwise", () => {
|
|
46
|
+
expect(goneRecheckInterval({ state: { error: null } }, 5_000)).toBe(5_000);
|
|
47
|
+
expect(goneRecheckInterval({ state: { error: new ApiError("unavailable", 503) } }, undefined)).toBeUndefined();
|
|
48
|
+
});
|
|
49
|
+
});
|