@skyhook-io/radar-app 1.13.5 → 1.14.1

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 (242) hide show
  1. package/package.json +9 -9
  2. package/src/App.tsx +66 -28
  3. package/src/api/client.authRedirect.test.ts +12 -1
  4. package/src/api/client.prometheusStatus.test.ts +24 -0
  5. package/src/api/client.ts +324 -16
  6. package/src/api/diagnose.ts +32 -1
  7. package/src/api/drain-plan.test.ts +86 -0
  8. package/src/api/quotas.test.ts +26 -0
  9. package/src/api/quotas.ts +20 -0
  10. package/src/api/workloadMetrics.test.ts +40 -0
  11. package/src/api/workloadMetrics.ts +68 -0
  12. package/src/components/CloudConnectFlow.tsx +343 -46
  13. package/src/components/CloudFunnelButton.tsx +110 -37
  14. package/src/components/ConnectionErrorView.tsx +9 -10
  15. package/src/components/DebugOverlay.tsx +10 -6
  16. package/src/components/SyncProgressPanel.tsx +73 -0
  17. package/src/components/audit/AuditView.tsx +2 -0
  18. package/src/components/cloudConnectHandoff.test.ts +245 -0
  19. package/src/components/cloudConnectHandoff.ts +276 -0
  20. package/src/components/curl/ServiceCurlButton.tsx +19 -26
  21. package/src/components/diagnose/AgentCase.tsx +18 -5
  22. package/src/components/diagnose/AnalysisStory.test.tsx +308 -0
  23. package/src/components/diagnose/AnalysisStory.tsx +564 -0
  24. package/src/components/diagnose/DiagnoseContext.test.ts +10 -0
  25. package/src/components/diagnose/DiagnoseContext.tsx +26 -8
  26. package/src/components/diagnose/DiagnoseSurface.tsx +20 -14
  27. package/src/components/diagnose/InvestigationEvidencePane.story.test.tsx +771 -0
  28. package/src/components/diagnose/InvestigationEvidencePane.test.tsx +8 -33
  29. package/src/components/diagnose/InvestigationEvidencePane.tsx +809 -168
  30. package/src/components/diagnose/InvestigationResourceEvidence.test.tsx +30 -0
  31. package/src/components/diagnose/InvestigationResourceEvidence.tsx +20 -0
  32. package/src/components/diagnose/InvestigationView.tsx +493 -529
  33. package/src/components/diagnose/investigationCase.test.tsx +267 -129
  34. package/src/components/diagnose/investigationCase.ts +122 -77
  35. package/src/components/diagnose/investigationEvidence/adapters/changes.ts +60 -0
  36. package/src/components/diagnose/investigationEvidence/adapters/diagnose.test.ts +573 -0
  37. package/src/components/diagnose/investigationEvidence/adapters/diagnose.ts +935 -0
  38. package/src/components/diagnose/investigationEvidence/adapters/events.ts +96 -0
  39. package/src/components/diagnose/investigationEvidence/adapters/helm.test.ts +242 -0
  40. package/src/components/diagnose/investigationEvidence/adapters/helm.ts +208 -0
  41. package/src/components/diagnose/investigationEvidence/adapters/issues.ts +172 -0
  42. package/src/components/diagnose/investigationEvidence/adapters/logs.test.ts +283 -0
  43. package/src/components/diagnose/investigationEvidence/adapters/logs.ts +183 -0
  44. package/src/components/diagnose/investigationEvidence/adapters/permissions.test.ts +528 -0
  45. package/src/components/diagnose/investigationEvidence/adapters/permissions.ts +292 -0
  46. package/src/components/diagnose/investigationEvidence/adapters/prometheus.test.ts +1641 -0
  47. package/src/components/diagnose/investigationEvidence/adapters/prometheus.ts +769 -0
  48. package/src/components/diagnose/investigationEvidence/adapters/resource.test.ts +81 -0
  49. package/src/components/diagnose/investigationEvidence/adapters/resource.ts +285 -0
  50. package/src/components/diagnose/investigationEvidence/adapters/topology.ts +207 -0
  51. package/src/components/diagnose/investigationEvidence/builder.ts +427 -0
  52. package/src/components/diagnose/investigationEvidence/evidenceFixtures.ts +179 -0
  53. package/src/components/diagnose/investigationEvidence/identity.test.ts +632 -0
  54. package/src/components/diagnose/investigationEvidence/identity.ts +261 -0
  55. package/src/components/diagnose/investigationEvidence/index.ts +55 -0
  56. package/src/components/diagnose/investigationEvidence/observations.ts +978 -0
  57. package/src/components/diagnose/investigationEvidence/parse.ts +467 -0
  58. package/src/components/diagnose/investigationEvidence/projection.test.ts +2736 -0
  59. package/src/components/diagnose/investigationEvidence/types.ts +514 -0
  60. package/src/components/diagnose/investigationEvidencePresentation.test.ts +2 -0
  61. package/src/components/diagnose/investigationEvidencePresentation.ts +3 -0
  62. package/src/components/diagnose/investigationState.test.ts +316 -58
  63. package/src/components/diagnose/investigationState.ts +257 -44
  64. package/src/components/diagnose/investigationStory.test.ts +161 -0
  65. package/src/components/diagnose/investigationStory.ts +207 -0
  66. package/src/components/diagnose/parts.test.tsx +9 -6
  67. package/src/components/diagnose/parts.tsx +977 -193
  68. package/src/components/diagnose/storyParts.test.tsx +426 -0
  69. package/src/components/diagnose/target.test.ts +19 -0
  70. package/src/components/diagnose/target.ts +3 -2
  71. package/src/components/diagnose/toolCallLabel.test.ts +51 -0
  72. package/src/components/diagnose/toolCallLabel.ts +135 -0
  73. package/src/components/diagnose/useDisclosureReveal.ts +10 -11
  74. package/src/components/gitops/GitOpsView.tsx +7 -0
  75. package/src/components/gitops/RemoteDestinationCloudHint.tsx +26 -0
  76. package/src/components/helm/HelmCompareRoute.tsx +18 -4
  77. package/src/components/helm/HelmReleaseDrawer.tsx +11 -26
  78. package/src/components/helm/InstallWizard.tsx +8 -3
  79. package/src/components/home/MCPSetupDialog.tsx +15 -9
  80. package/src/components/portforward/PortForwardManager.tsx +6 -13
  81. package/src/components/resource/PrometheusCharts.overlays.test.ts +22 -0
  82. package/src/components/resource/PrometheusCharts.tsx +4 -3
  83. package/src/components/resource/PrometheusChartsGrid.render.test.tsx +99 -0
  84. package/src/components/resource/PrometheusChartsGrid.tsx +75 -35
  85. package/src/components/resource/RightsizingStrip.test.ts +38 -0
  86. package/src/components/resource/RightsizingStrip.tsx +15 -3
  87. package/src/components/resource/WorkloadMetricsHelpDialog.test.tsx +79 -0
  88. package/src/components/resource/WorkloadMetricsHelpDialog.tsx +145 -0
  89. package/src/components/resource/WorkloadMetricsSection.render.test.tsx +351 -0
  90. package/src/components/resource/WorkloadMetricsSection.tsx +480 -0
  91. package/src/components/resource/workloadMetricValues.test.ts +65 -0
  92. package/src/components/resource/workloadMetricValues.ts +30 -0
  93. package/src/components/resources/PodFilePreview.tsx +3 -3
  94. package/src/components/resources/ResourcesView.tsx +44 -6
  95. package/src/components/resources/renderers/NamespaceRenderer.tsx +7 -1
  96. package/src/components/resources/renderers/PodRenderer.tsx +4 -0
  97. package/src/components/resources/renderers/RolloutRenderer.tsx +15 -1
  98. package/src/components/resources/renderers/ServiceRenderer.tsx +2 -1
  99. package/src/components/resources/renderers/WorkloadRenderer.tsx +3 -0
  100. package/src/components/rightsizing/RightsizingScanView.tsx +21 -0
  101. package/src/components/settings/SettingsDialog.tsx +98 -16
  102. package/src/components/traffic/TrafficFlowList.tsx +229 -70
  103. package/src/components/traffic/TrafficGraph.tsx +6 -6
  104. package/src/components/traffic/TrafficView.tsx +10 -9
  105. package/src/components/traffic/trafficFilters.test.ts +28 -1
  106. package/src/components/traffic/trafficFilters.ts +31 -0
  107. package/src/components/ui/CommandPalette.tsx +10 -6
  108. package/src/components/ui/DiagnosticsOverlay.tsx +10 -6
  109. package/src/components/ui/Disclosure.tsx +47 -0
  110. package/src/components/ui/Markdown.tsx +23 -11
  111. package/src/components/ui/ShortcutHelpOverlay.tsx +3 -1
  112. package/src/components/ui/UpdateNotification.tsx +18 -2
  113. package/src/components/workload/WorkloadView.tsx +19 -8
  114. package/src/context/ConnectionContext.tsx +48 -3
  115. package/src/context/DiagnoseCustomization.tsx +1 -0
  116. package/src/index.css +36 -6
  117. package/src/utils/navigation.ts +1 -1
  118. package/src/utils/topology-namespace.test.ts +64 -0
  119. package/src/utils/topology-namespace.ts +18 -0
  120. package/src/components/diagnose/investigationEvidence.test.ts +0 -6617
  121. package/src/components/diagnose/investigationEvidence.ts +0 -5572
  122. package/src/components/gitops/GitOpsActions.tsx +0 -1
  123. package/src/components/gitops/GitOpsStatusBadge.tsx +0 -1
  124. package/src/components/gitops/ManagedResourcesList.tsx +0 -1
  125. package/src/components/gitops/SyncCountdown.tsx +0 -1
  126. package/src/components/gitops/index.ts +0 -4
  127. package/src/components/helm/ManifestDiffViewer.tsx +0 -69
  128. package/src/components/logs/JsonLogLine.tsx +0 -1
  129. package/src/components/logs/LogCore.tsx +0 -2
  130. package/src/components/logs/useLogBuffer.ts +0 -2
  131. package/src/components/logs/useLogSearch.ts +0 -1
  132. package/src/components/resource-drawer/ResourceDrawer.tsx +0 -216
  133. package/src/components/resources/drawer-components.tsx +0 -1
  134. package/src/components/resources/renderers/AlertRenderer.tsx +0 -1
  135. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +0 -1
  136. package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +0 -1
  137. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +0 -1
  138. package/src/components/resources/renderers/CertificateRenderer.tsx +0 -1
  139. package/src/components/resources/renderers/CertificateRequestRenderer.tsx +0 -1
  140. package/src/components/resources/renderers/ChallengeRenderer.tsx +0 -1
  141. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +0 -1
  142. package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +0 -1
  143. package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +0 -1
  144. package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +0 -1
  145. package/src/components/resources/renderers/ConfigMapRenderer.tsx +0 -1
  146. package/src/components/resources/renderers/CronJobRenderer.tsx +0 -1
  147. package/src/components/resources/renderers/CronWorkflowRenderer.tsx +0 -1
  148. package/src/components/resources/renderers/EventRenderer.tsx +0 -1
  149. package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +0 -1
  150. package/src/components/resources/renderers/ExternalSecretRenderer.tsx +0 -1
  151. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +0 -1
  152. package/src/components/resources/renderers/GRPCRouteRenderer.tsx +0 -1
  153. package/src/components/resources/renderers/GatewayClassRenderer.tsx +0 -1
  154. package/src/components/resources/renderers/GatewayRenderer.tsx +0 -1
  155. package/src/components/resources/renderers/GenericRenderer.tsx +0 -1
  156. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +0 -1
  157. package/src/components/resources/renderers/HTTPRouteRenderer.tsx +0 -1
  158. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +0 -1
  159. package/src/components/resources/renderers/IngressClassRenderer.tsx +0 -1
  160. package/src/components/resources/renderers/IngressRenderer.tsx +0 -1
  161. package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +0 -1
  162. package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +0 -1
  163. package/src/components/resources/renderers/IstioGatewayRenderer.tsx +0 -1
  164. package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +0 -1
  165. package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +0 -1
  166. package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +0 -1
  167. package/src/components/resources/renderers/JobRenderer.tsx +0 -1
  168. package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +0 -1
  169. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +0 -1
  170. package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +0 -1
  171. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +0 -1
  172. package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +0 -1
  173. package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +0 -1
  174. package/src/components/resources/renderers/KnativeEventingRenderer.tsx +0 -1
  175. package/src/components/resources/renderers/KnativeFlowRenderer.tsx +0 -1
  176. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +0 -1
  177. package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +0 -1
  178. package/src/components/resources/renderers/KnativeRouteRenderer.tsx +0 -1
  179. package/src/components/resources/renderers/KnativeServiceRenderer.tsx +0 -1
  180. package/src/components/resources/renderers/KnativeSourceRenderer.tsx +0 -1
  181. package/src/components/resources/renderers/KustomizationRenderer.tsx +0 -1
  182. package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +0 -1
  183. package/src/components/resources/renderers/LeaseRenderer.tsx +0 -1
  184. package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +0 -1
  185. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +0 -1
  186. package/src/components/resources/renderers/OrderRenderer.tsx +0 -1
  187. package/src/components/resources/renderers/PersistentVolumeRenderer.tsx +0 -1
  188. package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +0 -1
  189. package/src/components/resources/renderers/PodMonitorRenderer.tsx +0 -1
  190. package/src/components/resources/renderers/PriorityClassRenderer.tsx +0 -1
  191. package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +0 -1
  192. package/src/components/resources/renderers/ReplicaSetRenderer.tsx +0 -1
  193. package/src/components/resources/renderers/RuntimeClassRenderer.tsx +0 -1
  194. package/src/components/resources/renderers/SbomReportRenderer.tsx +0 -1
  195. package/src/components/resources/renderers/SealedSecretRenderer.tsx +0 -1
  196. package/src/components/resources/renderers/SecretRenderer.tsx +0 -1
  197. package/src/components/resources/renderers/SecretStoreRenderer.tsx +0 -1
  198. package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +0 -1
  199. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +0 -1
  200. package/src/components/resources/renderers/StorageClassRenderer.tsx +0 -1
  201. package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +0 -1
  202. package/src/components/resources/renderers/VPARenderer.tsx +0 -1
  203. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +0 -1
  204. package/src/components/resources/renderers/VeleroVSLRenderer.tsx +0 -1
  205. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +0 -1
  206. package/src/components/resources/renderers/WebhookConfigRenderer.tsx +0 -1
  207. package/src/components/resources/renderers/WorkflowRenderer.tsx +0 -1
  208. package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +0 -1
  209. package/src/components/resources/renderers/argo-cells.tsx +0 -1
  210. package/src/components/resources/renderers/certmanager-cells.tsx +0 -1
  211. package/src/components/resources/renderers/cnpg-cells.tsx +0 -1
  212. package/src/components/resources/renderers/eso-cells.tsx +0 -1
  213. package/src/components/resources/renderers/flux-cells.tsx +0 -1
  214. package/src/components/resources/renderers/index.ts +0 -94
  215. package/src/components/resources/renderers/istio-cells.tsx +0 -1
  216. package/src/components/resources/renderers/karpenter-cells.tsx +0 -1
  217. package/src/components/resources/renderers/keda-cells.tsx +0 -1
  218. package/src/components/resources/renderers/knative-cells.tsx +0 -1
  219. package/src/components/resources/renderers/kyverno-cells.tsx +0 -1
  220. package/src/components/resources/renderers/prometheus-cells.tsx +0 -1
  221. package/src/components/resources/renderers/traefik-cells.tsx +0 -1
  222. package/src/components/resources/renderers/trivy-cells.tsx +0 -1
  223. package/src/components/resources/renderers/trivy-shared.tsx +0 -1
  224. package/src/components/resources/renderers/velero-cells.tsx +0 -1
  225. package/src/components/resources/resource-utils-argo.ts +0 -2
  226. package/src/components/resources/resource-utils-certmanager.ts +0 -2
  227. package/src/components/resources/resource-utils-eso.ts +0 -2
  228. package/src/components/resources/resource-utils-flux.ts +0 -2
  229. package/src/components/resources/resource-utils-istio.ts +0 -2
  230. package/src/components/resources/resource-utils-karpenter.ts +0 -2
  231. package/src/components/resources/resource-utils-knative.ts +0 -2
  232. package/src/components/resources/resource-utils-kyverno.ts +0 -2
  233. package/src/components/resources/resource-utils-prometheus.ts +0 -2
  234. package/src/components/resources/resource-utils-traefik.ts +0 -1
  235. package/src/components/resources/resource-utils-trivy.ts +0 -2
  236. package/src/components/resources/resource-utils-velero.ts +0 -2
  237. package/src/components/shared/EditableYamlView.tsx +0 -24
  238. package/src/components/timeline/DiffViewer.tsx +0 -1
  239. package/src/components/traffic/index.ts +0 -3
  240. package/src/components/ui/ForceDeleteConfirmDialog.tsx +0 -1
  241. package/src/components/ui/MetricsChart.tsx +0 -1
  242. package/src/components/ui/ResourceBar.tsx +0 -1
@@ -1,7 +1,7 @@
1
1
  import { useState, useMemo, useCallback, useEffect } from 'react'
2
2
  import { useLocation, useNavigate } from 'react-router-dom'
3
3
  import { useQuery } from '@tanstack/react-query'
4
- import { ApiError, debugNamespaceLog, fetchJSON, isForbiddenError, useCapabilities, useNamespaceCapabilities, useResources, useSecretCertExpiry, useTopPodMetrics, useTopNodeMetrics, useBulkDeleteResources, useBulkRestartWorkloads, useBulkScaleWorkloads, useAudit } from '../../api/client'
4
+ import { ApiError, debugNamespaceLog, fetchJSON, isForbiddenError, isKindSyncFailed, isStillLoadingError, useCapabilities, useNamespaceCapabilities, useResources, useSecretCertExpiry, useTopPodMetrics, useTopNodeMetrics, useBulkDeleteResources, useBulkRestartWorkloads, useBulkScaleWorkloads, useAudit } from '../../api/client'
5
5
  import { isBadgeWorthy } from '../../utils/auditBadges'
6
6
  import type { AuditBadgeMessage } from '@skyhook-io/k8s-ui'
7
7
  import { apiUrl, getAuthHeaders, getCredentialsMode, stripBasename } from '../../api/config'
@@ -143,7 +143,18 @@ export function ResourcesView({ namespaces, selectedResource, onResourceClick, o
143
143
  }
144
144
  },
145
145
  staleTime: 10000,
146
- refetchInterval: 60000, // Safety net — SSE k8s_event drives near-real-time invalidation
146
+ // SSE invalidation isn't running while connecting, and mid-sync counts
147
+ // are what unlatch guarded kinds as their informers finish — poll fast
148
+ // during the shell, settle to the safety net once connected.
149
+ refetchInterval: connection.state === 'connecting' ? 3000 : 60000,
150
+ // During the first seconds of the progressive shell the endpoint 503s
151
+ // (cluster_connecting) until the mid-sync cache handle exists; keep the
152
+ // query pending rather than parking it in error state, which would
153
+ // unlatch the large-list guard at the connected flip.
154
+ retry: (failureCount: number, error: Error) =>
155
+ isStillLoadingError(error) ? true : failureCount < 3,
156
+ retryDelay: (failureCount: number, error: Error) =>
157
+ isStillLoadingError(error) ? 2000 : Math.min(1000 * 2 ** failureCount, 30000),
147
158
  })
148
159
 
149
160
  // Determine if selected kind is a CRD (only CRDs should send ?group= to backend)
@@ -200,8 +211,29 @@ export function ResourcesView({ namespaces, selectedResource, onResourceClick, o
200
211
  const isSelectedKindGuarded = selectedCountKey !== '' && LARGE_RESOURCE_LIST_GUARD_KEYS.has(selectedCountKey)
201
212
  const selectedKindSummaryServed = SUMMARY_LIST_KINDS.has(selectedCountKey)
202
213
  const selectedKindRowLimit = selectedKindSummaryServed ? SUMMARY_LIST_LIMIT : LARGE_RESOURCE_LIST_LIMIT
203
- const waitingForGuardCount = isSelectedKindGuarded && !countsData && !countsIsError
204
- const largeListBlocked = isSelectedKindGuarded && countsData != null && (selectedCountUnavailable || (selectedCountKnown && (selectedCount ?? 0) > selectedKindRowLimit))
214
+ // While still 'connecting', /resource-counts is unavailable — its error
215
+ // must NOT unlatch the large-list guard, or a huge Pods list could fetch
216
+ // unguarded on exactly the clusters the guard protects. Counts arrive
217
+ // right after 'connected' and settle the guard then.
218
+ const syncShellActive = connection.state === 'connecting'
219
+ // A kind whose sync deadline fired is unavailable-with-a-reason: hand it to
220
+ // the list endpoint, whose 503 kind_sync_failed renders the honest error —
221
+ // the guard would otherwise trap it on "count unavailable" forever. Safe to
222
+ // unblock: the server serves no rows for a failed kind.
223
+ const selectedCountFailedSync =
224
+ selectedCountUnavailable && countsData?.reasons?.[selectedCountKey] === 'kind_sync_failed'
225
+ // A guarded kind whose informer hasn't finished reports unavailable with a
226
+ // kind_sync_pending reason — "count not known YET", so keep the loading
227
+ // state. Keyed on the reason, not on connection.state: deferred kinds sync
228
+ // after connect. Reasonless unavailable stays a real verification failure
229
+ // and blocks the view as before.
230
+ const selectedCountPendingSync =
231
+ (selectedCountUnavailable && countsData?.reasons?.[selectedCountKey] === 'kind_sync_pending') ||
232
+ (syncShellActive && selectedCountUnavailable && !selectedCountFailedSync)
233
+ const waitingForGuardCount = isSelectedKindGuarded &&
234
+ ((!countsData && (!countsIsError || syncShellActive)) || selectedCountPendingSync)
235
+ const largeListBlocked = isSelectedKindGuarded && countsData != null && !selectedCountPendingSync && !selectedCountFailedSync &&
236
+ (selectedCountUnavailable || (selectedCountKnown && (selectedCount ?? 0) > selectedKindRowLimit))
205
237
  const selectedKindQueryBlocked = waitingForGuardCount || largeListBlocked
206
238
  const podCount = countsData?.counts.Pod
207
239
  const podCountKnown = hasResourceCount(countsData?.counts, 'Pod')
@@ -285,9 +317,15 @@ export function ResourcesView({ namespaces, selectedResource, onResourceClick, o
285
317
  staleTime: 30000,
286
318
  refetchInterval: 120000, // Safety net — SSE k8s_event drives near-real-time invalidation
287
319
  retry: (failureCount: number, error: Error) => {
288
- if (isForbiddenError(error)) return false
320
+ if (isForbiddenError(error) || isKindSyncFailed(error)) return false
321
+ // Cluster still connecting, or this kind's informer still completing
322
+ // its initial sync: keep the query in its loading state and retry —
323
+ // the header's sync-progress label explains the wait.
324
+ if (isStillLoadingError(error)) return true
289
325
  return failureCount < 3
290
326
  },
327
+ retryDelay: (failureCount: number, error: Error) =>
328
+ isStillLoadingError(error) ? 2000 : Math.min(1000 * 2 ** failureCount, 30000),
291
329
  })
292
330
 
293
331
  // Map to ResourceQueryResult shape
@@ -398,7 +436,7 @@ export function ResourcesView({ namespaces, selectedResource, onResourceClick, o
398
436
  resourceUnavailable={countsData?.unavailable}
399
437
  selectedKindQuery={selectedKindQueryResult}
400
438
  printerTable={selectedKindQueryBlocked ? null : selectedKindQuery.data?.printerTable ?? null}
401
- connectionState={connection.state}
439
+ connectionState={connection.state === 'connecting' && connection.syncStatus ? 'syncing' : connection.state}
402
440
  largeListGuard={largeListGuard}
403
441
  onSelectedKindChange={setSelectedKind}
404
442
  topPodMetrics={topPodMetrics}
@@ -1,7 +1,7 @@
1
1
  import { NamespaceRenderer as BaseNamespaceRenderer } from '@skyhook-io/k8s-ui/components/resources/renderers/NamespaceRenderer'
2
2
  import type { ResourceRef } from '@skyhook-io/k8s-ui'
3
3
  import { useRBACNamespace } from '../../../api/rbac'
4
- import { useNamespaceQuotas } from '../../../api/quotas'
4
+ import { useNamespaceLimitRanges, useNamespaceQuotas } from '../../../api/quotas'
5
5
  import { isForbiddenError } from '../../../api/client'
6
6
 
7
7
  interface NamespaceRendererProps {
@@ -17,6 +17,9 @@ export function NamespaceRenderer({ data, onNavigate }: NamespaceRendererProps)
17
17
  // RBAC sections). Surface other errors (500/503) so a quota-constrained
18
18
  // namespace doesn't silently render as quota-free.
19
19
  const quotaErr = quotaError && !isForbiddenError(quotaError) ? (quotaError as Error) : null
20
+ // LimitRanges keep their 403: the section states the denial rather than
21
+ // hiding, because "no rules apply here" is the wrong thing to imply.
22
+ const { data: limitRangeData, isLoading: limitRangeLoading, error: limitRangeError } = useNamespaceLimitRanges(name, !!name)
20
23
  return (
21
24
  <BaseNamespaceRenderer
22
25
  data={data}
@@ -25,6 +28,9 @@ export function NamespaceRenderer({ data, onNavigate }: NamespaceRendererProps)
25
28
  rbacError={error as Error | null}
26
29
  quotaData={quotaData}
27
30
  quotaError={quotaErr}
31
+ limitRangeData={limitRangeData}
32
+ limitRangeLoading={limitRangeLoading}
33
+ limitRangeError={limitRangeError}
28
34
  onNavigate={onNavigate}
29
35
  />
30
36
  )
@@ -7,6 +7,7 @@ import { useCapabilitiesContext, useNamespacedCapabilities, useIsLocalDeployment
7
7
  import { getVisibleLiveMetrics, isLiveMetricsUnavailable, shouldFetchLiveMetrics, usePodEnvironment, usePodMetrics, usePodMetricsHistory, usePrometheusResourceMetrics, usePrometheusStatus, useRevealPodEnvironment } from '../../../api/client'
8
8
  import { useRBACSubject } from '../../../api/rbac'
9
9
  import { usePolicyResource } from '../../../api/policy'
10
+ import { podLimitRangeNames, useNamespaceLimitRanges } from '../../../api/quotas'
10
11
  import { podAwaitsScheduling } from '../../capacity/podDemandGate'
11
12
  import { PortForwardInlineButton } from '../../portforward/PortForwardButton'
12
13
  import { ImageFilesystemModal } from '../ImageFilesystemModal'
@@ -79,6 +80,8 @@ export function PodRenderer({ data, onCopy, copied, onNavigate, onOpenLogs, reso
79
80
  'pods', namespace ?? '', podName, !!namespace && !!podName,
80
81
  )
81
82
 
83
+ const { data: limitRanges, isSuccess: limitRangesReady } = useNamespaceLimitRanges(namespace ?? '', !!namespace)
84
+
82
85
  return (
83
86
  <BasePodRenderer
84
87
  data={data}
@@ -110,6 +113,7 @@ export function PodRenderer({ data, onCopy, copied, onNavigate, onOpenLogs, reso
110
113
  policyData={policyData ?? null}
111
114
  policyLoading={policyLoading}
112
115
  policyError={policyError as Error | null}
116
+ namespaceLimitRangeNames={limitRangesReady ? podLimitRangeNames(limitRanges) : undefined}
113
117
  canExec={canExec}
114
118
  canViewLogs={canViewLogs}
115
119
  canPortForward={showPortForward}
@@ -1,5 +1,12 @@
1
1
  import { RolloutRenderer as BaseRolloutRenderer } from '@skyhook-io/k8s-ui/components/resources/renderers/RolloutRenderer'
2
- import { useRolloutAction, useRolloutCapabilities, type RolloutAction } from '../../../api/client'
2
+ import {
3
+ useRolloutAction,
4
+ useRolloutAnalysisRuns,
5
+ useRolloutCapabilities,
6
+ useWorkloadRevisions,
7
+ useWorkloadPods,
8
+ type RolloutAction,
9
+ } from '../../../api/client'
3
10
 
4
11
  interface RolloutRendererProps {
5
12
  data: any
@@ -11,6 +18,9 @@ export function RolloutRenderer({ data, onNavigate }: RolloutRendererProps) {
11
18
  const name = data?.metadata?.name ?? ''
12
19
  const { data: capabilities } = useRolloutCapabilities(namespace, name)
13
20
  const action = useRolloutAction()
21
+ const { data: analysisRunsResponse, error: analysisRunsError } = useRolloutAnalysisRuns(namespace, name)
22
+ const { data: revisions } = useWorkloadRevisions('rollouts', namespace, name)
23
+ const { data: podsResponse } = useWorkloadPods('rollouts', namespace, name)
14
24
 
15
25
  return (
16
26
  <BaseRolloutRenderer
@@ -19,6 +29,10 @@ export function RolloutRenderer({ data, onNavigate }: RolloutRendererProps) {
19
29
  capabilities={capabilities}
20
30
  onAction={(next: RolloutAction) => action.mutate({ action: next, namespace, name })}
21
31
  pendingAction={action.isPending ? action.variables?.action ?? null : null}
32
+ analysisRunHistory={analysisRunsResponse?.items}
33
+ analysisRunHistoryError={analysisRunsError}
34
+ revisions={revisions}
35
+ pods={podsResponse?.pods}
22
36
  />
23
37
  )
24
38
  }
@@ -5,6 +5,7 @@ import { CurlButton, CurlPanel, isHttpishPort, defaultScheme, defaultPathForPort
5
5
  import { useResources } from '../../../api/client'
6
6
  import { useNamespacedCapabilities, useIsLocalDeployment } from '../../../contexts/CapabilitiesContext'
7
7
  import type { ResourceRef } from '../../../types'
8
+ import { DURATION_DISCLOSURE } from '@skyhook-io/k8s-ui/utils/animation'
8
9
 
9
10
  interface ServiceRendererProps {
10
11
  data: any
@@ -32,7 +33,7 @@ export function ServiceRenderer({ data, onCopy, copied, onNavigate }: ServiceRen
32
33
  setCurl((p) => (p ? { ...p, closing: true } : null))
33
34
  // Only drop the panel if it's still the one closing. Opening another port
34
35
  // (which sets closing:false) before this fires must not clear the new panel.
35
- window.setTimeout(() => setCurl((p) => (p?.closing ? null : p)), 220)
36
+ window.setTimeout(() => setCurl((p) => (p?.closing ? null : p)), DURATION_DISCLOSURE + 20)
36
37
  }, [])
37
38
  const spec = data.spec || {}
38
39
  const shouldLoadEndpointSlices = Boolean(
@@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom'
3
3
  import { useScaleWorkload, fetchJSON } from '../../../api/client'
4
4
  import { useRBACSubject } from '../../../api/rbac'
5
5
  import { usePolicyResource } from '../../../api/policy'
6
+ import { podLimitRangeNames, useNamespaceLimitRanges } from '../../../api/quotas'
6
7
  import { useQueries, useQueryClient } from '@tanstack/react-query'
7
8
  import { kindToPlural, kindToPluralWithGroup } from '@skyhook-io/k8s-ui/utils/navigation'
8
9
  import type { Relationships, ResourceRef, ResourceWithRelationships, WorkloadPodInfo } from '../../../types'
@@ -49,6 +50,7 @@ export function WorkloadRenderer({ kind, data, onNavigate, scaleBlockedBy, workl
49
50
  const { data: policyData, isLoading: policyLoading, error: policyError } = usePolicyResource(
50
51
  kindToPlural(kind), namespace, metadata.name || '', !!namespace && !!metadata.name,
51
52
  )
53
+ const { data: limitRanges, isSuccess: limitRangesReady } = useNamespaceLimitRanges(namespace, !!namespace)
52
54
  const hpaRefs = (scaleBlockedBy ?? []).filter(ref => {
53
55
  const refKind = ref.kind.toLowerCase()
54
56
  return refKind === 'horizontalpodautoscaler' || refKind === 'hpa'
@@ -96,6 +98,7 @@ export function WorkloadRenderer({ kind, data, onNavigate, scaleBlockedBy, workl
96
98
  policyData={policyData ?? null}
97
99
  policyLoading={policyLoading}
98
100
  policyError={policyError as Error | null}
101
+ namespaceLimitRangeNames={limitRangesReady ? podLimitRangeNames(limitRanges) : undefined}
99
102
  scaleBlockedBy={scaleBlockedBy}
100
103
  workloadPods={workloadPods}
101
104
  scalerDiagnostics={scalerDiagnostics}
@@ -799,6 +799,27 @@ function FitWhy({ label, row }: { label: string; row?: RightsizingRow }) {
799
799
  }
800
800
 
801
801
  function EvidenceNote({ row }: { row: RightsizingRow }) {
802
+ const primary = primaryEvidenceNote(row)
803
+ // A denied pod read qualifies whichever reason applies rather than standing in
804
+ // for it: a row can both be unable to check its pods and have a limit conflict,
805
+ // and hiding either one behind the other loses something the reader needs.
806
+ const inventory =
807
+ row.liveInventoryUnavailable && row.resource === 'memory' ? (
808
+ <p className="mt-1 text-xs text-theme-text-tertiary">
809
+ Radar could not read this workload&apos;s pods, so it could not check them for
810
+ out-of-memory restarts.
811
+ </p>
812
+ ) : null
813
+ if (!primary && !inventory) return null
814
+ return (
815
+ <>
816
+ {primary}
817
+ {inventory}
818
+ </>
819
+ )
820
+ }
821
+
822
+ function primaryEvidenceNote(row: RightsizingRow) {
802
823
  if (row.hpaManaged)
803
824
  return (
804
825
  <p className="mt-1 text-xs text-theme-text-tertiary">
@@ -9,7 +9,7 @@ import {
9
9
  import { clsx } from 'clsx'
10
10
  import { useQueryClient } from '@tanstack/react-query'
11
11
  import { useAnimatedUnmount } from '../../hooks/useAnimatedUnmount'
12
- import { TRANSITION_BACKDROP, TRANSITION_PANEL } from '../../utils/animation'
12
+ import { TRANSITION_BACKDROP, TRANSITION_PANEL, overlayExitMs, overlayTransitionStyle } from '../../utils/animation'
13
13
  import { apiUrl, getAuthHeaders, getCredentialsMode, routePath } from '../../api/config'
14
14
  import {
15
15
  useCloudRole, useVersionCheck, useClusterInfo, usePrometheusStatus, useArgoStatus, useCapabilities,
@@ -135,7 +135,7 @@ export function SettingsDialog({
135
135
  }: SettingsDialogProps) {
136
136
  const queryClient = useQueryClient()
137
137
  const dialogRef = useRef<HTMLDivElement>(null)
138
- const { shouldRender, isOpen } = useAnimatedUnmount(open, 200)
138
+ const { shouldRender, isOpen } = useAnimatedUnmount(open, overlayExitMs('dialog'))
139
139
  const { data: versionInfo } = useVersionCheck()
140
140
  // Radar configuration (kubeconfig, port, integrations…) is host-level and
141
141
  // affects every user of this instance, so it's gated to owners. Personal
@@ -473,6 +473,7 @@ export function SettingsDialog({
473
473
  TRANSITION_BACKDROP,
474
474
  isOpen ? 'opacity-100' : 'opacity-0'
475
475
  )}
476
+ style={overlayTransitionStyle(isOpen, 'dialog')}
476
477
  onClick={() => requestCloseRef.current()}
477
478
  />
478
479
 
@@ -499,6 +500,7 @@ export function SettingsDialog({
499
500
  TRANSITION_PANEL,
500
501
  isOpen ? 'opacity-100 scale-100' : 'opacity-0 scale-95'
501
502
  )}
503
+ style={overlayTransitionStyle(isOpen, 'dialog')}
502
504
  >
503
505
  {/* Header — spans both panes */}
504
506
  <div className="flex items-center justify-between p-4 border-b border-theme-border shrink-0">
@@ -628,6 +630,7 @@ export function SettingsDialog({
628
630
  locked={!canEditConfig}
629
631
  >
630
632
  <PrometheusConfigField
633
+ local={deploymentMode === 'local'}
631
634
  value={editedConfig.prometheusUrl ?? ''}
632
635
  configuredHeaderKeys={configData?.prometheusHeaderKeys ?? []}
633
636
  onChange={(v) => updateConfigField('prometheusUrl', v || undefined)}
@@ -698,6 +701,9 @@ export function SettingsDialog({
698
701
  envManaged={configData?.argoCdEnvManaged ?? false}
699
702
  envError={configData?.argoCdEnvError}
700
703
  cliSession={configData?.argoCdCliSession}
704
+ anonymous={argoSectionStatus?.connected && argoSectionStatus.anonymous}
705
+ connectedAddress={argoSectionStatus?.connected && !argoSectionStatus.anonymous ? argoSectionStatus.address : undefined}
706
+ active={section === 'argocd'}
701
707
  statusReason={
702
708
  argoSectionStatus?.configured && !argoSectionStatus.connected
703
709
  ? argoSectionStatus.reason
@@ -716,6 +722,12 @@ export function SettingsDialog({
716
722
  : prev
717
723
  )
718
724
  void refetchArgoSectionStatus()
725
+ // The GitOps detail page the user came from doesn't poll
726
+ // when idle; without this the notice that sent them here
727
+ // would still be up when they get back.
728
+ void queryClient.invalidateQueries({
729
+ predicate: (query) => typeof query.queryKey[0] === 'string' && query.queryKey[0].startsWith('gitops-'),
730
+ })
719
731
  }}
720
732
  />
721
733
  </SectionPane>
@@ -1105,9 +1117,9 @@ function OverviewPanel({ active, onNavigate }: { active: boolean; onNavigate: (s
1105
1117
  },
1106
1118
  {
1107
1119
  id: 'prometheus', icon: Activity, label: 'Metrics',
1108
- tone: prom?.connected ? 'ok' : prom?.available ? 'warn' : 'off',
1109
- value: prom?.connected ? 'Connected' : prom?.available ? 'Not reachable' : 'Not configured',
1110
- detail: prom?.connected ? prom.address : undefined,
1120
+ tone: prom?.connected ? 'ok' : prom?.discovering ? 'unknown' : prom?.error ? 'warn' : 'off',
1121
+ value: prom?.connected ? 'Connected' : prom?.discovering ? 'Discovering…' : prom?.error ? 'Not connected' : 'Not configured',
1122
+ detail: prom?.connected ? prom.address : prom?.discovering ? undefined : prom?.error,
1111
1123
  },
1112
1124
  {
1113
1125
  id: costConfigurationAction(cost?.reason).section, icon: Coins, label: 'Cost',
@@ -1127,7 +1139,7 @@ function OverviewPanel({ active, onNavigate }: { active: boolean; onNavigate: (s
1127
1139
  // Configured-but-not-connected is often a permanently rejected/expired
1128
1140
  // token, not a transient reconnect — "Not reachable" matches Prometheus and
1129
1141
  // doesn't imply it will recover on its own.
1130
- value: argo?.connected ? 'Connected' : argo?.configured ? 'Not reachable' : 'Not connected',
1142
+ value: argo?.connected ? (argo.anonymous ? 'Connected · no token needed' : 'Connected') : argo?.configured ? 'Not reachable' : 'Not connected',
1131
1143
  detail: argo?.connected ? argo.address : argo?.reason,
1132
1144
  },
1133
1145
  {
@@ -1899,11 +1911,13 @@ type CurrencySaveState =
1899
1911
  type HeaderRow = { key: string; value: string }
1900
1912
 
1901
1913
  function PrometheusConfigField({
1914
+ local,
1902
1915
  value,
1903
1916
  onChange,
1904
1917
  configuredHeaderKeys,
1905
1918
  onApplied,
1906
1919
  }: {
1920
+ local: boolean
1907
1921
  value: string
1908
1922
  onChange: (value: string) => void
1909
1923
  configuredHeaderKeys: string[]
@@ -1981,14 +1995,14 @@ function PrometheusConfigField({
1981
1995
  return (
1982
1996
  <div>
1983
1997
  <p className="text-xs text-theme-text-tertiary mb-3">
1984
- Powers the CPU / memory graphs, usage history, and rightsizing hints on workload and node pages.
1998
+ Connect existing Prometheus-compatible data for resource usage, workload HTTP metrics and rightsizing. Available charts depend on collected metrics.
1985
1999
  </p>
1986
2000
  <label className="block text-sm font-medium text-theme-text-primary mb-1">
1987
- Server URL
2001
+ Metrics backend URL
1988
2002
  </label>
1989
2003
  <p className="text-xs text-theme-text-tertiary mb-1">
1990
- Manual PromQL-compatible query URL — works with Prometheus, VictoriaMetrics, Thanos, and
1991
- Mimir. Set this to skip auto-discovery.
2004
+ Base URL reachable from Radar, not your browser — Prometheus, VictoriaMetrics, Thanos or
2005
+ Mimir. Include any backend path prefix, but not /api/v1/query. Leave empty for cluster discovery; headers require a URL.
1992
2006
  </p>
1993
2007
  <div className="flex items-center gap-2">
1994
2008
  <Input
@@ -1997,7 +2011,7 @@ function PrometheusConfigField({
1997
2011
  placeholder="http://prometheus-server.monitoring:9090"
1998
2012
  className="flex-1 min-w-0 px-3 py-1.5 text-sm bg-theme-elevated border border-theme-border rounded-md text-theme-text-primary placeholder:text-theme-text-tertiary focus:outline-none focus:border-skyhook-500"
1999
2013
  />
2000
- <Tooltip content="Apply this URL to the running server now — no restart" wrapperClassName="shrink-0">
2014
+ <Tooltip content="Apply the connection now — no restart. Changing the URL or headers clears any workload scope override and resumes automatic identity matching." wrapperClassName="shrink-0">
2001
2015
  <button
2002
2016
  onClick={handleApply}
2003
2017
  disabled={apply.status === 'applying'}
@@ -2028,6 +2042,12 @@ function PrometheusConfigField({
2028
2042
  Applies immediately — no restart needed.
2029
2043
  </p>
2030
2044
  )}
2045
+ <p className="mt-2 text-xs text-theme-text-tertiary">
2046
+ {local
2047
+ ? 'Saved URL and headers apply across all local cluster contexts.'
2048
+ : 'Changes affect this Radar installation. Use deployment settings for configuration that survives Pod replacement.'}
2049
+ {' URL changes do not clear headers. Replace or clear credentials before switching backends.'}
2050
+ </p>
2031
2051
 
2032
2052
  {/* Auth headers — for token / multi-tenant backends (Bearer, X-Scope-OrgID). */}
2033
2053
  <div className="mt-3">
@@ -2095,7 +2115,8 @@ function PrometheusConfigField({
2095
2115
  <p className="text-xs text-theme-text-tertiary">
2096
2116
  Saved when you click Apply now. Entered headers replace all stored
2097
2117
  ones — values are hidden, so re-enter any you want to keep. Leave
2098
- blank to keep existing headers unchanged.
2118
+ all rows blank to keep existing headers unchanged. To clear all saved
2119
+ headers, remove every row and click Apply now.
2099
2120
  </p>
2100
2121
  </div>
2101
2122
  )}
@@ -2128,6 +2149,9 @@ function ArgoCDConfigField({
2128
2149
  envError,
2129
2150
  cliSession,
2130
2151
  statusReason,
2152
+ anonymous,
2153
+ connectedAddress,
2154
+ active,
2131
2155
  onChangeUrl,
2132
2156
  onChangeInsecureTls,
2133
2157
  onApplied,
@@ -2139,12 +2163,15 @@ function ArgoCDConfigField({
2139
2163
  envError?: string
2140
2164
  cliSession?: { server: string; user: string; insecure?: boolean }
2141
2165
  statusReason?: string
2166
+ anonymous?: boolean
2167
+ connectedAddress?: string
2168
+ active?: boolean
2142
2169
  onChangeUrl: (value: string) => void
2143
2170
  onChangeInsecureTls: (value: boolean) => void
2144
2171
  onApplied?: (v: { url: string; insecureTls: boolean; tokenSet: boolean }) => void
2145
2172
  }) {
2146
2173
  if (envManaged) {
2147
- return <ArgoCDEnvManagedField url={url} insecureTls={insecureTls} envError={envError} />
2174
+ return <ArgoCDEnvManagedField url={url} insecureTls={insecureTls} envError={envError} connectedAddress={connectedAddress} statusReason={statusReason} />
2148
2175
  }
2149
2176
  return (
2150
2177
  <ArgoCDEditableField
@@ -2153,6 +2180,9 @@ function ArgoCDConfigField({
2153
2180
  tokenSet={tokenSet}
2154
2181
  cliSession={cliSession}
2155
2182
  statusReason={statusReason}
2183
+ anonymous={anonymous}
2184
+ connectedAddress={connectedAddress}
2185
+ active={active}
2156
2186
  onChangeUrl={onChangeUrl}
2157
2187
  onChangeInsecureTls={onChangeInsecureTls}
2158
2188
  onApplied={onApplied}
@@ -2168,10 +2198,17 @@ function ArgoCDEnvManagedField({
2168
2198
  url,
2169
2199
  insecureTls,
2170
2200
  envError,
2201
+ connectedAddress,
2202
+ statusReason,
2171
2203
  }: {
2172
2204
  url: string
2173
2205
  insecureTls: boolean
2174
2206
  envError?: string
2207
+ // Live connection, from the status endpoint: the address when the
2208
+ // token-backed session is up, else why it isn't. The card is read-only,
2209
+ // so this is the one thing on it that can change after deploy.
2210
+ connectedAddress?: string
2211
+ statusReason?: string
2175
2212
  }) {
2176
2213
  if (envError) {
2177
2214
  return (
@@ -2224,6 +2261,21 @@ function ArgoCDEnvManagedField({
2224
2261
  <dt className="w-24 shrink-0 text-theme-text-tertiary">Token</dt>
2225
2262
  <dd className="text-theme-text-secondary">provided via environment</dd>
2226
2263
  </div>
2264
+ <div className="flex gap-2">
2265
+ <dt className="w-24 shrink-0 text-theme-text-tertiary">Status</dt>
2266
+ <dd className="min-w-0 truncate text-theme-text-secondary">
2267
+ {connectedAddress ? (
2268
+ <span className="inline-flex items-center gap-1">
2269
+ <Check className="w-3 h-3 shrink-0 text-green-600 dark:text-green-400/80" />
2270
+ Connected at {connectedAddress}
2271
+ </span>
2272
+ ) : statusReason ? (
2273
+ <span className="text-warning-text">Not reachable — {statusReason}</span>
2274
+ ) : (
2275
+ 'Not connected'
2276
+ )}
2277
+ </dd>
2278
+ </div>
2227
2279
  {insecureTls && (
2228
2280
  <div className="flex gap-2">
2229
2281
  <dt className="w-24 shrink-0 text-theme-text-tertiary">TLS</dt>
@@ -2246,6 +2298,9 @@ function ArgoCDEditableField({
2246
2298
  tokenSet,
2247
2299
  cliSession,
2248
2300
  statusReason,
2301
+ anonymous,
2302
+ connectedAddress,
2303
+ active,
2249
2304
  onChangeUrl,
2250
2305
  onChangeInsecureTls,
2251
2306
  onApplied,
@@ -2255,6 +2310,9 @@ function ArgoCDEditableField({
2255
2310
  tokenSet: boolean
2256
2311
  cliSession?: { server: string; user: string; insecure?: boolean }
2257
2312
  statusReason?: string
2313
+ anonymous?: boolean
2314
+ connectedAddress?: string
2315
+ active?: boolean
2258
2316
  onChangeUrl: (value: string) => void
2259
2317
  onChangeInsecureTls: (value: boolean) => void
2260
2318
  onApplied?: (v: { url: string; insecureTls: boolean; tokenSet: boolean }) => void
@@ -2348,20 +2406,43 @@ function ArgoCDEditableField({
2348
2406
 
2349
2407
  const showConfiguredPlaceholder = effectiveTokenSet && !tokenTouched && !tokenCleared
2350
2408
  const connecting = state.status === 'connecting'
2409
+ // Opening this section with nothing set up (typically from the GitOps
2410
+ // page's "connect Radar to your Argo CD server") starts the user at the
2411
+ // token — unless a CLI session offers a one-click connect, or reads
2412
+ // already work anonymously.
2413
+ const tokenInputRef = useRef<HTMLInputElement>(null)
2414
+ useEffect(() => {
2415
+ if (active && !effectiveTokenSet && !cliSession && !anonymous) tokenInputRef.current?.focus()
2416
+ }, [active, effectiveTokenSet, cliSession, anonymous])
2351
2417
 
2352
2418
  return (
2353
2419
  <div>
2354
2420
  <p className="text-xs text-theme-text-tertiary mb-3">
2355
2421
  Connect your Argo CD server for the full Git-rendered desired-vs-live diff on GitOps
2356
- Application pages — what Git declares vs what's actually running. Without it, Radar falls
2357
- back to a lighter annotation-based drift that can miss fields.
2422
+ Application pages — what Git declares vs what's actually running — and, on Argo CD 3, for
2423
+ Argo's own health verdict on each resource. Without it, Radar falls back to a lighter
2424
+ annotation-based drift that can miss fields, and to its own read of each resource.
2358
2425
  </p>
2359
2426
 
2427
+ {state.status !== 'connected' && (anonymous || connectedAddress) && (
2428
+ <p className="mb-3 flex items-center gap-1.5 text-xs text-theme-text-secondary">
2429
+ <Check className="w-3.5 h-3.5 shrink-0 text-green-600 dark:text-green-400/80" />
2430
+ {anonymous ? (
2431
+ <>
2432
+ Your Argo CD server lets Radar read without a token, so it's already connected. Add a token only
2433
+ if anonymous access gets restricted, or a URL to point Radar at a specific server.
2434
+ </>
2435
+ ) : (
2436
+ <>Connected to Argo CD at {connectedAddress}.</>
2437
+ )}
2438
+ </p>
2439
+ )}
2440
+
2360
2441
  {statusReason && state.status !== 'connected' && (
2361
2442
  <div className="mb-3 rounded-md border border-theme-border bg-theme-elevated p-3">
2362
2443
  <p className="flex items-center gap-1.5 text-sm font-medium text-warning-text">
2363
2444
  <AlertTriangle className="w-3.5 h-3.5 shrink-0" />
2364
- Argo CD token needs attention
2445
+ Argo CD connection needs attention
2365
2446
  </p>
2366
2447
  <p className="mt-1 text-xs text-theme-text-secondary">{statusReason}</p>
2367
2448
  </div>
@@ -2419,6 +2500,7 @@ function ArgoCDEditableField({
2419
2500
  </p>
2420
2501
  <div className="flex items-center gap-2">
2421
2502
  <input
2503
+ ref={tokenInputRef}
2422
2504
  type="password"
2423
2505
  value={showConfiguredPlaceholder ? '' : token}
2424
2506
  onChange={(e) => { setToken(e.target.value); setTokenTouched(true); setTokenCleared(false); clearStatus() }}