@skyhook-io/radar-app 1.13.4 → 1.14.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.
Files changed (230) hide show
  1. package/package.json +8 -8
  2. package/src/App.tsx +60 -24
  3. package/src/RadarApp.tsx +5 -0
  4. package/src/api/client.authRedirect.test.ts +12 -1
  5. package/src/api/client.prometheusStatus.test.ts +24 -0
  6. package/src/api/client.ts +325 -16
  7. package/src/api/diagnose.ts +62 -15
  8. package/src/api/drain-plan.test.ts +86 -0
  9. package/src/api/quotas.test.ts +26 -0
  10. package/src/api/quotas.ts +20 -0
  11. package/src/api/workloadMetrics.test.ts +40 -0
  12. package/src/api/workloadMetrics.ts +68 -0
  13. package/src/components/CloudConnectFlow.tsx +179 -42
  14. package/src/components/CloudFunnelButton.tsx +107 -36
  15. package/src/components/ConnectionErrorView.test.tsx +30 -0
  16. package/src/components/ConnectionErrorView.tsx +31 -19
  17. package/src/components/SyncProgressPanel.tsx +73 -0
  18. package/src/components/audit/AuditView.tsx +2 -0
  19. package/src/components/cloudConnectHandoff.test.ts +140 -0
  20. package/src/components/cloudConnectHandoff.ts +124 -0
  21. package/src/components/diagnose/AgentCase.tsx +131 -0
  22. package/src/components/diagnose/DiagnoseContext.tsx +2 -1
  23. package/src/components/diagnose/DiagnoseSurface.test.tsx +0 -20
  24. package/src/components/diagnose/DiagnoseSurface.tsx +32 -195
  25. package/src/components/diagnose/InvestigationEvidencePane.test.tsx +783 -7
  26. package/src/components/diagnose/InvestigationEvidencePane.tsx +774 -133
  27. package/src/components/diagnose/InvestigationView.tsx +222 -5
  28. package/src/components/diagnose/diagnoseEvidenceTypes.ts +22 -1
  29. package/src/components/diagnose/investigationCase.test.tsx +1568 -0
  30. package/src/components/diagnose/investigationCase.ts +439 -0
  31. package/src/components/diagnose/investigationEvidence/adapters/changes.ts +60 -0
  32. package/src/components/diagnose/investigationEvidence/adapters/diagnose.test.ts +573 -0
  33. package/src/components/diagnose/investigationEvidence/adapters/diagnose.ts +935 -0
  34. package/src/components/diagnose/investigationEvidence/adapters/events.ts +96 -0
  35. package/src/components/diagnose/investigationEvidence/adapters/helm.test.ts +242 -0
  36. package/src/components/diagnose/investigationEvidence/adapters/helm.ts +208 -0
  37. package/src/components/diagnose/investigationEvidence/adapters/issues.ts +172 -0
  38. package/src/components/diagnose/investigationEvidence/adapters/logs.test.ts +283 -0
  39. package/src/components/diagnose/investigationEvidence/adapters/logs.ts +183 -0
  40. package/src/components/diagnose/investigationEvidence/adapters/permissions.test.ts +528 -0
  41. package/src/components/diagnose/investigationEvidence/adapters/permissions.ts +292 -0
  42. package/src/components/diagnose/investigationEvidence/adapters/prometheus.test.ts +1641 -0
  43. package/src/components/diagnose/investigationEvidence/adapters/prometheus.ts +769 -0
  44. package/src/components/diagnose/investigationEvidence/adapters/resource.test.ts +54 -0
  45. package/src/components/diagnose/investigationEvidence/adapters/resource.ts +257 -0
  46. package/src/components/diagnose/investigationEvidence/adapters/topology.ts +207 -0
  47. package/src/components/diagnose/investigationEvidence/builder.ts +418 -0
  48. package/src/components/diagnose/investigationEvidence/evidenceFixtures.ts +179 -0
  49. package/src/components/diagnose/investigationEvidence/identity.test.ts +614 -0
  50. package/src/components/diagnose/investigationEvidence/identity.ts +261 -0
  51. package/src/components/diagnose/investigationEvidence/index.ts +55 -0
  52. package/src/components/diagnose/investigationEvidence/observations.ts +954 -0
  53. package/src/components/diagnose/investigationEvidence/parse.ts +467 -0
  54. package/src/components/diagnose/investigationEvidence/projection.test.ts +2703 -0
  55. package/src/components/diagnose/investigationEvidence/types.ts +512 -0
  56. package/src/components/diagnose/investigationEvidenceKinds.ts +218 -0
  57. package/src/components/diagnose/investigationEvidencePresentation.test.ts +31 -0
  58. package/src/components/diagnose/investigationEvidencePresentation.ts +1 -0
  59. package/src/components/diagnose/investigationMetrics.test.ts +712 -0
  60. package/src/components/diagnose/investigationMetrics.ts +393 -0
  61. package/src/components/diagnose/investigationSourceFocus.ts +2 -0
  62. package/src/components/diagnose/investigationState.test.ts +322 -0
  63. package/src/components/diagnose/investigationState.ts +147 -25
  64. package/src/components/diagnose/parts.test.tsx +482 -0
  65. package/src/components/diagnose/parts.tsx +418 -41
  66. package/src/components/diagnose/target.test.ts +19 -0
  67. package/src/components/diagnose/target.ts +3 -2
  68. package/src/components/gitops/GitOpsView.tsx +7 -0
  69. package/src/components/gitops/RemoteDestinationCloudHint.tsx +26 -0
  70. package/src/components/resource/PrometheusCharts.overlays.test.ts +22 -0
  71. package/src/components/resource/PrometheusCharts.tsx +4 -3
  72. package/src/components/resource/PrometheusChartsGrid.render.test.tsx +99 -0
  73. package/src/components/resource/PrometheusChartsGrid.tsx +245 -103
  74. package/src/components/resource/RightsizingStrip.test.ts +38 -0
  75. package/src/components/resource/RightsizingStrip.tsx +15 -3
  76. package/src/components/resource/WorkloadMetricsHelpDialog.test.tsx +79 -0
  77. package/src/components/resource/WorkloadMetricsHelpDialog.tsx +145 -0
  78. package/src/components/resource/WorkloadMetricsSection.render.test.tsx +351 -0
  79. package/src/components/resource/WorkloadMetricsSection.tsx +486 -0
  80. package/src/components/resource/workloadMetricValues.test.ts +65 -0
  81. package/src/components/resource/workloadMetricValues.ts +30 -0
  82. package/src/components/resources/PodFilePreview.test.tsx +131 -0
  83. package/src/components/resources/PodFilePreview.tsx +394 -0
  84. package/src/components/resources/PodFilesystemModal.tsx +157 -67
  85. package/src/components/resources/ResourcesView.tsx +44 -6
  86. package/src/components/resources/renderers/NamespaceRenderer.tsx +7 -1
  87. package/src/components/resources/renderers/PodRenderer.tsx +4 -0
  88. package/src/components/resources/renderers/RolloutRenderer.tsx +15 -1
  89. package/src/components/resources/renderers/WorkloadRenderer.tsx +3 -0
  90. package/src/components/rightsizing/RightsizingScanView.tsx +21 -0
  91. package/src/components/settings/SettingsDialog.tsx +94 -14
  92. package/src/components/traffic/TrafficFlowList.tsx +229 -70
  93. package/src/components/traffic/TrafficGraph.tsx +6 -6
  94. package/src/components/traffic/TrafficView.tsx +10 -9
  95. package/src/components/traffic/trafficFilters.test.ts +28 -1
  96. package/src/components/traffic/trafficFilters.ts +31 -0
  97. package/src/components/workload/WorkloadView.tsx +19 -8
  98. package/src/context/ConnectionContext.tsx +48 -3
  99. package/src/context/DiagnoseCustomization.test.tsx +26 -0
  100. package/src/context/DiagnoseCustomization.tsx +15 -2
  101. package/src/index.css +17 -0
  102. package/src/index.ts +1 -0
  103. package/src/utils/navigation.ts +1 -1
  104. package/src/utils/shell-safe.test.ts +25 -1
  105. package/src/utils/shell-safe.ts +16 -0
  106. package/src/utils/topology-namespace.test.ts +64 -0
  107. package/src/utils/topology-namespace.ts +18 -0
  108. package/src/components/diagnose/investigationEvidence.test.ts +0 -5202
  109. package/src/components/diagnose/investigationEvidence.ts +0 -4784
  110. package/src/components/gitops/GitOpsActions.tsx +0 -1
  111. package/src/components/gitops/GitOpsStatusBadge.tsx +0 -1
  112. package/src/components/gitops/ManagedResourcesList.tsx +0 -1
  113. package/src/components/gitops/SyncCountdown.tsx +0 -1
  114. package/src/components/gitops/index.ts +0 -4
  115. package/src/components/helm/ManifestDiffViewer.tsx +0 -69
  116. package/src/components/logs/JsonLogLine.tsx +0 -1
  117. package/src/components/logs/LogCore.tsx +0 -2
  118. package/src/components/logs/useLogBuffer.ts +0 -2
  119. package/src/components/logs/useLogSearch.ts +0 -1
  120. package/src/components/resource-drawer/ResourceDrawer.tsx +0 -216
  121. package/src/components/resources/drawer-components.tsx +0 -1
  122. package/src/components/resources/renderers/AlertRenderer.tsx +0 -1
  123. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +0 -1
  124. package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +0 -1
  125. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +0 -1
  126. package/src/components/resources/renderers/CertificateRenderer.tsx +0 -1
  127. package/src/components/resources/renderers/CertificateRequestRenderer.tsx +0 -1
  128. package/src/components/resources/renderers/ChallengeRenderer.tsx +0 -1
  129. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +0 -1
  130. package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +0 -1
  131. package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +0 -1
  132. package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +0 -1
  133. package/src/components/resources/renderers/ConfigMapRenderer.tsx +0 -1
  134. package/src/components/resources/renderers/CronJobRenderer.tsx +0 -1
  135. package/src/components/resources/renderers/CronWorkflowRenderer.tsx +0 -1
  136. package/src/components/resources/renderers/EventRenderer.tsx +0 -1
  137. package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +0 -1
  138. package/src/components/resources/renderers/ExternalSecretRenderer.tsx +0 -1
  139. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +0 -1
  140. package/src/components/resources/renderers/GRPCRouteRenderer.tsx +0 -1
  141. package/src/components/resources/renderers/GatewayClassRenderer.tsx +0 -1
  142. package/src/components/resources/renderers/GatewayRenderer.tsx +0 -1
  143. package/src/components/resources/renderers/GenericRenderer.tsx +0 -1
  144. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +0 -1
  145. package/src/components/resources/renderers/HTTPRouteRenderer.tsx +0 -1
  146. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +0 -1
  147. package/src/components/resources/renderers/IngressClassRenderer.tsx +0 -1
  148. package/src/components/resources/renderers/IngressRenderer.tsx +0 -1
  149. package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +0 -1
  150. package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +0 -1
  151. package/src/components/resources/renderers/IstioGatewayRenderer.tsx +0 -1
  152. package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +0 -1
  153. package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +0 -1
  154. package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +0 -1
  155. package/src/components/resources/renderers/JobRenderer.tsx +0 -1
  156. package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +0 -1
  157. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +0 -1
  158. package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +0 -1
  159. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +0 -1
  160. package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +0 -1
  161. package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +0 -1
  162. package/src/components/resources/renderers/KnativeEventingRenderer.tsx +0 -1
  163. package/src/components/resources/renderers/KnativeFlowRenderer.tsx +0 -1
  164. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +0 -1
  165. package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +0 -1
  166. package/src/components/resources/renderers/KnativeRouteRenderer.tsx +0 -1
  167. package/src/components/resources/renderers/KnativeServiceRenderer.tsx +0 -1
  168. package/src/components/resources/renderers/KnativeSourceRenderer.tsx +0 -1
  169. package/src/components/resources/renderers/KustomizationRenderer.tsx +0 -1
  170. package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +0 -1
  171. package/src/components/resources/renderers/LeaseRenderer.tsx +0 -1
  172. package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +0 -1
  173. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +0 -1
  174. package/src/components/resources/renderers/OrderRenderer.tsx +0 -1
  175. package/src/components/resources/renderers/PersistentVolumeRenderer.tsx +0 -1
  176. package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +0 -1
  177. package/src/components/resources/renderers/PodMonitorRenderer.tsx +0 -1
  178. package/src/components/resources/renderers/PriorityClassRenderer.tsx +0 -1
  179. package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +0 -1
  180. package/src/components/resources/renderers/ReplicaSetRenderer.tsx +0 -1
  181. package/src/components/resources/renderers/RuntimeClassRenderer.tsx +0 -1
  182. package/src/components/resources/renderers/SbomReportRenderer.tsx +0 -1
  183. package/src/components/resources/renderers/SealedSecretRenderer.tsx +0 -1
  184. package/src/components/resources/renderers/SecretRenderer.tsx +0 -1
  185. package/src/components/resources/renderers/SecretStoreRenderer.tsx +0 -1
  186. package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +0 -1
  187. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +0 -1
  188. package/src/components/resources/renderers/StorageClassRenderer.tsx +0 -1
  189. package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +0 -1
  190. package/src/components/resources/renderers/VPARenderer.tsx +0 -1
  191. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +0 -1
  192. package/src/components/resources/renderers/VeleroVSLRenderer.tsx +0 -1
  193. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +0 -1
  194. package/src/components/resources/renderers/WebhookConfigRenderer.tsx +0 -1
  195. package/src/components/resources/renderers/WorkflowRenderer.tsx +0 -1
  196. package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +0 -1
  197. package/src/components/resources/renderers/argo-cells.tsx +0 -1
  198. package/src/components/resources/renderers/certmanager-cells.tsx +0 -1
  199. package/src/components/resources/renderers/cnpg-cells.tsx +0 -1
  200. package/src/components/resources/renderers/eso-cells.tsx +0 -1
  201. package/src/components/resources/renderers/flux-cells.tsx +0 -1
  202. package/src/components/resources/renderers/index.ts +0 -94
  203. package/src/components/resources/renderers/istio-cells.tsx +0 -1
  204. package/src/components/resources/renderers/karpenter-cells.tsx +0 -1
  205. package/src/components/resources/renderers/keda-cells.tsx +0 -1
  206. package/src/components/resources/renderers/knative-cells.tsx +0 -1
  207. package/src/components/resources/renderers/kyverno-cells.tsx +0 -1
  208. package/src/components/resources/renderers/prometheus-cells.tsx +0 -1
  209. package/src/components/resources/renderers/traefik-cells.tsx +0 -1
  210. package/src/components/resources/renderers/trivy-cells.tsx +0 -1
  211. package/src/components/resources/renderers/trivy-shared.tsx +0 -1
  212. package/src/components/resources/renderers/velero-cells.tsx +0 -1
  213. package/src/components/resources/resource-utils-argo.ts +0 -2
  214. package/src/components/resources/resource-utils-certmanager.ts +0 -2
  215. package/src/components/resources/resource-utils-eso.ts +0 -2
  216. package/src/components/resources/resource-utils-flux.ts +0 -2
  217. package/src/components/resources/resource-utils-istio.ts +0 -2
  218. package/src/components/resources/resource-utils-karpenter.ts +0 -2
  219. package/src/components/resources/resource-utils-knative.ts +0 -2
  220. package/src/components/resources/resource-utils-kyverno.ts +0 -2
  221. package/src/components/resources/resource-utils-prometheus.ts +0 -2
  222. package/src/components/resources/resource-utils-traefik.ts +0 -1
  223. package/src/components/resources/resource-utils-trivy.ts +0 -2
  224. package/src/components/resources/resource-utils-velero.ts +0 -2
  225. package/src/components/shared/EditableYamlView.tsx +0 -24
  226. package/src/components/timeline/DiffViewer.tsx +0 -1
  227. package/src/components/traffic/index.ts +0 -3
  228. package/src/components/ui/ForceDeleteConfirmDialog.tsx +0 -1
  229. package/src/components/ui/MetricsChart.tsx +0 -1
  230. package/src/components/ui/ResourceBar.tsx +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyhook-io/radar-app",
3
- "version": "1.13.4",
3
+ "version": "1.14.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",
@@ -29,7 +29,7 @@
29
29
  "lint": "eslint src && node scripts/check-no-native-title.mjs"
30
30
  },
31
31
  "dependencies": {
32
- "@fontsource-variable/dm-sans": "^5.2.8",
32
+ "@fontsource-variable/dm-sans": "^5.3.0",
33
33
  "@fontsource/dm-mono": "^5.3.0",
34
34
  "@monaco-editor/react": "^4.7.0",
35
35
  "diff": "^9.0.0",
@@ -60,9 +60,9 @@
60
60
  "@tailwindcss/vite": "^4.3.3",
61
61
  "@tanstack/react-query": "^5.102.8",
62
62
  "@types/node": "^26.2.0",
63
- "@types/react": "^19.2.18",
64
- "@types/react-dom": "^19.2.3",
65
- "@vitejs/plugin-react": "^6.1.0",
63
+ "@types/react": "^19.3.0",
64
+ "@types/react-dom": "^19.3.0",
65
+ "@vitejs/plugin-react": "^6.1.1",
66
66
  "@xyflow/react": "^12.11.2",
67
67
  "clsx": "^2.1.1",
68
68
  "elkjs": "^0.11.1",
@@ -73,13 +73,13 @@
73
73
  "lucide-react": "^1.37.0",
74
74
  "postcss": "^8.5.28",
75
75
  "prettier": "^3.9.5",
76
- "react": "^19.2.8",
77
- "react-dom": "^19.2.8",
76
+ "react": "^19.3.0",
77
+ "react-dom": "^19.3.0",
78
78
  "react-router-dom": "^7.18.3",
79
79
  "tailwind-merge": "^3.6.0",
80
80
  "tailwindcss": "^4.3.3",
81
81
  "typescript": "^6.0.2",
82
- "typescript-eslint": "^8.67.0",
82
+ "typescript-eslint": "^8.70.0",
83
83
  "vite": "^8.2.2"
84
84
  },
85
85
  "sideEffects": [
package/src/App.tsx CHANGED
@@ -43,6 +43,7 @@ import { useMediaQuery } from './hooks/useMediaQuery'
43
43
  import { ContextSwitchProvider, useContextSwitch } from './context/ContextSwitchContext'
44
44
  import { ConnectionProvider, useConnection } from './context/ConnectionContext'
45
45
  import { ConnectionErrorView } from './components/ConnectionErrorView'
46
+ import { SyncProgressPanel } from './components/SyncProgressPanel'
46
47
  import { CapabilitiesProvider, useCapabilitiesContext } from './contexts/CapabilitiesContext'
47
48
  import { UserMenu } from './components/UserMenu'
48
49
  import { ErrorBoundary } from './components/ui/ErrorBoundary'
@@ -53,6 +54,7 @@ import { DiagnosticsOverlay } from './components/ui/DiagnosticsOverlay'
53
54
  import { useEventSource } from './hooks/useEventSource'
54
55
  import { debugNamespaceLog, useNamespaces, useNamespaceScope, useSetActiveNamespace, useSwitchContext, useAuthMe, useAudit } from './api/client'
55
56
  import { buildAuditSeverityMap } from './utils/auditBadges'
57
+ import { isInNamespaceScope, scopeNodesToNamespaces } from './utils/topology-namespace'
56
58
  import { routePath, apiUrl, getAuthHeaders, getCredentialsMode, stripBasename } from './api/config'
57
59
  import { KeyboardShortcutProvider, useRegisterShortcut, useRegisterShortcuts, useSuppressBaseShortcuts } from './hooks/useKeyboardShortcuts'
58
60
  import { useAnimatedUnmount } from './hooks/useAnimatedUnmount'
@@ -992,6 +994,15 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
992
994
  const contentReady = !isSwitching && !authMePending &&
993
995
  !(authMe?.authEnabled && !authMe?.username) && connection.state === 'connected'
994
996
 
997
+ // Progressive shell during the initial informer sync: once the server
998
+ // publishes per-kind sync progress, render the app instead of the splash.
999
+ // Resource views serve kinds as they become ready; everything else shows
1000
+ // the sync progress panel until 'connected'.
1001
+ const shellDuringSync = !isSwitching && !authMePending &&
1002
+ !(authMe?.authEnabled && !authMe?.username) &&
1003
+ connection.state === 'connecting' && !!connection.syncStatus?.kinds?.length
1004
+ const viewsSyncGated = shellDuringSync && mainView !== 'resources'
1005
+
995
1006
  const { clusterLoadState, showHomeClusterLoadFallback, clusterLoadInitial } = useClusterLoadState({
996
1007
  namespaces,
997
1008
  mainView,
@@ -1245,16 +1256,22 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
1245
1256
  const clusterConnectionState = connection.state
1246
1257
  const clusterConnected = clusterConnectionState === 'connected'
1247
1258
  const liveUpdatesDisconnected = clusterConnected && !eventStreamConnected && !eventStreamConnecting
1259
+ // During the progressive shell, the header label carries the global sync
1260
+ // progress — the one place that explains why some views are open and
1261
+ // others still loading.
1262
+ const syncProgressLabel = connection.syncStatus
1263
+ ? `Loading cluster data — ${connection.syncStatus.criticalSynced + connection.syncStatus.deferredSynced} of ${connection.syncStatus.criticalTotal + connection.syncStatus.deferredTotal} ready`
1264
+ : 'Connecting'
1248
1265
  const headerConnectionLabel =
1249
1266
  clusterConnectionState === 'disconnected' ? 'Disconnected' :
1250
- clusterConnectionState === 'connecting' ? 'Connecting' :
1267
+ clusterConnectionState === 'connecting' ? syncProgressLabel :
1251
1268
  liveUpdatesDisconnected ? 'Live updates disconnected' :
1252
1269
  clusterLoadState.loading ? `Connected — ${clusterLoadState.message}` :
1253
1270
  crdDiscoveryStatus === 'discovering' ? 'Connected — discovering Custom Resources...' :
1254
1271
  'Connected'
1255
1272
  const headerConnectionDisplayLabel =
1256
1273
  clusterConnectionState === 'disconnected' ? 'Disconnected' :
1257
- clusterConnectionState === 'connecting' ? 'Connecting' :
1274
+ clusterConnectionState === 'connecting' ? syncProgressLabel :
1258
1275
  liveUpdatesDisconnected ? 'Live updates disconnected' :
1259
1276
  showClusterWarmupLabel ? clusterLoadState.message :
1260
1277
  crdDiscoveryStatus === 'discovering' ? 'Discovering Custom Resources…' :
@@ -1604,8 +1621,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
1604
1621
  // Filter by namespace (client-side) and by visible kinds
1605
1622
  const nsSet = namespaces.length > 0 ? new Set(namespaces) : null
1606
1623
  const filteredNodes = displayedTopology.nodes.filter(node =>
1607
- effectiveKinds.has(node.kind) &&
1608
- (!nsSet || nsSet.has(node.data.namespace as string) || !(node.data.namespace as string))
1624
+ effectiveKinds.has(node.kind) && isInNamespaceScope(node, nsSet)
1609
1625
  )
1610
1626
  const filteredNodeIds = new Set(filteredNodes.map(n => n.id))
1611
1627
 
@@ -1630,6 +1646,16 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
1630
1646
  }
1631
1647
  }, [displayedTopology, visibleKinds, namespaces, topologyMode])
1632
1648
 
1649
+ // Namespace-scoped but NOT kind-filtered: the sidebar derives its kind list
1650
+ // and visible/hidden footer counts from this prop, so handing it the
1651
+ // kind-filtered graph nodes would drop every hidden kind from the list and
1652
+ // "hidden" would always read zero. Reads displayedTopology so the counts
1653
+ // freeze with the graph while paused.
1654
+ const filterSidebarNodes = useMemo(() => {
1655
+ if (!displayedTopology) return []
1656
+ return scopeNodesToNamespaces(displayedTopology.nodes, namespaces)
1657
+ }, [displayedTopology, namespaces])
1658
+
1633
1659
  // Cluster Audit findings, joined onto topology nodes by the audit key the
1634
1660
  // backend stamps on each node (data.auditKey). Only badge-worthy findings
1635
1661
  // reach the graph; the raw auditDanger/auditWarning property names remain at
@@ -2021,7 +2047,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
2021
2047
  {/* Connecting view — shown during initial connection or retry.
2022
2048
  Icon is pane-anchored so its screen position matches the
2023
2049
  host hub splash across cross-document transitions. */}
2024
- {!isSwitching && !(authMe?.authEnabled && !authMe?.username) && connection.state === 'connecting' && (
2050
+ {!isSwitching && !(authMe?.authEnabled && !authMe?.username) && connection.state === 'connecting' && !shellDuringSync && (
2025
2051
  <PaneLoader
2026
2052
  label="Connecting to cluster"
2027
2053
  className="flex-1 min-h-0 bg-theme-base"
@@ -2082,13 +2108,21 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
2082
2108
  {/* inert while a fullscreen detail overlay covers the views — keeps the
2083
2109
  retained background list out of the focus order + a11y tree (the visual
2084
2110
  cover already blocks pointer events). */}
2085
- {contentReady && <div className="flex-1 flex overflow-hidden" inert={expandedView}>
2111
+ {(contentReady || shellDuringSync) && <div className="flex-1 flex overflow-hidden" inert={expandedView}>
2086
2112
  {/* Search included, not just the path: selection inside a view rides in
2087
2113
  the query (?resource=, ?release=), so a path-only key would still
2088
2114
  strand a crash on the view that produced it. */}
2089
2115
  <ErrorBoundary resetKey={location.pathname + location.search}>
2116
+ {/* Initial sync in progress: views that need the full cluster dataset
2117
+ show per-kind progress instead; resource views work as kinds sync. */}
2118
+ {viewsSyncGated && connection.syncStatus && (
2119
+ <SyncProgressPanel
2120
+ syncStatus={connection.syncStatus}
2121
+ onNavigateToKind={(key) => navigate({ pathname: `/resources/${key}` })}
2122
+ />
2123
+ )}
2090
2124
  {/* Home dashboard */}
2091
- {mainView === 'home' && (
2125
+ {!viewsSyncGated && mainView === 'home' && (
2092
2126
  <HomeView
2093
2127
  namespaces={namespaces}
2094
2128
  topology={topology}
@@ -2147,7 +2181,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
2147
2181
  )}
2148
2182
 
2149
2183
  {/* Topology view */}
2150
- {mainView === 'topology' && (
2184
+ {!viewsSyncGated && mainView === 'topology' && (
2151
2185
  <>
2152
2186
  {topology?.requiresNamespaceFilter && namespaces.length === 0 ? (
2153
2187
  /* Large cluster: prompt user to select a namespace */
@@ -2182,14 +2216,14 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
2182
2216
  <>
2183
2217
  {/* Filter sidebar */}
2184
2218
  <TopologyFilterSidebar
2185
- nodes={topology?.nodes || []}
2219
+ nodes={filterSidebarNodes}
2186
2220
  visibleKinds={visibleKinds}
2187
2221
  onToggleKind={handleToggleKind}
2188
2222
  onShowAll={handleShowAllKinds}
2189
2223
  onHideAll={handleHideAllKinds}
2190
2224
  collapsed={filterSidebarCollapsed}
2191
2225
  onToggleCollapse={() => setFilterSidebarCollapsed(prev => !prev)}
2192
- hiddenKinds={topology?.hiddenKinds}
2226
+ hiddenKinds={displayedTopology?.hiddenKinds}
2193
2227
  onEnableHiddenKind={(kind) => {
2194
2228
  setVisibleKinds(prev => new Set(prev).add(kind as NodeKind))
2195
2229
  console.log(`[topology] User requested to show hidden kind: ${kind}`)
@@ -2275,7 +2309,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
2275
2309
  )}
2276
2310
 
2277
2311
  {/* Timeline view */}
2278
- {mainView === 'timeline' && (
2312
+ {!viewsSyncGated && mainView === 'timeline' && (
2279
2313
  <TimelineView
2280
2314
  namespaces={namespaces}
2281
2315
  onResourceClick={(resource) => {
@@ -2293,7 +2327,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
2293
2327
  />
2294
2328
  )}
2295
2329
 
2296
- {mainView === 'helm' && (
2330
+ {!viewsSyncGated && mainView === 'helm' && (
2297
2331
  <HelmView
2298
2332
  namespaces={namespaces}
2299
2333
  selectedRelease={selectedHelmRelease}
@@ -2301,13 +2335,13 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
2301
2335
  />
2302
2336
  )}
2303
2337
 
2304
- {mainView === 'helmCompare' && (
2338
+ {!viewsSyncGated && mainView === 'helmCompare' && (
2305
2339
  <HelmCompareRoute />
2306
2340
  )}
2307
2341
 
2308
2342
  {/* GitOps view (inline only when the host hasn't taken it over — see
2309
2343
  the takeover splash below). */}
2310
- {mainView === 'gitops' && !isViewTakenOver('gitops') && (
2344
+ {!viewsSyncGated && mainView === 'gitops' && !isViewTakenOver('gitops') && (
2311
2345
  <GitOpsView
2312
2346
  namespaces={namespaces}
2313
2347
  onOpenResource={(resource) => {
@@ -2317,12 +2351,14 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
2317
2351
  navigateToResource(resource)
2318
2352
  }}
2319
2353
  onClearNamespaces={clearAllNamespaces}
2320
- onOpenSettings={() => openSettings()}
2354
+ // Every GitOps "open Settings" ask is about the Argo CD connection
2355
+ // (diff CTA, per-resource health notice), so land on that section.
2356
+ onOpenSettings={() => openSettings('argocd')}
2321
2357
  />
2322
2358
  )}
2323
2359
 
2324
2360
  {/* Applications view — deployable software grouped by app/release evidence */}
2325
- {mainView === 'applications' && (
2361
+ {!viewsSyncGated && mainView === 'applications' && (
2326
2362
  <ApplicationsView
2327
2363
  namespaces={namespaces}
2328
2364
  onOpenResource={(resource) => {
@@ -2343,16 +2379,16 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
2343
2379
  )}
2344
2380
 
2345
2381
  {/* Traffic view */}
2346
- {mainView === 'traffic' && (
2382
+ {!viewsSyncGated && mainView === 'traffic' && (
2347
2383
  <TrafficView namespaces={namespaces} />
2348
2384
  )}
2349
2385
 
2350
2386
  {/* Cost detail view */}
2351
- {mainView === 'cost' && (
2387
+ {!viewsSyncGated && mainView === 'cost' && (
2352
2388
  <CostView namespaces={namespaces} onBack={() => setMainView('home')} onOpenResource={navigateToResource} />
2353
2389
  )}
2354
2390
 
2355
- {mainView === 'capacity' && (
2391
+ {!viewsSyncGated && mainView === 'capacity' && (
2356
2392
  <CapacityView onOpenResource={navigateToResource} />
2357
2393
  )}
2358
2394
 
@@ -2370,7 +2406,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
2370
2406
 
2371
2407
  {/* Checks detail view. Cloud can take over fleet best practices while
2372
2408
  the target-specific upgrade route continues to render locally. */}
2373
- {mainView === 'checks' && (!isViewTakenOver('checks') || upgradeReadinessRoute) && (
2409
+ {!viewsSyncGated && mainView === 'checks' && (!isViewTakenOver('checks') || upgradeReadinessRoute) && (
2374
2410
  <AuditView
2375
2411
  namespaces={namespaces}
2376
2412
  onNavigateToResource={navigateToResourceList}
@@ -2382,7 +2418,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
2382
2418
  Hub fleet uses; a GitOps reconciler subject routes to its detail page,
2383
2419
  other resources open the standard resource view. Inline only when the
2384
2420
  host hasn't taken it over. */}
2385
- {mainView === 'issues' && !isViewTakenOver('issues') && (
2421
+ {!viewsSyncGated && mainView === 'issues' && !isViewTakenOver('issues') && (
2386
2422
  <IssuesPane
2387
2423
  namespaces={namespaces}
2388
2424
  onNavigateToResource={navigateFromIssue}
@@ -2392,7 +2428,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
2392
2428
  {/* Workload full view — the standalone fullscreen route for non-list
2393
2429
  surfaces and deep links. Expand-from-drawer is the ?full=1 overlay on
2394
2430
  /resources instead, so it never routes here. */}
2395
- {mainView === 'workload' && (
2431
+ {!viewsSyncGated && mainView === 'workload' && (
2396
2432
  <WorkloadViewRoute
2397
2433
  onNavigateToResource={(resource) => {
2398
2434
  navigate(relatedResourcePath(resource))
@@ -2401,7 +2437,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
2401
2437
  )}
2402
2438
 
2403
2439
  {/* Compare two resources of the same kind side-by-side */}
2404
- {mainView === 'compare' && <CompareViewRoute />}
2440
+ {!viewsSyncGated && mainView === 'compare' && <CompareViewRoute />}
2405
2441
 
2406
2442
  </ErrorBoundary>
2407
2443
  </div>}
@@ -2410,7 +2446,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
2410
2446
  {/* Resource detail drawer — stays mounted, expands to full-screen WorkloadView.
2411
2447
  Gated on contentReady so it never renders over the connecting/switching
2412
2448
  splash (which would push the centered logo off-center). */}
2413
- {contentReady && resourceDrawer.shouldRender && drawerResource && (
2449
+ {(contentReady || shellDuringSync) && resourceDrawer.shouldRender && drawerResource && (
2414
2450
  <ResourceDetailDrawer
2415
2451
  resource={drawerResource}
2416
2452
  initialTab={drawerInitialTab}
package/src/RadarApp.tsx CHANGED
@@ -42,6 +42,7 @@ import type { TimelineSourceConfig } from "./api/timelineSource";
42
42
  import { DiagnoseCustomizationProvider } from "./context/DiagnoseCustomization";
43
43
  import type {
44
44
  RenderDiagnoseAction,
45
+ RenderInvestigationRunActions,
45
46
  DiagnoseConsentCopy,
46
47
  } from "./context/DiagnoseCustomization";
47
48
  import { defaultDiagnoseAction } from "./components/diagnose/LocalDiagnoseAction";
@@ -113,6 +114,8 @@ export interface RadarAppProps {
113
114
  * agent-free. See ./context/DiagnoseCustomization for the render-prop shape.
114
115
  */
115
116
  renderDiagnoseAction?: RenderDiagnoseAction;
117
+ /** Host-owned controls for the focused investigation; absent in standalone Radar. */
118
+ renderInvestigationRunActions?: RenderInvestigationRunActions;
116
119
  /**
117
120
  * Replaces the first-run consent card's trust copy. REQUIRED of any host whose
118
121
  * backend runs the agent somewhere other than the user's own machine — the
@@ -202,6 +205,7 @@ export function RadarApp({
202
205
  manageDocumentTitle = false,
203
206
  documentTitleSuffix,
204
207
  renderDiagnoseAction,
208
+ renderInvestigationRunActions,
205
209
  diagnoseConsent,
206
210
  initialPath,
207
211
  onClusterLoadStateChange,
@@ -234,6 +238,7 @@ export function RadarApp({
234
238
  <DiagnoseCustomizationProvider
235
239
  value={renderDiagnoseAction ?? defaultDiagnoseAction}
236
240
  consentCopy={diagnoseConsent}
241
+ renderRunActions={renderInvestigationRunActions}
237
242
  >
238
243
  <DiagnoseProvider
239
244
  browserURLState={router !== "memory"}
@@ -1,4 +1,4 @@
1
- import { beforeEach, describe, expect, it, vi } from 'vitest'
1
+ import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
2
2
 
3
3
  // Deterministic config so the 401 handler's basename/routePath math is fixed and
4
4
  // independent of window/env. client.ts only consumes these six exports.
@@ -67,6 +67,17 @@ function make401(authMode: string): Response {
67
67
  } as unknown as Response
68
68
  }
69
69
 
70
+ // Each test re-imports ./client after resetModules, and the first import in the
71
+ // file is the one that transforms client.ts and everything it pulls in. That is
72
+ // seconds of work on a contended machine, and it would otherwise be charged to
73
+ // whichever test happens to run first, failing it on timeout while the rest of
74
+ // the suite passes in under a second. Pay it once, outside any test's budget —
75
+ // resetModules drops module instances but keeps the transform cache, so the
76
+ // per-test re-import stays cheap.
77
+ beforeAll(async () => {
78
+ await import('./client')
79
+ }, 120_000)
80
+
70
81
  beforeEach(() => {
71
82
  // Reset module state so the redirect gate starts fresh.
72
83
  vi.resetModules()
@@ -0,0 +1,24 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ PROM_STATUS_POLL_DISCOVERING_MS,
4
+ PROM_STATUS_POLL_IDLE_MS,
5
+ prometheusStatusRefetchInterval,
6
+ } from './client'
7
+
8
+ describe('prometheusStatusRefetchInterval', () => {
9
+ it('polls quickly while discovery is in flight or has not reported yet', () => {
10
+ expect(prometheusStatusRefetchInterval(undefined)).toBe(PROM_STATUS_POLL_IDLE_MS)
11
+ // Read before the server's run started: transient, keep polling.
12
+ expect(prometheusStatusRefetchInterval({ available: false, connected: false })).toBe(PROM_STATUS_POLL_DISCOVERING_MS)
13
+ // A run that ended without a connection carries its outcome.
14
+ expect(prometheusStatusRefetchInterval({ available: false, connected: false, error: 'no Prometheus service found in cluster' })).toBe(
15
+ PROM_STATUS_POLL_IDLE_MS,
16
+ )
17
+ expect(prometheusStatusRefetchInterval({ available: false, connected: false, discovering: true })).toBe(
18
+ PROM_STATUS_POLL_DISCOVERING_MS,
19
+ )
20
+ expect(prometheusStatusRefetchInterval({ available: true, connected: true, address: 'http://localhost:1' })).toBe(
21
+ PROM_STATUS_POLL_IDLE_MS,
22
+ )
23
+ })
24
+ })