@skyhook-io/radar-app 0.1.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 (251) hide show
  1. package/README.md +67 -0
  2. package/package.json +80 -0
  3. package/src/App.tsx +1538 -0
  4. package/src/RadarApp.tsx +145 -0
  5. package/src/api/apiResources.ts +28 -0
  6. package/src/api/client.ts +2583 -0
  7. package/src/api/config.ts +116 -0
  8. package/src/api/traffic.ts +139 -0
  9. package/src/components/ConnectionErrorView.tsx +272 -0
  10. package/src/components/ContextSwitcher.tsx +481 -0
  11. package/src/components/DebugOverlay.tsx +94 -0
  12. package/src/components/UserMenu.tsx +87 -0
  13. package/src/components/audit/AuditSettingsDialog.tsx +162 -0
  14. package/src/components/audit/AuditView.tsx +123 -0
  15. package/src/components/cost/CostTrendChart.tsx +388 -0
  16. package/src/components/cost/CostView.tsx +545 -0
  17. package/src/components/dock/BottomDock.tsx +96 -0
  18. package/src/components/dock/DockContext.tsx +11 -0
  19. package/src/components/dock/LocalTerminalTab.tsx +22 -0
  20. package/src/components/dock/LogsTab.tsx +26 -0
  21. package/src/components/dock/NodeTerminalTab.tsx +50 -0
  22. package/src/components/dock/TerminalTab.tsx +42 -0
  23. package/src/components/dock/TrafficFlowListTab.tsx +18 -0
  24. package/src/components/dock/WorkloadLogsTab.tsx +23 -0
  25. package/src/components/dock/index.ts +2 -0
  26. package/src/components/gitops/GitOpsActions.tsx +1 -0
  27. package/src/components/gitops/GitOpsStatusBadge.tsx +1 -0
  28. package/src/components/gitops/ManagedResourcesList.tsx +1 -0
  29. package/src/components/gitops/SyncCountdown.tsx +1 -0
  30. package/src/components/gitops/index.ts +4 -0
  31. package/src/components/helm/ChartBrowser.tsx +580 -0
  32. package/src/components/helm/HelmReleaseDrawer.tsx +774 -0
  33. package/src/components/helm/HelmView.tsx +475 -0
  34. package/src/components/helm/InstallWizard.tsx +1060 -0
  35. package/src/components/helm/ManifestDiffViewer.tsx +91 -0
  36. package/src/components/helm/ManifestViewer.tsx +61 -0
  37. package/src/components/helm/OwnedResources.tsx +465 -0
  38. package/src/components/helm/RevisionHistory.tsx +167 -0
  39. package/src/components/helm/ValuesDiffPreview.tsx +190 -0
  40. package/src/components/helm/ValuesViewer.tsx +365 -0
  41. package/src/components/helm/helm-utils.ts +37 -0
  42. package/src/components/home/ActivitySummary.tsx +262 -0
  43. package/src/components/home/CertificateHealthCard.tsx +105 -0
  44. package/src/components/home/ClusterHealthCard.tsx +483 -0
  45. package/src/components/home/CostCard.tsx +112 -0
  46. package/src/components/home/HealthRing.tsx +1 -0
  47. package/src/components/home/HelmSummary.tsx +129 -0
  48. package/src/components/home/HomeView.tsx +224 -0
  49. package/src/components/home/MCPSetupDialog.tsx +417 -0
  50. package/src/components/home/NetworkPolicyCoverageCard.tsx +109 -0
  51. package/src/components/home/TopologyPreview.tsx +219 -0
  52. package/src/components/home/TrafficSummary.tsx +154 -0
  53. package/src/components/logs/JsonLogLine.tsx +1 -0
  54. package/src/components/logs/LogCore.tsx +2 -0
  55. package/src/components/logs/LogsViewer.tsx +44 -0
  56. package/src/components/logs/WorkloadLogsViewer.tsx +40 -0
  57. package/src/components/logs/useLogBuffer.ts +2 -0
  58. package/src/components/logs/useLogSearch.ts +1 -0
  59. package/src/components/portforward/PortForwardButton.tsx +375 -0
  60. package/src/components/portforward/PortForwardManager.tsx +871 -0
  61. package/src/components/resource/PrometheusCharts.tsx +687 -0
  62. package/src/components/resource-drawer/ResourceDrawer.tsx +214 -0
  63. package/src/components/resources/ImageFilesystemModal.tsx +745 -0
  64. package/src/components/resources/PodFilesystemModal.tsx +407 -0
  65. package/src/components/resources/ResourceDetailDrawer.tsx +43 -0
  66. package/src/components/resources/ResourcesView.tsx +190 -0
  67. package/src/components/resources/drawer-components.tsx +1 -0
  68. package/src/components/resources/file-browser-utils.ts +35 -0
  69. package/src/components/resources/renderers/AlertRenderer.tsx +1 -0
  70. package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +17 -0
  71. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +1 -0
  72. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +1 -0
  73. package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +1 -0
  74. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +1 -0
  75. package/src/components/resources/renderers/CertificateRenderer.tsx +1 -0
  76. package/src/components/resources/renderers/CertificateRequestRenderer.tsx +1 -0
  77. package/src/components/resources/renderers/ChallengeRenderer.tsx +1 -0
  78. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +1 -0
  79. package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +1 -0
  80. package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +1 -0
  81. package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +1 -0
  82. package/src/components/resources/renderers/ConfigMapRenderer.tsx +1 -0
  83. package/src/components/resources/renderers/CronJobRenderer.tsx +1 -0
  84. package/src/components/resources/renderers/EventRenderer.tsx +1 -0
  85. package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +1 -0
  86. package/src/components/resources/renderers/ExternalSecretRenderer.tsx +1 -0
  87. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +1 -0
  88. package/src/components/resources/renderers/GRPCRouteRenderer.tsx +1 -0
  89. package/src/components/resources/renderers/GatewayClassRenderer.tsx +1 -0
  90. package/src/components/resources/renderers/GatewayRenderer.tsx +1 -0
  91. package/src/components/resources/renderers/GenericRenderer.tsx +1 -0
  92. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +1 -0
  93. package/src/components/resources/renderers/HPARenderer.tsx +1 -0
  94. package/src/components/resources/renderers/HTTPRouteRenderer.tsx +1 -0
  95. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +1 -0
  96. package/src/components/resources/renderers/IngressClassRenderer.tsx +1 -0
  97. package/src/components/resources/renderers/IngressRenderer.tsx +1 -0
  98. package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +1 -0
  99. package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +1 -0
  100. package/src/components/resources/renderers/IstioGatewayRenderer.tsx +1 -0
  101. package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +1 -0
  102. package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +1 -0
  103. package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +1 -0
  104. package/src/components/resources/renderers/JobRenderer.tsx +1 -0
  105. package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +1 -0
  106. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +1 -0
  107. package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +1 -0
  108. package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +1 -0
  109. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +1 -0
  110. package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +1 -0
  111. package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +1 -0
  112. package/src/components/resources/renderers/KnativeEventingRenderer.tsx +1 -0
  113. package/src/components/resources/renderers/KnativeFlowRenderer.tsx +1 -0
  114. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +1 -0
  115. package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +1 -0
  116. package/src/components/resources/renderers/KnativeRouteRenderer.tsx +1 -0
  117. package/src/components/resources/renderers/KnativeServiceRenderer.tsx +1 -0
  118. package/src/components/resources/renderers/KnativeSourceRenderer.tsx +1 -0
  119. package/src/components/resources/renderers/KustomizationRenderer.tsx +1 -0
  120. package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +1 -0
  121. package/src/components/resources/renderers/LeaseRenderer.tsx +1 -0
  122. package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +1 -0
  123. package/src/components/resources/renderers/NodeRenderer.tsx +44 -0
  124. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +1 -0
  125. package/src/components/resources/renderers/OrderRenderer.tsx +1 -0
  126. package/src/components/resources/renderers/PVCRenderer.tsx +1 -0
  127. package/src/components/resources/renderers/PersistentVolumeRenderer.tsx +1 -0
  128. package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +1 -0
  129. package/src/components/resources/renderers/PodMonitorRenderer.tsx +1 -0
  130. package/src/components/resources/renderers/PodRenderer.tsx +94 -0
  131. package/src/components/resources/renderers/PriorityClassRenderer.tsx +1 -0
  132. package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +1 -0
  133. package/src/components/resources/renderers/ReplicaSetRenderer.tsx +1 -0
  134. package/src/components/resources/renderers/RoleBindingRenderer.tsx +1 -0
  135. package/src/components/resources/renderers/RoleRenderer.tsx +1 -0
  136. package/src/components/resources/renderers/RolloutRenderer.tsx +1 -0
  137. package/src/components/resources/renderers/RuntimeClassRenderer.tsx +1 -0
  138. package/src/components/resources/renderers/SbomReportRenderer.tsx +1 -0
  139. package/src/components/resources/renderers/SealedSecretRenderer.tsx +1 -0
  140. package/src/components/resources/renderers/SecretRenderer.tsx +1 -0
  141. package/src/components/resources/renderers/SecretStoreRenderer.tsx +1 -0
  142. package/src/components/resources/renderers/ServiceAccountRenderer.tsx +1 -0
  143. package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +1 -0
  144. package/src/components/resources/renderers/ServiceRenderer.tsx +26 -0
  145. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +1 -0
  146. package/src/components/resources/renderers/StorageClassRenderer.tsx +1 -0
  147. package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +1 -0
  148. package/src/components/resources/renderers/VPARenderer.tsx +1 -0
  149. package/src/components/resources/renderers/VeleroBSLRenderer.tsx +1 -0
  150. package/src/components/resources/renderers/VeleroBackupRenderer.tsx +1 -0
  151. package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +1 -0
  152. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +1 -0
  153. package/src/components/resources/renderers/VeleroVSLRenderer.tsx +1 -0
  154. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +1 -0
  155. package/src/components/resources/renderers/WebhookConfigRenderer.tsx +1 -0
  156. package/src/components/resources/renderers/WorkflowRenderer.tsx +1 -0
  157. package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +1 -0
  158. package/src/components/resources/renderers/WorkloadRenderer.tsx +52 -0
  159. package/src/components/resources/renderers/argo-cells.tsx +1 -0
  160. package/src/components/resources/renderers/certmanager-cells.tsx +1 -0
  161. package/src/components/resources/renderers/cnpg-cells.tsx +1 -0
  162. package/src/components/resources/renderers/eso-cells.tsx +1 -0
  163. package/src/components/resources/renderers/flux-cells.tsx +1 -0
  164. package/src/components/resources/renderers/index.ts +91 -0
  165. package/src/components/resources/renderers/istio-cells.tsx +1 -0
  166. package/src/components/resources/renderers/karpenter-cells.tsx +1 -0
  167. package/src/components/resources/renderers/keda-cells.tsx +1 -0
  168. package/src/components/resources/renderers/knative-cells.tsx +1 -0
  169. package/src/components/resources/renderers/kyverno-cells.tsx +1 -0
  170. package/src/components/resources/renderers/prometheus-cells.tsx +1 -0
  171. package/src/components/resources/renderers/traefik-cells.tsx +1 -0
  172. package/src/components/resources/renderers/trivy-cells.tsx +1 -0
  173. package/src/components/resources/renderers/trivy-shared.tsx +1 -0
  174. package/src/components/resources/renderers/velero-cells.tsx +1 -0
  175. package/src/components/resources/resource-utils-argo.ts +2 -0
  176. package/src/components/resources/resource-utils-certmanager.ts +2 -0
  177. package/src/components/resources/resource-utils-cnpg.ts +2 -0
  178. package/src/components/resources/resource-utils-eso.ts +2 -0
  179. package/src/components/resources/resource-utils-flux.ts +2 -0
  180. package/src/components/resources/resource-utils-istio.ts +2 -0
  181. package/src/components/resources/resource-utils-karpenter.ts +2 -0
  182. package/src/components/resources/resource-utils-keda.ts +2 -0
  183. package/src/components/resources/resource-utils-knative.ts +2 -0
  184. package/src/components/resources/resource-utils-kyverno.ts +2 -0
  185. package/src/components/resources/resource-utils-prometheus.ts +2 -0
  186. package/src/components/resources/resource-utils-traefik.ts +1 -0
  187. package/src/components/resources/resource-utils-trivy.ts +2 -0
  188. package/src/components/resources/resource-utils-velero.ts +2 -0
  189. package/src/components/resources/resource-utils.ts +5 -0
  190. package/src/components/settings/SettingsDialog.tsx +537 -0
  191. package/src/components/shared/CreateResourceDialog.tsx +17 -0
  192. package/src/components/shared/EditableYamlView.tsx +24 -0
  193. package/src/components/shared/LargeClusterNamespacePicker.tsx +70 -0
  194. package/src/components/shared/ResourceRendererDispatch.tsx +31 -0
  195. package/src/components/timeline/DiffViewer.tsx +1 -0
  196. package/src/components/timeline/TimelineList.tsx +69 -0
  197. package/src/components/timeline/TimelineSwimlanes.tsx +1308 -0
  198. package/src/components/timeline/TimelineView.tsx +157 -0
  199. package/src/components/timeline/shared.tsx +1 -0
  200. package/src/components/traffic/TrafficFilterSidebar.tsx +571 -0
  201. package/src/components/traffic/TrafficFlowList.tsx +415 -0
  202. package/src/components/traffic/TrafficFlowListContext.tsx +68 -0
  203. package/src/components/traffic/TrafficGraph.tsx +1546 -0
  204. package/src/components/traffic/TrafficView.tsx +1213 -0
  205. package/src/components/traffic/TrafficWizard.tsx +386 -0
  206. package/src/components/traffic/index.ts +3 -0
  207. package/src/components/ui/CodeViewer.tsx +8 -0
  208. package/src/components/ui/CommandPalette.tsx +460 -0
  209. package/src/components/ui/ConfirmDialog.tsx +1 -0
  210. package/src/components/ui/DiagnosticsOverlay.tsx +619 -0
  211. package/src/components/ui/ErrorBoundary.tsx +46 -0
  212. package/src/components/ui/ForceDeleteConfirmDialog.tsx +1 -0
  213. package/src/components/ui/Markdown.tsx +108 -0
  214. package/src/components/ui/MetricsChart.tsx +1 -0
  215. package/src/components/ui/NamespaceSelector.tsx +436 -0
  216. package/src/components/ui/ResourceBar.tsx +1 -0
  217. package/src/components/ui/ShortcutHelpOverlay.tsx +301 -0
  218. package/src/components/ui/Toast.tsx +1 -0
  219. package/src/components/ui/Tooltip.tsx +1 -0
  220. package/src/components/ui/UpdateNotification.tsx +299 -0
  221. package/src/components/ui/YamlEditor.tsx +1 -0
  222. package/src/components/workload/WorkloadView.tsx +532 -0
  223. package/src/context/ConnectionContext.tsx +173 -0
  224. package/src/context/ContextSwitchContext.tsx +56 -0
  225. package/src/context/NavCustomization.tsx +62 -0
  226. package/src/context/ThemeContext.tsx +97 -0
  227. package/src/contexts/CapabilitiesContext.tsx +130 -0
  228. package/src/hooks/useAnimatedUnmount.ts +1 -0
  229. package/src/hooks/useDesktopDownload.ts +41 -0
  230. package/src/hooks/useEventSource.ts +262 -0
  231. package/src/hooks/useFavorites.ts +69 -0
  232. package/src/hooks/useKeyboardShortcuts.tsx +7 -0
  233. package/src/hooks/useRefreshAnimation.ts +1 -0
  234. package/src/index.css +243 -0
  235. package/src/index.ts +17 -0
  236. package/src/main.tsx +158 -0
  237. package/src/types/gitops.ts +2 -0
  238. package/src/types.ts +3 -0
  239. package/src/utils/animation.ts +2 -0
  240. package/src/utils/badge-colors.ts +2 -0
  241. package/src/utils/context-name.ts +2 -0
  242. package/src/utils/desktop-download.ts +66 -0
  243. package/src/utils/desktop-open-folder.ts +21 -0
  244. package/src/utils/format.ts +2 -0
  245. package/src/utils/log-format.ts +12 -0
  246. package/src/utils/navigation.ts +23 -0
  247. package/src/utils/resource-hierarchy.ts +2 -0
  248. package/src/utils/resource-icons.ts +2 -0
  249. package/src/utils/skeleton-yaml.ts +2 -0
  250. package/src/utils/traffic-colors.ts +54 -0
  251. package/src/vite-env.d.ts +1 -0
@@ -0,0 +1,532 @@
1
+ import { useMemo, useEffect, useCallback, useState } from 'react'
2
+ import { useQueries } from '@tanstack/react-query'
3
+ import { useNavigate, useLocation, useSearchParams } from 'react-router-dom'
4
+ import { clsx } from 'clsx'
5
+ import { Terminal } from 'lucide-react'
6
+ import {
7
+ WorkloadView as BaseWorkloadView,
8
+ type RendererOverrides,
9
+ } from '@skyhook-io/k8s-ui'
10
+ import type { SelectedResource, ResourceRef, ResolvedEnvFrom } from '../../types'
11
+ import type { NavigateToResource } from '../../utils/navigation'
12
+ import {
13
+ useChanges, useResourceWithRelationships, usePodLogs, useTopology, useUpdateResource,
14
+ useDeleteResource, useTriggerCronJob, useSuspendCronJob, useResumeCronJob,
15
+ useRestartWorkload, useWorkloadRevisions, useRollbackWorkload,
16
+ useFluxReconcile, useFluxSyncWithSource, useFluxSuspend, useFluxResume,
17
+ useArgoSync, useArgoRefresh, useArgoSuspend, useArgoResume,
18
+ useCordonNode, useUncordonNode, useDrainNode,
19
+ useCascadeDeletePreview,
20
+ fetchJSON,
21
+ } from '../../api/client'
22
+ import { PrometheusCharts, isPrometheusSupported } from '../resource/PrometheusCharts'
23
+ import { useResourceAudit } from '../../api/client'
24
+ import { AuditAlerts } from '@skyhook-io/k8s-ui'
25
+ import { WorkloadLogsViewer } from '../logs/WorkloadLogsViewer'
26
+ import { LogsViewer } from '../logs/LogsViewer'
27
+ import { useCanUpdateSecrets, useCanNodeWrite, useNamespacedCapabilities } from '../../contexts/CapabilitiesContext'
28
+ import { useOpenTerminal, useOpenLogs, useOpenWorkloadLogs, useOpenNodeTerminal } from '../dock'
29
+ import { PortForwardButton } from '../portforward/PortForwardButton'
30
+ import { useToast } from '../ui/Toast'
31
+ import { PodRenderer } from '../resources/renderers/PodRenderer'
32
+ import { NodeRenderer } from '../resources/renderers/NodeRenderer'
33
+ import { ServiceRenderer } from '../resources/renderers/ServiceRenderer'
34
+ import { WorkloadRenderer } from '../resources/renderers/WorkloadRenderer'
35
+ import { CreateResourceDialog } from '../shared/CreateResourceDialog'
36
+ import { cleanYamlForDuplicate } from '../../utils/skeleton-yaml'
37
+
38
+ type TabType = 'overview' | 'timeline' | 'logs' | 'metrics' | 'yaml'
39
+
40
+ // Stable reference — web renderer wrappers inject platform hooks internally
41
+ const rendererOverrides: RendererOverrides = {
42
+ PodRenderer, NodeRenderer, ServiceRenderer, WorkloadRenderer,
43
+ }
44
+
45
+ // ============================================================================
46
+ // ROUTE WRAPPER — parses kind/ns/name from URL
47
+ // ============================================================================
48
+
49
+ interface WorkloadViewRouteProps {
50
+ onNavigateToResource?: NavigateToResource
51
+ }
52
+
53
+ export function WorkloadViewRoute({ onNavigateToResource }: WorkloadViewRouteProps) {
54
+ const location = useLocation()
55
+ const navigate = useNavigate()
56
+
57
+ // Parse /workload/:kind/:ns/:name from pathname
58
+ const parts = location.pathname.replace(/^\//, '').split('/')
59
+ // parts[0] = 'workload', parts[1] = kind, parts[2] = ns, parts[3+] = name (may contain slashes)
60
+ const kind = parts[1] || ''
61
+ const namespace = parts[2] || ''
62
+ const name = parts.slice(3).join('/') || ''
63
+
64
+ if (!kind || !namespace || !name) {
65
+ return (
66
+ <div className="flex items-center justify-center h-full text-theme-text-tertiary">
67
+ Invalid workload URL
68
+ </div>
69
+ )
70
+ }
71
+
72
+ const handleBack = useCallback(() => {
73
+ if (window.history.length > 1) {
74
+ navigate(-1)
75
+ } else {
76
+ navigate('/')
77
+ }
78
+ }, [navigate])
79
+
80
+ const handleNavigate = useCallback((resource: SelectedResource) => {
81
+ // Navigate to another workload view
82
+ navigate(`/workload/${resource.kind}/${resource.namespace}/${resource.name}`)
83
+ }, [navigate])
84
+
85
+ return (
86
+ <WorkloadView
87
+ kind={kind}
88
+ namespace={namespace}
89
+ name={name}
90
+ onBack={handleBack}
91
+ onNavigateToResource={onNavigateToResource || handleNavigate}
92
+ />
93
+ )
94
+ }
95
+
96
+ // ============================================================================
97
+ // WORKLOAD VIEW WRAPPER — injects data fetching hooks
98
+ // ============================================================================
99
+
100
+ interface WorkloadViewProps {
101
+ kind: string
102
+ namespace: string
103
+ name: string
104
+ onBack: () => void
105
+ onNavigateToResource?: NavigateToResource
106
+ onCollapseToDrawer?: () => void
107
+ expanded?: boolean
108
+ onClose?: () => void
109
+ onExpand?: () => void
110
+ initialTab?: 'detail' | 'yaml'
111
+ group?: string
112
+ }
113
+
114
+ function useActionsBarProps(kind: string, namespace: string, name: string) {
115
+ const { showCopied } = useToast()
116
+ const openTerminal = useOpenTerminal()
117
+ const openLogs = useOpenLogs()
118
+ const openWorkloadLogs = useOpenWorkloadLogs()
119
+ const openNodeTerminal = useOpenNodeTerminal()
120
+ const { canExec, canViewLogs, canPortForward } = useNamespacedCapabilities(namespace)
121
+
122
+ const deleteMutation = useDeleteResource()
123
+ const restartWorkloadMutation = useRestartWorkload()
124
+ const rollbackMutation = useRollbackWorkload()
125
+ const triggerCronJobMutation = useTriggerCronJob()
126
+ const suspendCronJobMutation = useSuspendCronJob()
127
+ const resumeCronJobMutation = useResumeCronJob()
128
+
129
+ const isRollbackKind = ['deployments', 'statefulsets', 'daemonsets'].includes(kind.toLowerCase())
130
+ const { data: revisionsList, isLoading: revisionsLoading, error: revisionsError } = useWorkloadRevisions(kind.toLowerCase(), namespace, name, isRollbackKind)
131
+
132
+ const fluxReconcileMutation = useFluxReconcile()
133
+ const fluxSyncWithSourceMutation = useFluxSyncWithSource()
134
+ const fluxSuspendMutation = useFluxSuspend()
135
+ const fluxResumeMutation = useFluxResume()
136
+
137
+ const argoSyncMutation = useArgoSync()
138
+ const argoRefreshMutation = useArgoRefresh()
139
+ const argoSuspendMutation = useArgoSuspend()
140
+ const argoResumeMutation = useArgoResume()
141
+
142
+ const { data: cascadePreview, isLoading: cascadeLoading } = useCascadeDeletePreview(kind, namespace, name, true)
143
+
144
+ const canNodeWrite = useCanNodeWrite()
145
+ const cordonMutation = useCordonNode()
146
+ const uncordonMutation = useUncordonNode()
147
+ const drainMutation = useDrainNode()
148
+
149
+ return {
150
+ canExec,
151
+ canViewLogs,
152
+ canPortForward,
153
+ onOpenTerminal: openTerminal,
154
+ onOpenLogs: openLogs,
155
+ onOpenWorkloadLogs: openWorkloadLogs,
156
+ onOpenNodeTerminal: openNodeTerminal,
157
+ onCopyCommand: (text: string, message: string, event: React.MouseEvent) => showCopied(text, message, event),
158
+ renderPortForward: ({ type, namespace: ns, name: n, className }: { type: 'pod' | 'service'; namespace: string; name: string; className?: string }) => (
159
+ <PortForwardButton type={type} namespace={ns} name={n} className={className} />
160
+ ),
161
+ onDelete: (params: any, callbacks?: any) => deleteMutation.mutate(params, { onSuccess: callbacks?.onSuccess }),
162
+ isDeleting: deleteMutation.isPending,
163
+ cascadeDependents: cascadePreview?.dependents,
164
+ cascadeLoading,
165
+ onRestart: (params: any) => restartWorkloadMutation.mutate(params),
166
+ isRestarting: restartWorkloadMutation.isPending,
167
+ revisions: revisionsList,
168
+ revisionsLoading,
169
+ revisionsError: revisionsError ?? null,
170
+ onRollback: (params: any, callbacks?: any) => rollbackMutation.mutate(params, { onSuccess: callbacks?.onSuccess }),
171
+ isRollingBack: rollbackMutation.isPending,
172
+ onTriggerCronJob: (params: any) => triggerCronJobMutation.mutate(params),
173
+ isTriggeringCronJob: triggerCronJobMutation.isPending,
174
+ onSuspendCronJob: (params: any) => suspendCronJobMutation.mutate(params),
175
+ isSuspendingCronJob: suspendCronJobMutation.isPending,
176
+ onResumeCronJob: (params: any) => resumeCronJobMutation.mutate(params),
177
+ isResumingCronJob: resumeCronJobMutation.isPending,
178
+ onFluxReconcile: (params: any) => fluxReconcileMutation.mutate(params),
179
+ isFluxReconciling: fluxReconcileMutation.isPending,
180
+ onFluxSyncWithSource: (params: any) => fluxSyncWithSourceMutation.mutate(params),
181
+ isFluxSyncing: fluxSyncWithSourceMutation.isPending,
182
+ onFluxSuspend: (params: any) => fluxSuspendMutation.mutate(params),
183
+ isFluxSuspending: fluxSuspendMutation.isPending,
184
+ onFluxResume: (params: any) => fluxResumeMutation.mutate(params),
185
+ isFluxResuming: fluxResumeMutation.isPending,
186
+ onArgoSync: (params: any) => argoSyncMutation.mutate(params),
187
+ isArgoSyncing: argoSyncMutation.isPending,
188
+ onArgoRefresh: (params: any) => argoRefreshMutation.mutate(params),
189
+ isArgoRefreshing: argoRefreshMutation.isPending,
190
+ onArgoSuspend: (params: any) => argoSuspendMutation.mutate(params),
191
+ isArgoSuspending: argoSuspendMutation.isPending,
192
+ onArgoResume: (params: any) => argoResumeMutation.mutate(params),
193
+ isArgoResuming: argoResumeMutation.isPending,
194
+ canNodeWrite,
195
+ onCordonNode: (params: any) => cordonMutation.mutate(params),
196
+ isCordoningNode: cordonMutation.isPending,
197
+ onUncordonNode: (params: any) => uncordonMutation.mutate(params),
198
+ isUncordoningNode: uncordonMutation.isPending,
199
+ onDrainNode: (params: any) => drainMutation.mutate(params),
200
+ isDrainingNode: drainMutation.isPending,
201
+ }
202
+ }
203
+
204
+ export function WorkloadView({
205
+ kind: kindProp,
206
+ namespace,
207
+ name,
208
+ expanded = true,
209
+ ...rest
210
+ }: WorkloadViewProps) {
211
+ const [searchParams, setSearchParams] = useSearchParams()
212
+
213
+ // Tab state from URL query param — migrate legacy tab names
214
+ const rawTab = searchParams.get('tab')
215
+ const migratedTab: TabType = rawTab === 'info' ? 'overview'
216
+ : rawTab === 'events' ? 'timeline'
217
+ : (rawTab as TabType) || 'overview'
218
+
219
+ const handleTabChange = useCallback((tab: TabType) => {
220
+ const params = new URLSearchParams(searchParams)
221
+ if (tab === 'overview') {
222
+ params.delete('tab')
223
+ } else {
224
+ params.set('tab', tab)
225
+ }
226
+ setSearchParams(params, { replace: true })
227
+ }, [searchParams, setSearchParams])
228
+
229
+ // Fetch resource with relationships
230
+ const { data: resourceResponse, isLoading: resourceLoading, refetch: refetchResource } = useResourceWithRelationships<any>(kindProp, namespace, name, rest.group)
231
+ const resource = resourceResponse?.resource
232
+ const relationships = resourceResponse?.relationships
233
+ const certificateInfo = resourceResponse?.certificateInfo
234
+
235
+ // For pods: extract envFrom ConfigMap/Secret names and resolve their keys
236
+ const isPod = kindProp.toLowerCase() === 'pods'
237
+ const { envFromConfigMapNames, envFromSecretNames } = useMemo(() => {
238
+ if (!isPod || !resource) return { envFromConfigMapNames: [] as string[], envFromSecretNames: [] as string[] }
239
+ const cmNames = new Set<string>()
240
+ const secretNames = new Set<string>()
241
+ const containers = [...(resource.spec?.containers || []), ...(resource.spec?.initContainers || [])]
242
+ for (const c of containers) {
243
+ for (const ef of (c.envFrom || [])) {
244
+ if (ef.configMapRef?.name) cmNames.add(ef.configMapRef.name)
245
+ if (ef.secretRef?.name) secretNames.add(ef.secretRef.name)
246
+ }
247
+ }
248
+ return { envFromConfigMapNames: Array.from(cmNames), envFromSecretNames: Array.from(secretNames) }
249
+ }, [isPod, resource])
250
+
251
+ const configMapQueries = useQueries({
252
+ queries: envFromConfigMapNames.map((cmName) => ({
253
+ queryKey: ['resources', 'configmaps', namespace, cmName],
254
+ queryFn: () => fetchJSON<any>(`/resources/configmaps/${namespace}/${cmName}`),
255
+ enabled: isPod,
256
+ staleTime: 30000,
257
+ })),
258
+ })
259
+
260
+ const secretQueries = useQueries({
261
+ queries: envFromSecretNames.map((secretName) => ({
262
+ queryKey: ['resources', 'secrets', namespace, secretName],
263
+ queryFn: () => fetchJSON<any>(`/resources/secrets/${namespace}/${secretName}`),
264
+ enabled: isPod,
265
+ staleTime: 30000,
266
+ })),
267
+ })
268
+
269
+ const resolvedEnvFrom = useMemo(() => {
270
+ if (!isPod || (envFromConfigMapNames.length === 0 && envFromSecretNames.length === 0)) return undefined
271
+ const result: ResolvedEnvFrom = {}
272
+ envFromConfigMapNames.forEach((n, i) => {
273
+ // Single-resource endpoint returns { resource, relationships } wrapper
274
+ const cm = configMapQueries[i]?.data?.resource ?? configMapQueries[i]?.data
275
+ if (cm) result[n] = { keys: Object.keys(cm.data || {}), values: cm.data || {}, isSecret: false }
276
+ })
277
+ envFromSecretNames.forEach((n, i) => {
278
+ const secret = secretQueries[i]?.data?.resource ?? secretQueries[i]?.data
279
+ if (secret) {
280
+ const decodedValues: Record<string, string> = {}
281
+ for (const [k, v] of Object.entries(secret.data || {})) {
282
+ try { decodedValues[k] = atob(v as string) } catch { decodedValues[k] = v as string }
283
+ }
284
+ result[n] = { keys: Object.keys(decodedValues), values: decodedValues, isSecret: true }
285
+ }
286
+ })
287
+ return Object.keys(result).length > 0 ? result : undefined
288
+ }, [isPod, envFromConfigMapNames, envFromSecretNames, configMapQueries, secretQueries])
289
+
290
+ // Fetch topology for hierarchy building (only when expanded)
291
+ const { data: topology } = useTopology([namespace], 'resources', { enabled: expanded })
292
+
293
+ // Fetch all events for this resource's namespace (only when expanded)
294
+ const { data: allEvents, isLoading: eventsLoading } = useChanges({
295
+ namespaces: [namespace],
296
+ timeRange: 'all',
297
+ includeK8sEvents: true,
298
+ includeManaged: true,
299
+ limit: 10000,
300
+ enabled: expanded,
301
+ })
302
+
303
+ // RBAC
304
+ const canUpdateSecrets = useCanUpdateSecrets()
305
+ const updateResource = useUpdateResource()
306
+ const actionsBarProps = useActionsBarProps(kindProp, namespace, name)
307
+
308
+ const handleUpdateResource = useCallback(async (params: { kind: string; namespace: string; name: string; yaml: string }) => {
309
+ await updateResource.mutateAsync(params)
310
+ }, [updateResource])
311
+
312
+ // Duplicate dialog
313
+ const [duplicateDialogOpen, setDuplicateDialogOpen] = useState(false)
314
+ const [duplicateYaml, setDuplicateYaml] = useState('')
315
+
316
+ const handleDuplicate = useCallback((params: { kind: string; namespace: string; name: string; yaml: string }) => {
317
+ setDuplicateYaml(cleanYamlForDuplicate(params.yaml))
318
+ setDuplicateDialogOpen(true)
319
+ }, [])
320
+
321
+ return (
322
+ <>
323
+ <BaseWorkloadView
324
+ kind={kindProp}
325
+ namespace={namespace}
326
+ name={name}
327
+ expanded={expanded}
328
+ {...rest}
329
+ // Data
330
+ resource={resource}
331
+ relationships={relationships}
332
+ certificateInfo={certificateInfo}
333
+ isLoading={resourceLoading}
334
+ refetch={refetchResource}
335
+ // Timeline
336
+ allEvents={allEvents}
337
+ eventsLoading={eventsLoading}
338
+ topology={topology}
339
+ // Capabilities
340
+ canUpdateSecrets={canUpdateSecrets}
341
+ // Mutations
342
+ onUpdateResource={handleUpdateResource}
343
+ isUpdatingResource={updateResource.isPending}
344
+ updateResourceError={updateResource.error?.message ?? null}
345
+ // Tab state (URL-synced)
346
+ activeTab={migratedTab}
347
+ onTabChange={handleTabChange}
348
+ // Render props
349
+ renderLogsTab={(props) => <LogsTabContent {...props} />}
350
+ renderMetricsTab={({ kind, namespace: ns, name: n }) => (
351
+ <PrometheusCharts kind={kind} namespace={ns} name={n} showEmptyState />
352
+ )}
353
+ isMetricsAvailable={(kind, res) =>
354
+ isPrometheusSupported(kind) && !(kind === 'Pod' && res?.status?.phase === 'Pending')
355
+ }
356
+ onDuplicate={handleDuplicate}
357
+ actionsBarProps={actionsBarProps}
358
+ rendererOverrides={rendererOverrides}
359
+ resolvedEnvFrom={resolvedEnvFrom}
360
+ renderOverviewExtra={({ kind: k, namespace: ns, name: n }) => (
361
+ <AuditSection kind={k} namespace={ns} name={n} />
362
+ )}
363
+ />
364
+ <CreateResourceDialog
365
+ open={duplicateDialogOpen}
366
+ onClose={() => setDuplicateDialogOpen(false)}
367
+ initialYaml={duplicateYaml}
368
+ title="Duplicate Resource"
369
+ onCreated={(result) => {
370
+ rest.onNavigateToResource?.({ kind: result.kind, namespace: result.namespace, name: result.name, group: '' })
371
+ }}
372
+ />
373
+ </>
374
+ )
375
+ }
376
+
377
+ // ============================================================================
378
+ // LOGS TAB — platform-specific (uses data-fetching hooks)
379
+ // ============================================================================
380
+
381
+ const WORKLOAD_LOG_KINDS = new Set(['Deployment', 'StatefulSet', 'DaemonSet'])
382
+
383
+ function LogsTabContent({
384
+ kind,
385
+ apiKind,
386
+ namespace,
387
+ name,
388
+ resource,
389
+ pods,
390
+ selectedPod,
391
+ onSelectPod,
392
+ initialContainer,
393
+ onConsumeInitialContainer,
394
+ }: {
395
+ kind: string
396
+ apiKind: string
397
+ namespace: string
398
+ name: string
399
+ resource: any
400
+ pods: ResourceRef[]
401
+ selectedPod: string | null
402
+ onSelectPod: (name: string | null) => void
403
+ initialContainer: string | null
404
+ onConsumeInitialContainer: () => void
405
+ }) {
406
+ // Workload kinds (Deployment, StatefulSet, DaemonSet) use the aggregated workload logs viewer
407
+ if (WORKLOAD_LOG_KINDS.has(kind)) {
408
+ return (
409
+ <div className="h-full">
410
+ <WorkloadLogsViewer kind={apiKind} namespace={namespace} name={name} />
411
+ </div>
412
+ )
413
+ }
414
+
415
+ // Individual Pod — use LogsViewer with container list from resource data
416
+ if (kind === 'Pod') {
417
+ return <PodLogsTab namespace={namespace} name={name} resource={resource} initialContainer={initialContainer} onConsumeInitialContainer={onConsumeInitialContainer} />
418
+ }
419
+
420
+ // Other kinds with associated pods (Jobs, CronJobs, ReplicaSets, etc.) — pod selector + LogsViewer
421
+ return (
422
+ <MultiPodLogsTab
423
+ pods={pods}
424
+ namespace={namespace}
425
+ selectedPod={selectedPod}
426
+ onSelectPod={onSelectPod}
427
+ initialContainer={initialContainer}
428
+ />
429
+ )
430
+ }
431
+
432
+ function PodLogsTab({ namespace, name, resource, initialContainer, onConsumeInitialContainer }: {
433
+ namespace: string
434
+ name: string
435
+ resource: any
436
+ initialContainer?: string | null
437
+ onConsumeInitialContainer?: () => void
438
+ }) {
439
+ const containers = useMemo(() => {
440
+ const names: string[] = []
441
+ for (const c of resource?.spec?.initContainers || []) if (c.name) names.push(c.name)
442
+ for (const c of resource?.spec?.containers || []) if (c.name) names.push(c.name)
443
+ return names
444
+ }, [resource])
445
+
446
+ useEffect(() => {
447
+ if (initialContainer && containers.includes(initialContainer)) {
448
+ onConsumeInitialContainer?.()
449
+ }
450
+ }, [initialContainer, containers, onConsumeInitialContainer])
451
+
452
+ return (
453
+ <div className="h-full">
454
+ <LogsViewer
455
+ namespace={namespace}
456
+ podName={name}
457
+ containers={containers}
458
+ initialContainer={initialContainer || undefined}
459
+ />
460
+ </div>
461
+ )
462
+ }
463
+
464
+ function MultiPodLogsTab({ pods, namespace, selectedPod, onSelectPod, initialContainer }: {
465
+ pods: ResourceRef[]
466
+ namespace: string
467
+ selectedPod: string | null
468
+ onSelectPod: (name: string | null) => void
469
+ initialContainer?: string | null
470
+ }) {
471
+ useEffect(() => {
472
+ if (pods.length > 0 && !selectedPod) {
473
+ onSelectPod(pods[0].name)
474
+ }
475
+ }, [pods, selectedPod, onSelectPod])
476
+
477
+ const podNamespace = pods.find(p => p.name === selectedPod)?.namespace || namespace
478
+
479
+ // Fetch container list for the selected pod
480
+ const { data: logsData } = usePodLogs(podNamespace, selectedPod || '', { tailLines: 1 })
481
+ const containers = logsData?.containers || []
482
+
483
+ if (pods.length === 0) {
484
+ return (
485
+ <div className="flex flex-col items-center justify-center h-full text-theme-text-tertiary">
486
+ <Terminal className="w-12 h-12 mb-4 opacity-50" />
487
+ <p>No pods available</p>
488
+ </div>
489
+ )
490
+ }
491
+
492
+ return (
493
+ <div className="h-full flex flex-col">
494
+ {pods.length > 1 && (
495
+ <div className="shrink-0 border-b border-theme-border bg-theme-surface/50 px-4 py-2 flex gap-2 overflow-x-auto">
496
+ {pods.map(pod => (
497
+ <button
498
+ key={pod.name}
499
+ onClick={() => onSelectPod(pod.name)}
500
+ className={clsx(
501
+ 'px-3 py-1.5 text-sm rounded-lg whitespace-nowrap transition-colors',
502
+ selectedPod === pod.name
503
+ ? 'bg-blue-500 text-theme-text-primary'
504
+ : 'bg-theme-elevated text-theme-text-secondary hover:bg-theme-hover'
505
+ )}
506
+ >
507
+ {pod.name.length > 40 ? '...' + pod.name.slice(-37) : pod.name}
508
+ </button>
509
+ ))}
510
+ </div>
511
+ )}
512
+ {selectedPod && containers.length > 0 && (
513
+ <div className="flex-1 min-h-0">
514
+ <LogsViewer
515
+ key={selectedPod}
516
+ namespace={podNamespace}
517
+ podName={selectedPod}
518
+ containers={containers}
519
+ initialContainer={initialContainer || undefined}
520
+ />
521
+ </div>
522
+ )}
523
+ </div>
524
+ )
525
+ }
526
+
527
+ function AuditSection({ kind, namespace, name }: { kind: string; namespace: string; name: string }) {
528
+ const navigate = useNavigate()
529
+ const { data: findings } = useResourceAudit(kind, namespace, name)
530
+ if (!findings || findings.length === 0) return null
531
+ return <AuditAlerts findings={findings} onViewAll={() => navigate('/audit')} />
532
+ }
@@ -0,0 +1,173 @@
1
+ import { createContext, useContext, useState, useCallback, useEffect, useRef, ReactNode } from 'react'
2
+ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
3
+ import type { ContextInfo } from '../types'
4
+ import { getApiBase, getAuthHeaders, getCredentialsMode } from '../api/config'
5
+
6
+ export type ConnectionStateType = 'connected' | 'disconnected' | 'connecting'
7
+
8
+ export interface ConnectionState {
9
+ state: ConnectionStateType
10
+ context: string
11
+ clusterName?: string
12
+ error?: string
13
+ errorType?: string // auth, network, timeout, unknown
14
+ progressMessage?: string
15
+ }
16
+
17
+ interface ConnectionStatusResponse extends ConnectionState {
18
+ contexts: ContextInfo[]
19
+ }
20
+
21
+ interface ConnectionContextValue {
22
+ connection: ConnectionState
23
+ contexts: ContextInfo[]
24
+ retry: () => void
25
+ isRetrying: boolean
26
+ updateFromSSE: (status: ConnectionState) => void
27
+ }
28
+
29
+ const ConnectionContext = createContext<ConnectionContextValue | null>(null)
30
+
31
+ async function fetchConnectionStatus(): Promise<ConnectionStatusResponse> {
32
+ const response = await fetch(`${getApiBase()}/connection`, {
33
+ credentials: getCredentialsMode(),
34
+ headers: getAuthHeaders(),
35
+ })
36
+ if (!response.ok) {
37
+ throw new Error('Failed to fetch connection status')
38
+ }
39
+ return response.json()
40
+ }
41
+
42
+ async function retryConnection(): Promise<ConnectionState> {
43
+ const response = await fetch(`${getApiBase()}/connection/retry`, {
44
+ method: 'POST',
45
+ credentials: getCredentialsMode(),
46
+ headers: getAuthHeaders(),
47
+ })
48
+ if (!response.ok) {
49
+ const error = await response.json().catch(() => ({ error: 'Unknown error' }))
50
+ throw new Error(error.error || `HTTP ${response.status}`)
51
+ }
52
+ return response.json()
53
+ }
54
+
55
+ export function ConnectionProvider({ children }: { children: ReactNode }) {
56
+ const queryClient = useQueryClient()
57
+ const [connection, setConnection] = useState<ConnectionState>({
58
+ state: 'connecting',
59
+ context: '',
60
+ })
61
+ const [contexts, setContexts] = useState<ContextInfo[]>([])
62
+ // Track if SSE has started delivering connection_state events
63
+ // Once SSE is active, it becomes the authoritative source for connection state
64
+ const sseActiveRef = useRef(false)
65
+
66
+ // Fetch initial connection status
67
+ // Poll while connecting to get progress updates (SSE not established yet)
68
+ const { data } = useQuery<ConnectionStatusResponse>({
69
+ queryKey: ['connection-status'],
70
+ queryFn: fetchConnectionStatus,
71
+ staleTime: 500, // Allow frequent refetches while connecting
72
+ refetchInterval: connection.state === 'connecting' ? 500 : false, // Poll every 500ms while connecting
73
+ refetchOnWindowFocus: false,
74
+ })
75
+
76
+ // Update state from query result
77
+ // Once SSE is active, only update contexts from poll (SSE handles connection state)
78
+ useEffect(() => {
79
+ if (data) {
80
+ // Always update contexts from poll data
81
+ setContexts(data.contexts || [])
82
+ // Only update connection state from poll if SSE hasn't taken over
83
+ if (!sseActiveRef.current) {
84
+ setConnection({
85
+ state: data.state,
86
+ context: data.context,
87
+ clusterName: data.clusterName,
88
+ error: data.error,
89
+ errorType: data.errorType,
90
+ progressMessage: data.progressMessage,
91
+ })
92
+ }
93
+ }
94
+ }, [data])
95
+
96
+ // Retry mutation
97
+ const retryMutation = useMutation({
98
+ mutationFn: retryConnection,
99
+ onMutate: () => {
100
+ // Reset SSE active flag - polling can provide state until SSE reconnects
101
+ sseActiveRef.current = false
102
+ // Set connecting state while retrying
103
+ setConnection(prev => ({
104
+ ...prev,
105
+ state: 'connecting',
106
+ error: undefined,
107
+ errorType: undefined,
108
+ progressMessage: 'Connecting to cluster...',
109
+ }))
110
+ },
111
+ onSuccess: (result) => {
112
+ setConnection(result)
113
+ // Clear all query cache to get fresh data from new connection
114
+ queryClient.removeQueries()
115
+ queryClient.invalidateQueries()
116
+ },
117
+ onError: (error: Error) => {
118
+ setConnection(prev => ({
119
+ ...prev,
120
+ state: 'disconnected',
121
+ error: error.message,
122
+ progressMessage: undefined,
123
+ }))
124
+ },
125
+ })
126
+
127
+ const retry = useCallback(() => {
128
+ retryMutation.mutate()
129
+ }, [retryMutation])
130
+
131
+ // Handler for SSE connection_state events
132
+ const updateFromSSE = useCallback((status: ConnectionState) => {
133
+ // Mark SSE as active - it's now the authoritative source for connection state
134
+ sseActiveRef.current = true
135
+ setConnection(prev => {
136
+ // Don't transition back to 'connecting' from 'connected'. This happens when the
137
+ // pod restarts and the SSE reconnects while the new pod's K8s cache is still
138
+ // syncing. Hiding the main content here causes a flash — keep the 'connected'
139
+ // state and wait for either 'connected' (sync done) or 'disconnected' (failure).
140
+ if (prev.state === 'connected' && status.state === 'connecting') {
141
+ return prev
142
+ }
143
+ return status
144
+ })
145
+
146
+ // If we just connected, invalidate queries to fetch fresh data
147
+ if (status.state === 'connected') {
148
+ queryClient.invalidateQueries()
149
+ }
150
+ }, [queryClient])
151
+
152
+ const value: ConnectionContextValue = {
153
+ connection,
154
+ contexts,
155
+ retry,
156
+ isRetrying: retryMutation.isPending,
157
+ updateFromSSE,
158
+ }
159
+
160
+ return (
161
+ <ConnectionContext.Provider value={value}>
162
+ {children}
163
+ </ConnectionContext.Provider>
164
+ )
165
+ }
166
+
167
+ export function useConnection() {
168
+ const context = useContext(ConnectionContext)
169
+ if (!context) {
170
+ throw new Error('useConnection must be used within ConnectionProvider')
171
+ }
172
+ return context
173
+ }