@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,619 @@
1
+ import { useEffect, useState, useCallback } from 'react'
2
+ import { X, Copy, Check, ExternalLink } from 'lucide-react'
3
+ import { clsx } from 'clsx'
4
+ import { TRANSITION_BACKDROP, TRANSITION_PANEL } from '../../utils/animation'
5
+ import { openExternal } from '../../utils/navigation'
6
+ import { useDiagnostics } from '../../api/client'
7
+ import type { DiagnosticsSnapshot, DiagMetricsSourceHealth, DiagDropRecord, DiagErrorEntry } from '../../api/client'
8
+
9
+ interface DiagnosticsOverlayProps {
10
+ onClose: () => void
11
+ isOpen?: boolean
12
+ }
13
+
14
+ export function DiagnosticsOverlay({ onClose, isOpen = true }: DiagnosticsOverlayProps) {
15
+ const { data, isLoading, error } = useDiagnostics(true)
16
+ const [copied, setCopied] = useState<'json' | 'formatted' | null>(null)
17
+ const [reportOpened, setReportOpened] = useState(false)
18
+
19
+ // Close on Escape (capture phase)
20
+ useEffect(() => {
21
+ const handler = (e: KeyboardEvent) => {
22
+ if (e.key === 'Escape') {
23
+ e.preventDefault()
24
+ e.stopPropagation()
25
+ onClose()
26
+ }
27
+ }
28
+ document.addEventListener('keydown', handler, true)
29
+ return () => document.removeEventListener('keydown', handler, true)
30
+ }, [onClose])
31
+
32
+ const copyToClipboard = useCallback(async (type: 'json' | 'formatted') => {
33
+ if (!data) return
34
+ const text = type === 'json'
35
+ ? JSON.stringify(data, null, 2)
36
+ : formatForGitHub(data)
37
+ try {
38
+ await navigator.clipboard.writeText(text)
39
+ setCopied(type)
40
+ setTimeout(() => setCopied(null), 2000)
41
+ } catch {
42
+ // Clipboard API can fail on non-HTTPS origins or without focus
43
+ console.warn('Failed to copy to clipboard')
44
+ }
45
+ }, [data])
46
+
47
+ const openBugReport = useCallback(() => {
48
+ if (!data) return
49
+ const body = formatForBugReport(data)
50
+ const url = `https://github.com/skyhook-io/radar/issues/new?labels=bug&body=${encodeURIComponent(body)}`
51
+ if (url.length > 8000) {
52
+ // URL too long for GitHub — copy diagnostics to clipboard and open blank issue
53
+ navigator.clipboard.writeText(body).catch(() => {})
54
+ openExternal('https://github.com/skyhook-io/radar/issues/new?labels=bug&template=bug_report.md')
55
+ setCopied('formatted')
56
+ setTimeout(() => setCopied(null), 2000)
57
+ return
58
+ }
59
+ openExternal(url)
60
+ setReportOpened(true)
61
+ setTimeout(() => setReportOpened(false), 2000)
62
+ }, [data])
63
+
64
+ return (
65
+ <div className="fixed inset-0 z-[100] flex items-start justify-center pt-[8vh]">
66
+ {/* Backdrop */}
67
+ <div
68
+ className={clsx(
69
+ 'absolute inset-0 bg-theme-base/60 backdrop-blur-sm',
70
+ TRANSITION_BACKDROP,
71
+ isOpen ? 'opacity-100' : 'opacity-0'
72
+ )}
73
+ onClick={onClose}
74
+ />
75
+
76
+ {/* Panel */}
77
+ <div className={clsx(
78
+ 'relative w-full max-w-2xl mx-4 dialog overflow-hidden flex flex-col max-h-[84vh]',
79
+ TRANSITION_PANEL,
80
+ isOpen ? 'opacity-100 scale-100 translate-y-0' : 'opacity-0 scale-[0.97] translate-y-3'
81
+ )}>
82
+ {/* Header */}
83
+ <div className="flex items-center justify-between px-5 py-3.5 border-b border-theme-border shrink-0">
84
+ <div className="flex items-center gap-3">
85
+ <h2 className="text-sm font-semibold text-theme-text-primary">Diagnostics</h2>
86
+ {data && (
87
+ <span className="text-xs text-theme-text-tertiary">
88
+ v{data.radarVersion} &middot; up {data.uptime}
89
+ </span>
90
+ )}
91
+ </div>
92
+ <button onClick={onClose} className="p-1 rounded-md text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated/50">
93
+ <X className="w-4 h-4" />
94
+ </button>
95
+ </div>
96
+
97
+ {/* Content */}
98
+ <div className="overflow-y-auto flex-1 px-5 py-4 space-y-4">
99
+ {isLoading && (
100
+ <div className="text-sm text-theme-text-tertiary text-center py-8">Loading diagnostics...</div>
101
+ )}
102
+ {error && (
103
+ <div className="text-sm text-red-400 text-center py-8">Failed to load diagnostics: {(error as Error).message}</div>
104
+ )}
105
+ {data && (
106
+ <>
107
+ <ErrorLogSection data={data} />
108
+ <ConnectionSection data={data} />
109
+ <KubeconfigSection data={data} />
110
+ <ClusterSection data={data} />
111
+ <CacheSection data={data} />
112
+ <MetricsSection data={data} />
113
+ <EventPipelineSection data={data} />
114
+ <InformersSection data={data} />
115
+ <PrometheusSection data={data} />
116
+ <TrafficSection data={data} />
117
+ <PermissionsSection data={data} />
118
+ <APIDiscoverySection data={data} />
119
+ <RuntimeSection data={data} />
120
+ <ConfigSection data={data} />
121
+ {data.errors && data.errors.length > 0 && (
122
+ <Section title="Collection Errors" warn>
123
+ {data.errors.map((e, i) => <Row key={i} label={`Error ${i + 1}`} value={e} />)}
124
+ </Section>
125
+ )}
126
+ </>
127
+ )}
128
+ </div>
129
+
130
+ {/* Footer */}
131
+ <div className="flex items-center gap-2 px-5 py-3 border-t border-theme-border shrink-0">
132
+ <CopyButton label="Copy as Markdown" onClick={() => copyToClipboard('formatted')} copied={copied === 'formatted'} />
133
+ <CopyButton label="Copy Raw JSON" onClick={() => copyToClipboard('json')} copied={copied === 'json'} />
134
+ <div className="flex-1" />
135
+ <button
136
+ onClick={openBugReport}
137
+ disabled={!data}
138
+ className={clsx(
139
+ 'flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed',
140
+ reportOpened
141
+ ? 'bg-green-500/20 text-green-400'
142
+ : 'bg-theme-elevated text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated/80'
143
+ )}
144
+ >
145
+ {reportOpened ? <Check className="w-3.5 h-3.5" /> : <ExternalLink className="w-3.5 h-3.5" />}
146
+ {reportOpened ? 'Opened!' : 'Report Bug'}
147
+ </button>
148
+ </div>
149
+ </div>
150
+ </div>
151
+ )
152
+ }
153
+
154
+ // --- Section components ---
155
+
156
+ function Section({ title, children, warn }: { title: string; children: React.ReactNode; warn?: boolean }) {
157
+ return (
158
+ <div className={clsx(
159
+ 'rounded-lg border px-3.5 py-2.5',
160
+ warn ? 'border-yellow-500/30 bg-yellow-500/5' : 'border-theme-border-light bg-theme-elevated/20'
161
+ )}>
162
+ <h3 className="text-[11px] font-semibold text-theme-text-tertiary uppercase tracking-wider mb-1.5">{title}</h3>
163
+ <div className="space-y-0.5">{children}</div>
164
+ </div>
165
+ )
166
+ }
167
+
168
+ function Row({ label, value, warn }: { label: string; value: React.ReactNode; warn?: boolean }) {
169
+ return (
170
+ <div className="flex items-baseline justify-between gap-4 text-xs">
171
+ <span className="text-theme-text-secondary shrink-0">{label}</span>
172
+ <span className={clsx(
173
+ 'text-right truncate',
174
+ warn ? 'text-yellow-400' : 'text-theme-text-primary'
175
+ )}>{value}</span>
176
+ </div>
177
+ )
178
+ }
179
+
180
+ function ErrorLogSection({ data }: { data: DiagnosticsSnapshot }) {
181
+ if (!data.recentErrors || data.recentErrors.length === 0) return null
182
+ const entries = data.recentErrors.slice(-10).reverse()
183
+ return (
184
+ <Section title={`Recent Errors (${data.recentErrors.length}${data.totalErrorsRecorded && data.totalErrorsRecorded > data.recentErrors.length ? ` of ${data.totalErrorsRecorded} total` : ''})`} warn>
185
+ {entries.map((e: DiagErrorEntry, i: number) => (
186
+ <Row key={i} label={`[${e.source}] ${new Date(e.time).toLocaleTimeString()}`} value={e.message} warn={e.level === 'error'} />
187
+ ))}
188
+ </Section>
189
+ )
190
+ }
191
+
192
+ function ConnectionSection({ data }: { data: DiagnosticsSnapshot }) {
193
+ if (!data.connection) return null
194
+ const c = data.connection
195
+ const warn = c.state !== 'connected'
196
+ return (
197
+ <Section title="Connection" warn={warn}>
198
+ <Row label="State" value={c.state} warn={warn} />
199
+ <Row label="Context" value={c.context} />
200
+ {c.clusterName && <Row label="Cluster" value={c.clusterName} />}
201
+ {c.error && <Row label="Error" value={c.error} warn />}
202
+ {c.errorType && <Row label="Error Type" value={c.errorType} warn />}
203
+ </Section>
204
+ )
205
+ }
206
+
207
+ function KubeconfigSection({ data }: { data: DiagnosticsSnapshot }) {
208
+ if (!data.kubeconfig) return null
209
+ const k = data.kubeconfig
210
+ // Missing exec plugins are the single strongest signal for desktop-app
211
+ // multi-cluster failures (radar#411) — GUI apps often don't inherit the
212
+ // user's PATH, so aws/gcloud/doctl/kubelogin can be invisible even though
213
+ // the CLI works fine. Highlight the whole section when that's the case.
214
+ const missing = k.execPluginsMissing ?? []
215
+ const present = k.execPluginsPresent ?? []
216
+ const hasMissing = missing.length > 0
217
+ return (
218
+ <Section title="Kubeconfig" warn={hasMissing}>
219
+ <Row label="Mode" value={k.mode || '(not initialized)'} />
220
+ <Row label="Files Loaded" value={k.fileCount} />
221
+ <Row label="Contexts (post-merge)" value={k.contextCount} />
222
+ <Row label="Enriched From Shell" value={k.enrichedFromShell ? 'Yes' : 'No'} />
223
+ <Row
224
+ label="Current Context Uses Exec"
225
+ value={k.currentContextUsesExec ? 'Yes' : 'No'}
226
+ />
227
+ {present.length > 0 && (
228
+ <Row label="Exec Plugins on PATH" value={present.join(', ')} />
229
+ )}
230
+ {hasMissing && (
231
+ <Row
232
+ label="Exec Plugins MISSING from PATH"
233
+ value={missing.join(', ')}
234
+ warn
235
+ />
236
+ )}
237
+ </Section>
238
+ )
239
+ }
240
+
241
+ function ClusterSection({ data }: { data: DiagnosticsSnapshot }) {
242
+ if (!data.cluster) return null
243
+ const c = data.cluster
244
+ return (
245
+ <Section title="Cluster">
246
+ <Row label="Platform" value={c.platform} />
247
+ <Row label="Kubernetes" value={c.kubernetesVersion} />
248
+ <Row label="Nodes" value={c.nodeCount} />
249
+ <Row label="Namespaces" value={c.namespaceCount} />
250
+ {c.inCluster && <Row label="In-Cluster" value="Yes" />}
251
+ </Section>
252
+ )
253
+ }
254
+
255
+ function CacheSection({ data }: { data: DiagnosticsSnapshot }) {
256
+ if (!data.cache) return null
257
+ return (
258
+ <Section title="Cache">
259
+ <Row label="Total Resources" value={data.cache.totalResources.toLocaleString()} />
260
+ <Row label="Watched Kinds" value={data.cache.watchedKinds.length} />
261
+ </Section>
262
+ )
263
+ }
264
+
265
+ function MetricsSection({ data }: { data: DiagnosticsSnapshot }) {
266
+ if (!data.metrics) return null
267
+ const m = data.metrics
268
+ const pod = m.podMetrics
269
+ const node = m.nodeMetrics
270
+ const warn = pod.consecutiveErrors > 0 || node.consecutiveErrors > 0
271
+ return (
272
+ <Section title="Metrics Collection" warn={warn}>
273
+ <MetricsSourceRow label="Pod Metrics" source={pod} />
274
+ <MetricsSourceRow label="Node Metrics" source={node} />
275
+ <Row label="Poll Loop" value={`${m.totalCollections} collections, every ${m.pollIntervalSec}s, buffer ${m.bufferSize} points`} />
276
+ {m.lastAttempt && <Row label="Last Attempt" value={new Date(m.lastAttempt).toLocaleTimeString()} />}
277
+ </Section>
278
+ )
279
+ }
280
+
281
+ function MetricsSourceRow({ label, source }: { label: string; source: DiagMetricsSourceHealth }) {
282
+ const status = source.collecting ? 'collecting' : source.consecutiveErrors > 0 ? `${source.consecutiveErrors} errors` : 'idle'
283
+ const warn = source.consecutiveErrors > 0
284
+ return (
285
+ <>
286
+ <Row label={label} value={`${status} (${source.trackedCount} tracked, ${source.totalDataPoints} points)`} warn={warn} />
287
+ {source.lastError && <Row label={` Last Error`} value={source.lastError} warn />}
288
+ </>
289
+ )
290
+ }
291
+
292
+ function EventPipelineSection({ data }: { data: DiagnosticsSnapshot }) {
293
+ if (!data.eventPipeline) return null
294
+ const ep = data.eventPipeline
295
+ const totalDropped = Object.values(ep.dropped).reduce((a, b) => a + b, 0)
296
+ const totalReceived = Object.values(ep.received).reduce((a, b) => a + b, 0)
297
+ const warn = totalDropped > 0
298
+ return (
299
+ <Section title="Event Pipeline" warn={warn}>
300
+ <Row label="Total Received" value={totalReceived.toLocaleString()} />
301
+ <Row label="Total Dropped" value={totalDropped.toLocaleString()} warn={warn} />
302
+ <Row label="Uptime" value={ep.uptime} />
303
+ {ep.recentDrops && ep.recentDrops.length > 0 && (
304
+ <div className="mt-1.5 pt-1.5 border-t border-theme-border-light">
305
+ <span className="text-[10px] text-theme-text-tertiary uppercase">Recent Drops ({ep.recentDrops.length})</span>
306
+ {ep.recentDrops.slice(0, 5).map((d: DiagDropRecord, i: number) => (
307
+ <Row key={i} label={`${d.kind}/${d.name}`} value={d.reason} warn />
308
+ ))}
309
+ </div>
310
+ )}
311
+ </Section>
312
+ )
313
+ }
314
+
315
+ function InformersSection({ data }: { data: DiagnosticsSnapshot }) {
316
+ if (!data.informers) return null
317
+ const inf = data.informers
318
+ return (
319
+ <Section title="Informers">
320
+ <Row label="Typed" value={inf.typedCount} />
321
+ <Row label="Dynamic (CRDs)" value={inf.dynamicCount} />
322
+ {inf.watchedCRDs && inf.watchedCRDs.length > 0 && (
323
+ <Row label="Watched CRDs" value={inf.watchedCRDs.join(', ')} />
324
+ )}
325
+ </Section>
326
+ )
327
+ }
328
+
329
+ function PrometheusSection({ data }: { data: DiagnosticsSnapshot }) {
330
+ if (!data.prometheus) return null
331
+ const p = data.prometheus
332
+ const warn = !p.connected
333
+ return (
334
+ <Section title="Prometheus" warn={warn}>
335
+ <Row label="Connected" value={p.connected ? 'Yes' : 'No'} warn={warn} />
336
+ {p.address && <Row label="Address" value={p.address} />}
337
+ {p.serviceName && <Row label="Service" value={`${p.serviceNamespace}/${p.serviceName}`} />}
338
+ </Section>
339
+ )
340
+ }
341
+
342
+ function TrafficSection({ data }: { data: DiagnosticsSnapshot }) {
343
+ if (!data.traffic) return null
344
+ const t = data.traffic
345
+ return (
346
+ <Section title="Traffic">
347
+ <Row label="Active Source" value={t.activeSource || 'none'} />
348
+ {t.detected && t.detected.length > 0 && <Row label="Detected" value={t.detected.join(', ')} />}
349
+ {t.notDetected && t.notDetected.length > 0 && <Row label="Not Detected" value={t.notDetected.join(', ')} />}
350
+ </Section>
351
+ )
352
+ }
353
+
354
+ function PermissionsSection({ data }: { data: DiagnosticsSnapshot }) {
355
+ if (!data.permissions) return null
356
+ const p = data.permissions
357
+ const warn = (p.restricted && p.restricted.length > 0) || false
358
+ return (
359
+ <Section title="Permissions" warn={warn}>
360
+ <Row label="Capabilities" value={[
361
+ p.exec && 'exec', p.logs && 'logs', p.portForward && 'port-forward',
362
+ p.secrets && 'secrets', p.helmWrite && 'helm-write',
363
+ ].filter(Boolean).join(', ') || 'none'} />
364
+ {p.namespaceScoped && <Row label="Scope" value={`namespace: ${p.namespace}`} warn />}
365
+ {p.restricted && p.restricted.length > 0 && (
366
+ <Row label="Restricted" value={p.restricted.join(', ')} warn />
367
+ )}
368
+ </Section>
369
+ )
370
+ }
371
+
372
+ function APIDiscoverySection({ data }: { data: DiagnosticsSnapshot }) {
373
+ if (!data.apiDiscovery) return null
374
+ const d = data.apiDiscovery
375
+ return (
376
+ <Section title="API Discovery">
377
+ <Row label="Total Resources" value={d.totalResources} />
378
+ <Row label="CRDs" value={d.crdCount} />
379
+ {d.lastRefresh && <Row label="Last Refresh" value={new Date(d.lastRefresh).toLocaleTimeString()} />}
380
+ </Section>
381
+ )
382
+ }
383
+
384
+ function RuntimeSection({ data }: { data: DiagnosticsSnapshot }) {
385
+ if (!data.runtime) return null
386
+ const rt = data.runtime
387
+ return (
388
+ <Section title="Runtime">
389
+ <Row label="Heap" value={`${rt.heapMB.toFixed(1)} MB (${rt.heapObjectsK.toFixed(1)}K objects)`} />
390
+ <Row label="Goroutines" value={rt.goroutines} />
391
+ <Row label="CPUs" value={rt.numCPU} />
392
+ {data.sse && <Row label="SSE Clients" value={data.sse.connectedClients} />}
393
+ </Section>
394
+ )
395
+ }
396
+
397
+ function ConfigSection({ data }: { data: DiagnosticsSnapshot }) {
398
+ if (!data.config) return null
399
+ const cfg = data.config
400
+ return (
401
+ <Section title="Config">
402
+ <Row label="Port" value={cfg.port} />
403
+ <Row label="Dev Mode" value={cfg.devMode ? 'Yes' : 'No'} />
404
+ {cfg.namespace && <Row label="Namespace Filter" value={cfg.namespace} />}
405
+ <Row label="Timeline Storage" value={cfg.timelineStorage} />
406
+ <Row label="History Limit" value={cfg.historyLimit.toLocaleString()} />
407
+ <Row label="MCP Enabled" value={cfg.mcpEnabled ? 'Yes' : 'No'} />
408
+ <Row label="Prometheus URL" value={cfg.hasPrometheusURL ? 'Set' : 'Auto-discover'} />
409
+ </Section>
410
+ )
411
+ }
412
+
413
+ // --- Copy button ---
414
+
415
+ function CopyButton({ label, onClick, copied }: { label: string; onClick: () => void; copied: boolean }) {
416
+ return (
417
+ <button
418
+ onClick={onClick}
419
+ className={clsx(
420
+ 'flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md transition-colors',
421
+ copied
422
+ ? 'bg-green-500/20 text-green-400'
423
+ : 'bg-theme-elevated text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated/80'
424
+ )}
425
+ >
426
+ {copied ? <Check className="w-3.5 h-3.5" /> : <Copy className="w-3.5 h-3.5" />}
427
+ {copied ? 'Copied!' : label}
428
+ </button>
429
+ )
430
+ }
431
+
432
+ // --- GitHub-friendly formatting ---
433
+
434
+ function formatForGitHub(data: DiagnosticsSnapshot, includeRawJson = true): string {
435
+ const lines: string[] = []
436
+ lines.push(`## Radar Diagnostics`)
437
+ lines.push(``)
438
+ lines.push(`**Version:** ${data.radarVersion} | **Go:** ${data.goVersion} | **OS:** ${data.goos}/${data.goarch} | **Uptime:** ${data.uptime}`)
439
+ lines.push(``)
440
+
441
+ if (data.connection) {
442
+ const c = data.connection
443
+ lines.push(`### Connection`)
444
+ lines.push(`- State: \`${c.state}\``)
445
+ lines.push(`- Context: \`${c.context}\``)
446
+ if (c.clusterName) lines.push(`- Cluster: \`${c.clusterName}\``)
447
+ if (c.error) lines.push(`- Error: ${c.error}`)
448
+ if (c.errorType) lines.push(`- Error Type: \`${c.errorType}\``)
449
+ lines.push(``)
450
+ }
451
+
452
+ if (data.kubeconfig) {
453
+ const k = data.kubeconfig
454
+ lines.push(`### Kubeconfig`)
455
+ lines.push(`- Mode: \`${k.mode || '(not initialized)'}\` | Files: ${k.fileCount} | Contexts (post-merge): ${k.contextCount} | Enriched From Shell: ${k.enrichedFromShell ? 'Yes' : 'No'}`)
456
+ lines.push(`- Current Context Uses Exec: ${k.currentContextUsesExec ? 'Yes' : 'No'}`)
457
+ if (k.execPluginsPresent && k.execPluginsPresent.length > 0) {
458
+ lines.push(`- Exec Plugins on PATH: \`${k.execPluginsPresent.join('`, `')}\``)
459
+ }
460
+ if (k.execPluginsMissing && k.execPluginsMissing.length > 0) {
461
+ lines.push(`- **Exec Plugins MISSING from PATH:** \`${k.execPluginsMissing.join('`, `')}\``)
462
+ }
463
+ lines.push(``)
464
+ }
465
+
466
+ if (data.cluster) {
467
+ const c = data.cluster
468
+ lines.push(`### Cluster`)
469
+ lines.push(`- Platform: \`${c.platform}\` | K8s: \`${c.kubernetesVersion}\` | Nodes: ${c.nodeCount} | Namespaces: ${c.namespaceCount}${c.inCluster ? ' | In-Cluster' : ''}`)
470
+ lines.push(``)
471
+ }
472
+
473
+ if (data.cache) {
474
+ lines.push(`### Cache`)
475
+ lines.push(`- Total Resources: ${data.cache.totalResources.toLocaleString()} | Watched Kinds: ${data.cache.watchedKinds.length}`)
476
+ lines.push(``)
477
+ }
478
+
479
+ if (data.metrics) {
480
+ const m = data.metrics
481
+ const pod = m.podMetrics
482
+ const node = m.nodeMetrics
483
+ lines.push(`### Metrics Collection`)
484
+ lines.push(`- Pod: ${pod.collecting ? 'collecting' : 'idle'} (${pod.trackedCount} tracked, ${pod.totalDataPoints} points, ${pod.consecutiveErrors} errors)`)
485
+ lines.push(`- Node: ${node.collecting ? 'collecting' : 'idle'} (${node.trackedCount} tracked, ${node.totalDataPoints} points, ${node.consecutiveErrors} errors)`)
486
+ lines.push(`- Poll loop: ${m.totalCollections} collections, every ${m.pollIntervalSec}s, buffer ${m.bufferSize} points`)
487
+ if (pod.lastError) lines.push(`- Pod Error: ${pod.lastError}`)
488
+ if (node.lastError) lines.push(`- Node Error: ${node.lastError}`)
489
+ lines.push(``)
490
+ }
491
+
492
+ if (data.eventPipeline) {
493
+ const ep = data.eventPipeline
494
+ const totalReceived = Object.values(ep.received).reduce((a, b) => a + b, 0)
495
+ const totalDropped = Object.values(ep.dropped).reduce((a, b) => a + b, 0)
496
+ lines.push(`### Event Pipeline`)
497
+ lines.push(`- Received: ${totalReceived.toLocaleString()} | Dropped: ${totalDropped.toLocaleString()} | Uptime: ${ep.uptime}`)
498
+ if (ep.recentDrops && ep.recentDrops.length > 0) {
499
+ lines.push(`- Recent drops: ${ep.recentDrops.slice(0, 5).map(d => `${d.kind}/${d.name} (${d.reason})`).join(', ')}`)
500
+ }
501
+ lines.push(``)
502
+ }
503
+
504
+ if (data.timeline) {
505
+ const t = data.timeline
506
+ lines.push(`### Timeline`)
507
+ lines.push(`- Storage: \`${t.storageType}\` | Events: ${t.totalEvents.toLocaleString()} | Errors: ${t.storeErrors} | Drops: ${t.totalDrops}`)
508
+ lines.push(``)
509
+ }
510
+
511
+ if (data.informers) {
512
+ const inf = data.informers
513
+ lines.push(`### Informers`)
514
+ lines.push(`- Typed: ${inf.typedCount} | Dynamic: ${inf.dynamicCount}`)
515
+ if (inf.watchedCRDs && inf.watchedCRDs.length > 0) {
516
+ lines.push(`- CRDs: ${inf.watchedCRDs.join(', ')}`)
517
+ }
518
+ lines.push(``)
519
+ }
520
+
521
+ if (data.prometheus) {
522
+ const p = data.prometheus
523
+ lines.push(`### Prometheus`)
524
+ lines.push(`- Connected: ${p.connected ? 'Yes' : 'No'}${p.serviceName ? ` | Service: ${p.serviceNamespace}/${p.serviceName}` : ''}`)
525
+ lines.push(``)
526
+ }
527
+
528
+ if (data.traffic) {
529
+ const t = data.traffic
530
+ lines.push(`### Traffic`)
531
+ lines.push(`- Active: \`${t.activeSource || 'none'}\`${t.detected?.length ? ` | Detected: ${t.detected.join(', ')}` : ''}`)
532
+ lines.push(``)
533
+ }
534
+
535
+ if (data.permissions) {
536
+ const p = data.permissions
537
+ const caps = [p.exec && 'exec', p.logs && 'logs', p.portForward && 'port-forward', p.secrets && 'secrets', p.helmWrite && 'helm-write'].filter(Boolean).join(', ')
538
+ lines.push(`### Permissions`)
539
+ lines.push(`- Capabilities: ${caps || 'none'}${p.namespaceScoped ? ` | Scope: namespace \`${p.namespace}\`` : ''}`)
540
+ if (p.restricted && p.restricted.length > 0) lines.push(`- Restricted: ${p.restricted.join(', ')}`)
541
+ lines.push(``)
542
+ }
543
+
544
+ if (data.apiDiscovery) {
545
+ const d = data.apiDiscovery
546
+ lines.push(`### API Discovery`)
547
+ lines.push(`- Total Resources: ${d.totalResources} | CRDs: ${d.crdCount}`)
548
+ lines.push(``)
549
+ }
550
+
551
+ if (data.runtime) {
552
+ const rt = data.runtime
553
+ lines.push(`### Runtime`)
554
+ lines.push(`- Heap: ${rt.heapMB.toFixed(1)} MB | Objects: ${rt.heapObjectsK.toFixed(1)}K | Goroutines: ${rt.goroutines} | CPUs: ${rt.numCPU}`)
555
+ if (data.sse) lines.push(`- SSE Clients: ${data.sse.connectedClients}`)
556
+ lines.push(``)
557
+ }
558
+
559
+ if (data.config) {
560
+ const cfg = data.config
561
+ lines.push(`### Config`)
562
+ lines.push(`- Port: ${cfg.port} | Dev: ${cfg.devMode} | Timeline: \`${cfg.timelineStorage}\` | History: ${cfg.historyLimit} | MCP: ${cfg.mcpEnabled} | Prometheus URL: ${cfg.hasPrometheusURL ? 'manual' : 'auto'}`)
563
+ lines.push(``)
564
+ }
565
+
566
+ if (data.errors && data.errors.length > 0) {
567
+ lines.push(`### Collection Errors`)
568
+ for (const e of data.errors) {
569
+ lines.push(`- ${e}`)
570
+ }
571
+ lines.push(``)
572
+ }
573
+
574
+ if (data.recentErrors && data.recentErrors.length > 0) {
575
+ lines.push(`### Recent Errors (${data.recentErrors.length}${data.totalErrorsRecorded && data.totalErrorsRecorded > data.recentErrors.length ? ` of ${data.totalErrorsRecorded} total` : ''})`)
576
+ for (const e of data.recentErrors.slice(-10).reverse()) {
577
+ lines.push(`- **[${e.source}]** ${e.message} _(${new Date(e.time).toLocaleTimeString()})_`)
578
+ }
579
+ lines.push(``)
580
+ }
581
+
582
+ if (includeRawJson) {
583
+ lines.push(`<details><summary>Raw JSON</summary>`)
584
+ lines.push(``)
585
+ lines.push('```json')
586
+ lines.push(JSON.stringify(data, null, 2))
587
+ lines.push('```')
588
+ lines.push(`</details>`)
589
+ }
590
+
591
+ return lines.join('\n')
592
+ }
593
+
594
+ function formatForBugReport(data: DiagnosticsSnapshot): string {
595
+ const diagnostics = formatForGitHub(data, false)
596
+
597
+ const lines: string[] = []
598
+ lines.push(`## Describe the bug`)
599
+ lines.push(``)
600
+ lines.push(`<!-- A clear and concise description of what the bug is. -->`)
601
+ lines.push(``)
602
+ lines.push(`## To reproduce`)
603
+ lines.push(``)
604
+ lines.push(`<!-- Steps to reproduce the behavior -->`)
605
+ lines.push(``)
606
+ lines.push(`## Expected behavior`)
607
+ lines.push(``)
608
+ lines.push(`<!-- What you expected to happen -->`)
609
+ lines.push(``)
610
+ lines.push(`## Diagnostics`)
611
+ lines.push(``)
612
+ lines.push(`<details><summary>Diagnostics snapshot</summary>`)
613
+ lines.push(``)
614
+ lines.push(diagnostics)
615
+ lines.push(``)
616
+ lines.push(`</details>`)
617
+
618
+ return lines.join('\n')
619
+ }
@@ -0,0 +1,46 @@
1
+ import { Component, ReactNode } from 'react'
2
+ import { AlertTriangle, RefreshCw } from 'lucide-react'
3
+
4
+ interface Props {
5
+ children: ReactNode
6
+ }
7
+
8
+ interface State {
9
+ hasError: boolean
10
+ error: Error | null
11
+ }
12
+
13
+ export class ErrorBoundary extends Component<Props, State> {
14
+ state: State = { hasError: false, error: null }
15
+
16
+ static getDerivedStateFromError(error: Error): State {
17
+ return { hasError: true, error }
18
+ }
19
+
20
+ componentDidCatch(error: Error, info: React.ErrorInfo) {
21
+ console.error('[ErrorBoundary]', error, info.componentStack)
22
+ }
23
+
24
+ handleReset = () => this.setState({ hasError: false, error: null })
25
+
26
+ render() {
27
+ if (!this.state.hasError) return this.props.children
28
+
29
+ return (
30
+ <div className="flex flex-col items-center justify-center h-full p-8 text-center">
31
+ <AlertTriangle className="w-12 h-12 text-red-400 mb-4" />
32
+ <h2 className="text-lg font-semibold text-theme-text-primary mb-2">Something went wrong</h2>
33
+ <p className="text-sm text-theme-text-secondary mb-4 max-w-md">
34
+ {this.state.error?.message || 'An unexpected error occurred'}
35
+ </p>
36
+ <button
37
+ onClick={this.handleReset}
38
+ className="flex items-center gap-2 px-4 py-2 text-sm font-medium btn-brand rounded-lg"
39
+ >
40
+ <RefreshCw className="w-4 h-4" />
41
+ Try Again
42
+ </button>
43
+ </div>
44
+ )
45
+ }
46
+ }
@@ -0,0 +1 @@
1
+ export { ForceDeleteConfirmDialog } from '@skyhook-io/k8s-ui'