@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,105 @@
1
+ import type { DashboardCertificateHealth } from '../../api/client'
2
+ import { Shield, ArrowRight } from 'lucide-react'
3
+ import { clsx } from 'clsx'
4
+
5
+ interface CertificateHealthCardProps {
6
+ data: DashboardCertificateHealth
7
+ onNavigate: () => void
8
+ }
9
+
10
+ export function CertificateHealthCard({ data, onNavigate }: CertificateHealthCardProps) {
11
+ const hasIssues = data.expired > 0 || data.critical > 0
12
+ const hasWarnings = data.warning > 0
13
+ const accentColor = hasIssues ? 'text-red-500' : hasWarnings ? 'text-yellow-500' : 'text-green-500'
14
+ const accentBg = hasIssues ? 'bg-red-500/10' : hasWarnings ? 'bg-yellow-500/10' : 'bg-green-500/10'
15
+
16
+ return (
17
+ <button
18
+ onClick={onNavigate}
19
+ className="group h-[260px] rounded-xl bg-theme-surface shadow-theme-sm hover:-translate-y-1 hover:shadow-theme-md transition-all duration-200 text-left animate-fade-in-up"
20
+ >
21
+ <div className="flex flex-col h-full w-full">
22
+ <div className="flex items-center justify-between px-5 py-3 border-b border-theme-border/50">
23
+ <div className="flex items-center gap-2">
24
+ <Shield className={clsx('w-4 h-4', accentColor)} />
25
+ <span className={clsx('text-xs font-semibold uppercase tracking-wider', accentColor)}>TLS Certificates</span>
26
+ <span className={clsx('badge-sm', accentBg, accentColor)}>
27
+ {data.total}
28
+ </span>
29
+ </div>
30
+ </div>
31
+
32
+ <div className="flex-1 min-h-0 flex flex-col items-center justify-center px-4 py-4">
33
+ {/* Expiry distribution bar */}
34
+ <div className="flex items-center gap-3 w-full">
35
+ {/* Color bar showing distribution */}
36
+ <div className="flex-1 h-3 rounded-full overflow-hidden bg-theme-hover flex">
37
+ {data.healthy > 0 && (
38
+ <div
39
+ className="h-full bg-green-500"
40
+ style={{ width: `${(data.healthy / data.total) * 100}%` }}
41
+ />
42
+ )}
43
+ {data.warning > 0 && (
44
+ <div
45
+ className="h-full bg-yellow-500"
46
+ style={{ width: `${(data.warning / data.total) * 100}%` }}
47
+ />
48
+ )}
49
+ {data.critical > 0 && (
50
+ <div
51
+ className="h-full bg-orange-500"
52
+ style={{ width: `${(data.critical / data.total) * 100}%` }}
53
+ />
54
+ )}
55
+ {data.expired > 0 && (
56
+ <div
57
+ className="h-full bg-red-500"
58
+ style={{ width: `${(data.expired / data.total) * 100}%` }}
59
+ />
60
+ )}
61
+ </div>
62
+ </div>
63
+
64
+ {/* Breakdown */}
65
+ <div className="grid grid-cols-2 gap-x-6 gap-y-2 mt-4 w-full">
66
+ <BucketRow label="Healthy" count={data.healthy} color="text-green-400" dotColor="bg-green-500" />
67
+ <BucketRow label="Warning" subtitle="< 30d" count={data.warning} color="text-yellow-400" dotColor="bg-yellow-500" />
68
+ <BucketRow label="Critical" subtitle="< 7d" count={data.critical} color="text-orange-400" dotColor="bg-orange-500" />
69
+ <BucketRow label="Expired" count={data.expired} color="text-red-400" dotColor="bg-red-500" />
70
+ </div>
71
+ </div>
72
+
73
+ <div className="px-4 py-1.5 border-t border-theme-border/50 flex items-center justify-end">
74
+ <span className={clsx(
75
+ 'flex items-center gap-1.5 text-[10px] font-semibold uppercase tracking-wider transition-colors',
76
+ accentColor,
77
+ hasIssues ? 'group-hover:text-red-400' : hasWarnings ? 'group-hover:text-yellow-400' : 'group-hover:text-green-400'
78
+ )}>
79
+ View Secrets
80
+ <ArrowRight className="w-3.5 h-3.5 transition-transform group-hover:translate-x-0.5" />
81
+ </span>
82
+ </div>
83
+ </div>
84
+ </button>
85
+ )
86
+ }
87
+
88
+ function BucketRow({ label, subtitle, count, color, dotColor }: {
89
+ label: string
90
+ subtitle?: string
91
+ count: number
92
+ color: string
93
+ dotColor: string
94
+ }) {
95
+ return (
96
+ <div className="flex items-center gap-2">
97
+ <span className={clsx('w-2 h-2 rounded-full shrink-0', dotColor)} />
98
+ <span className="text-xs text-theme-text-secondary flex-1">
99
+ {label}
100
+ {subtitle && <span className="text-theme-text-tertiary ml-1">{subtitle}</span>}
101
+ </span>
102
+ <span className={clsx('text-sm font-semibold tabular-nums', count > 0 ? color : 'text-theme-text-tertiary')}>{count}</span>
103
+ </div>
104
+ )
105
+ }
@@ -0,0 +1,483 @@
1
+ import { useState } from 'react'
2
+ import type { DashboardResponse, DashboardMetrics, DashboardCRDCount, DashboardProblem } from '../../api/client'
3
+ import { HealthRing } from './HealthRing'
4
+ import {
5
+ AlertTriangle, CheckCircle, XCircle,
6
+ Cpu, MemoryStick, Database, Container, Globe, Network as NetworkIcon, Briefcase, Clock,
7
+ ArrowRight, Server, Boxes, Shield, Radio, Info,
8
+ } from 'lucide-react'
9
+ import { clsx } from 'clsx'
10
+ import { formatCPUMillicores, formatMemoryMiB } from '../../utils/format'
11
+ import { useCapabilitiesContext } from '../../contexts/CapabilitiesContext'
12
+ import { MCPSetupDialog } from './MCPSetupDialog'
13
+ import { Tooltip } from '../ui/Tooltip'
14
+
15
+ interface ClusterHealthCardProps {
16
+ health: DashboardResponse['health']
17
+ counts: DashboardResponse['resourceCounts']
18
+ cluster: DashboardResponse['cluster']
19
+ metrics: DashboardMetrics | null
20
+ metricsServerAvailable: boolean
21
+ topCRDs?: DashboardCRDCount[] // Loaded lazily, may be undefined
22
+ problems: DashboardProblem[]
23
+ nodeVersionSkew: DashboardResponse['nodeVersionSkew']
24
+ onNavigateToKind: (kind: string, group?: string) => void
25
+ onNavigateToView: () => void
26
+ onWarningEventsClick?: () => void
27
+ onUnhealthyClick?: () => void
28
+ }
29
+
30
+ function getMetricsInstallHint(platform: string): string {
31
+ const p = platform.toLowerCase()
32
+ if (p.includes('minikube')) return 'minikube addons enable metrics-server'
33
+ if (p.includes('gke') || p.includes('aks')) return 'metrics-server is usually pre-installed on this platform — check if it was disabled or removed'
34
+ if (p.includes('eks')) return 'kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml'
35
+ return 'kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml'
36
+ }
37
+
38
+ function MetricsUnavailableHint({ platform, metricsServerAvailable }: { platform: string; metricsServerAvailable: boolean }) {
39
+ if (metricsServerAvailable) {
40
+ return <span className="text-xs text-theme-text-tertiary">Waiting for metrics data...</span>
41
+ }
42
+
43
+ const hint = getMetricsInstallHint(platform)
44
+ const isPreInstalled = platform.toLowerCase().includes('gke') || platform.toLowerCase().includes('aks')
45
+
46
+ return (
47
+ <Tooltip
48
+ content={
49
+ <div className="space-y-1">
50
+ <div className="font-medium">How to fix</div>
51
+ <div>{isPreInstalled ? hint : <>Install by running:<br /><code className="text-[10px] opacity-80">{hint}</code></>}</div>
52
+ </div>
53
+ }
54
+ position="bottom"
55
+ className="!whitespace-normal !max-w-sm"
56
+ >
57
+ <span className="flex items-center gap-1.5 text-xs text-theme-text-tertiary">
58
+ <Info className="w-3 h-3 shrink-0" />
59
+ <span>Requires <span className="text-theme-text-secondary">metrics-server</span> to display CPU & memory usage</span>
60
+ </span>
61
+ </Tooltip>
62
+ )
63
+ }
64
+
65
+ // Get platform display name and icon path
66
+ function getPlatformInfo(platform: string): { name: string; icon: string | null } {
67
+ const platformLower = platform.toLowerCase()
68
+ if (platformLower.includes('gke') || platformLower.includes('google')) {
69
+ return { name: 'Google Kubernetes Engine', icon: '/icons/google_kubernetes_engine.png' }
70
+ }
71
+ if (platformLower.includes('eks') || platformLower.includes('amazon') || platformLower.includes('aws')) {
72
+ return { name: 'Amazon EKS', icon: '/icons/aws_eks.png' }
73
+ }
74
+ if (platformLower.includes('aks') || platformLower.includes('azure')) {
75
+ return { name: 'Azure Kubernetes Service', icon: '/icons/azure-aks.svg' }
76
+ }
77
+ if (platformLower.includes('openshift')) {
78
+ return { name: 'OpenShift', icon: null }
79
+ }
80
+ if (platformLower.includes('rancher')) {
81
+ return { name: 'Rancher', icon: null }
82
+ }
83
+ if (platformLower.includes('k3s')) {
84
+ return { name: 'K3s', icon: null }
85
+ }
86
+ if (platformLower.includes('kind')) {
87
+ return { name: 'kind', icon: null }
88
+ }
89
+ if (platformLower.includes('minikube')) {
90
+ return { name: 'Minikube', icon: null }
91
+ }
92
+ if (platformLower.includes('docker')) {
93
+ return { name: 'Docker Desktop', icon: null }
94
+ }
95
+ return { name: platform || 'Kubernetes', icon: null }
96
+ }
97
+
98
+ export function ClusterHealthCard({
99
+ health,
100
+ counts,
101
+ cluster,
102
+ metrics,
103
+ metricsServerAvailable,
104
+ topCRDs: _topCRDs,
105
+ problems,
106
+ nodeVersionSkew,
107
+ onNavigateToKind,
108
+ onNavigateToView,
109
+ onWarningEventsClick,
110
+ onUnhealthyClick,
111
+ }: ClusterHealthCardProps) {
112
+ void _topCRDs // Reserved for future CRD display
113
+
114
+ const [mcpDialogOpen, setMcpDialogOpen] = useState(false)
115
+ const { mcpEnabled } = useCapabilitiesContext()
116
+ const mcpUrl = `${window.location.origin}/mcp`
117
+
118
+ const restricted = counts.restricted ?? []
119
+ const isRestricted = (kind: string) => restricted.includes(kind)
120
+
121
+ // Pods ring segments
122
+ const podsTotal = health.healthy + health.warning + health.error
123
+ const podsRingSegments = [
124
+ { value: health.healthy, color: '#22c55e' }, // green-500
125
+ { value: health.warning, color: '#eab308' }, // yellow-500
126
+ { value: health.error, color: '#ef4444' }, // red-500
127
+ ]
128
+
129
+ // Deployments ring segments
130
+ const deploymentsRingSegments = [
131
+ { value: counts.deployments.available, color: '#22c55e' },
132
+ { value: counts.deployments.unavailable, color: '#ef4444' },
133
+ ]
134
+
135
+ // Nodes ring segments
136
+ const cordonedCount = counts.nodes.cordoned ?? 0
137
+ const nodesRingSegments = [
138
+ { value: counts.nodes.ready, color: '#22c55e' },
139
+ { value: cordonedCount, color: '#eab308' }, // amber for cordoned
140
+ { value: counts.nodes.notReady, color: '#ef4444' },
141
+ ]
142
+
143
+ // Secondary resource counts
144
+ // Show whichever networking type has more resources: Ingresses or Routes (Gateway API)
145
+ const routeCount = counts.routes ?? 0
146
+ const ingressCount = counts.ingresses ?? 0
147
+
148
+ type SecondaryResource = { kind: string; group?: string; label: string; icon: typeof Globe; total: number; subtitle?: string; hasIssues?: boolean }
149
+ const secondaryResources: SecondaryResource[] = [
150
+ { kind: 'statefulsets', label: 'StatefulSets', icon: Database, total: counts.statefulSets.total, subtitle: `${counts.statefulSets.ready} ready`, hasIssues: counts.statefulSets.unready > 0 },
151
+ { kind: 'daemonsets', label: 'DaemonSets', icon: Container, total: counts.daemonSets.total, subtitle: `${counts.daemonSets.ready} ready`, hasIssues: counts.daemonSets.unready > 0 },
152
+ { kind: 'services', label: 'Services', icon: Globe, total: counts.services },
153
+ routeCount > ingressCount
154
+ ? { kind: 'httproutes', group: 'gateway.networking.k8s.io', label: 'Routes', icon: Globe, total: routeCount }
155
+ : { kind: 'ingresses', label: 'Ingresses', icon: NetworkIcon, total: ingressCount },
156
+ { kind: 'jobs', label: 'Jobs', icon: Briefcase, total: counts.jobs.total, subtitle: `${counts.jobs.active} active`, hasIssues: counts.jobs.failed > 0 },
157
+ { kind: 'cronjobs', label: 'CronJobs', icon: Clock, total: counts.cronJobs.total, subtitle: `${counts.cronJobs.active} active` },
158
+ ]
159
+ const platformInfo = getPlatformInfo(cluster.platform)
160
+
161
+ return (
162
+ <div className="rounded-xl bg-theme-surface shadow-theme-sm overflow-hidden">
163
+ {/* Main health section - three columns */}
164
+ <div className="px-6 py-5 border-b border-theme-border/50">
165
+ <div className="flex items-stretch gap-8">
166
+ {/* Left: Cluster info */}
167
+ <div className="flex flex-col justify-center w-[300px] shrink-0 pr-8 border-r border-theme-border/50">
168
+ <div className="flex items-center gap-2 mb-2">
169
+ {platformInfo.icon ? (
170
+ <img src={platformInfo.icon} alt={platformInfo.name} className="w-5 h-5 object-contain" />
171
+ ) : (
172
+ <Server className="w-4 h-4 text-theme-text-tertiary" />
173
+ )}
174
+ <span className="text-xs text-theme-text-secondary">{platformInfo.name}</span>
175
+ </div>
176
+ <h2 className="text-sm font-semibold text-theme-text-primary break-all mb-1" title={cluster.name}>
177
+ {cluster.name || 'Cluster'}
178
+ </h2>
179
+ <div className="flex flex-col gap-1 text-xs text-theme-text-tertiary">
180
+ {cluster.version && (
181
+ <span>Kubernetes {cluster.version}</span>
182
+ )}
183
+ <span><span className="font-mono">{counts.namespaces}</span> namespaces</span>
184
+ </div>
185
+ {nodeVersionSkew && (
186
+ <Tooltip
187
+ content={
188
+ <div className="space-y-1.5">
189
+ <div className="font-medium">Node version skew detected</div>
190
+ {Object.entries(nodeVersionSkew.versions).map(([version, nodes]) => (
191
+ <div key={version}>
192
+ <span className="font-mono font-medium">v{version}</span>
193
+ <span className="text-theme-text-tertiary"> — {nodes.length} node{nodes.length > 1 ? 's' : ''}</span>
194
+ <div className="text-[10px] text-theme-text-tertiary pl-2">{nodes.join(', ')}</div>
195
+ </div>
196
+ ))}
197
+ </div>
198
+ }
199
+ position="bottom"
200
+ className="!whitespace-normal !max-w-sm"
201
+ >
202
+ <span className="flex items-center gap-1.5 mt-1 text-xs text-yellow-500">
203
+ <AlertTriangle className="w-3 h-3 shrink-0" />
204
+ Version skew: v{nodeVersionSkew.minVersion} — v{nodeVersionSkew.maxVersion}
205
+ </span>
206
+ </Tooltip>
207
+ )}
208
+ {/* MCP Server indicator */}
209
+ {mcpEnabled && (
210
+ <button
211
+ onClick={() => setMcpDialogOpen(true)}
212
+ className="flex items-center gap-2 mt-3 px-2.5 py-2 bg-purple-500/5 hover:bg-purple-500/10 border border-purple-500/20 rounded-md transition-colors w-full"
213
+ >
214
+ <Radio className="w-3.5 h-3.5 text-purple-400 animate-pulse shrink-0" />
215
+ <div className="flex flex-col gap-0.5 min-w-0 flex-1 text-left">
216
+ <span className="text-xs font-medium text-purple-400">MCP Server Live</span>
217
+ <span className="text-[10px] text-theme-text-tertiary truncate font-mono" title={mcpUrl}>
218
+ HTTP · {mcpUrl}
219
+ </span>
220
+ </div>
221
+ <Info className="w-3.5 h-3.5 text-purple-400/60 shrink-0" />
222
+ </button>
223
+ )}
224
+ <MCPSetupDialog open={mcpDialogOpen} onClose={() => setMcpDialogOpen(false)} mcpUrl={mcpUrl} />
225
+ </div>
226
+
227
+ {/* Center: Three health rings */}
228
+ <div className="flex-1 flex items-center justify-center gap-12">
229
+ {/* Pods Ring */}
230
+ {isRestricted('pods') ? (
231
+ <RestrictedRing label="Pods" />
232
+ ) : (
233
+ <button
234
+ onClick={() => onNavigateToKind('pods')}
235
+ className="flex flex-col items-center gap-2 hover:-translate-y-1 hover:scale-105 transition-all duration-200"
236
+ >
237
+ <HealthRing segments={podsRingSegments} size={88} strokeWidth={8} label={String(podsTotal)} />
238
+ <span className="text-sm font-semibold uppercase tracking-wider text-theme-text-secondary">Pods</span>
239
+ <div className="flex items-center gap-2 text-xs font-mono">
240
+ {health.healthy > 0 && (
241
+ <span className="flex items-center gap-0.5 text-green-500">
242
+ <CheckCircle className="w-3 h-3" />
243
+ {health.healthy}
244
+ </span>
245
+ )}
246
+ {health.warning > 0 && (
247
+ <span className="flex items-center gap-0.5 text-yellow-500">
248
+ <AlertTriangle className="w-3 h-3" />
249
+ {health.warning}
250
+ </span>
251
+ )}
252
+ {health.error > 0 && (
253
+ <span className="flex items-center gap-0.5 text-red-500">
254
+ <XCircle className="w-3 h-3" />
255
+ {health.error}
256
+ </span>
257
+ )}
258
+ </div>
259
+ </button>
260
+ )}
261
+
262
+ {/* Deployments Ring */}
263
+ {isRestricted('deployments') ? (
264
+ <RestrictedRing label="Deployments" />
265
+ ) : (
266
+ <button
267
+ onClick={() => onNavigateToKind('deployments')}
268
+ className="flex flex-col items-center gap-2 hover:-translate-y-1 hover:scale-105 transition-all duration-200"
269
+ >
270
+ <HealthRing segments={deploymentsRingSegments} size={88} strokeWidth={8} label={String(counts.deployments.total)} />
271
+ <span className="text-sm font-semibold uppercase tracking-wider text-theme-text-secondary">Deployments</span>
272
+ <div className="flex items-center gap-2 text-xs font-mono">
273
+ <span className="text-green-500">{counts.deployments.available} available</span>
274
+ {counts.deployments.unavailable > 0 && (
275
+ <span className="text-red-500">{counts.deployments.unavailable} unavailable</span>
276
+ )}
277
+ </div>
278
+ </button>
279
+ )}
280
+
281
+ {/* Nodes Ring */}
282
+ {isRestricted('nodes') ? (
283
+ <RestrictedRing label="Nodes" />
284
+ ) : (
285
+ <button
286
+ onClick={() => onNavigateToKind('nodes')}
287
+ className="flex flex-col items-center gap-2 hover:-translate-y-1 hover:scale-105 transition-all duration-200"
288
+ >
289
+ <HealthRing segments={nodesRingSegments} size={88} strokeWidth={8} label={String(counts.nodes.total)} />
290
+ <span className="text-sm font-semibold uppercase tracking-wider text-theme-text-secondary">Nodes</span>
291
+ <div className="flex items-center gap-2 text-xs font-mono">
292
+ <span className="text-green-500">{counts.nodes.ready} ready</span>
293
+ {cordonedCount > 0 && (
294
+ <span className="text-yellow-500">{cordonedCount} cordoned</span>
295
+ )}
296
+ {counts.nodes.notReady > 0 && (
297
+ <span className="text-red-500">{counts.nodes.notReady} not ready</span>
298
+ )}
299
+ </div>
300
+ </button>
301
+ )}
302
+ </div>
303
+
304
+ {/* Right: Resource utilization */}
305
+ <div className="flex flex-col justify-center w-[300px] shrink-0 pl-8 border-l border-theme-border/50">
306
+ <div className="flex items-center gap-2 mb-3">
307
+ <Boxes className="w-4 h-4 text-theme-text-tertiary" />
308
+ <span className="text-[10px] uppercase tracking-wider text-theme-text-tertiary">Resource Utilization</span>
309
+ </div>
310
+
311
+ <div className="space-y-3">
312
+ {metrics?.cpu && (
313
+ <div className="space-y-2">
314
+ <div className="flex items-center gap-1.5 text-[10px] uppercase tracking-wider text-theme-text-tertiary">
315
+ <Cpu className="w-3.5 h-3.5 text-theme-text-tertiary" />
316
+ CPU
317
+ </div>
318
+ <ResourceBar
319
+ label="Used"
320
+ used={formatCPUMillicores(metrics.cpu.usageMillis)}
321
+ total={formatCPUMillicores(metrics.cpu.capacityMillis)}
322
+ percent={metrics.cpu.usagePercent}
323
+ />
324
+ <ResourceBar
325
+ label="Requested"
326
+ used={formatCPUMillicores(metrics.cpu.requestsMillis)}
327
+ total={formatCPUMillicores(metrics.cpu.capacityMillis)}
328
+ percent={metrics.cpu.requestPercent}
329
+ />
330
+ </div>
331
+ )}
332
+ {metrics?.memory && (
333
+ <div className="space-y-2">
334
+ <div className="flex items-center gap-1.5 text-[10px] uppercase tracking-wider text-theme-text-tertiary">
335
+ <MemoryStick className="w-3.5 h-3.5 text-theme-text-tertiary" />
336
+ Memory
337
+ </div>
338
+ <ResourceBar
339
+ label="Used"
340
+ used={formatMemoryMiB(metrics.memory.usageMillis)}
341
+ total={formatMemoryMiB(metrics.memory.capacityMillis)}
342
+ percent={metrics.memory.usagePercent}
343
+ />
344
+ <ResourceBar
345
+ label="Requested"
346
+ used={formatMemoryMiB(metrics.memory.requestsMillis)}
347
+ total={formatMemoryMiB(metrics.memory.capacityMillis)}
348
+ percent={metrics.memory.requestPercent}
349
+ />
350
+ </div>
351
+ )}
352
+ {!metrics?.cpu && !metrics?.memory && (
353
+ <MetricsUnavailableHint platform={cluster.platform} metricsServerAvailable={metricsServerAvailable} />
354
+ )}
355
+ </div>
356
+
357
+ </div>
358
+ </div>
359
+ </div>
360
+
361
+ {/* Secondary resources row — matches top row's 3-column layout */}
362
+ <div className="flex items-stretch px-6 py-2.5 bg-theme-surface/30">
363
+ {/* Left column: Warning indicators (aligned with cluster info) */}
364
+ <div className="flex flex-col justify-center gap-1 w-1/4 shrink-0 pr-4 border-r border-theme-border/50">
365
+ {health.warningEvents > 0 && (
366
+ <button
367
+ onClick={onWarningEventsClick}
368
+ title="Native Kubernetes Warning events (e.g., ImagePullBackOff, FailedScheduling)"
369
+ className="badge status-degraded w-fit gap-1.5 hover:opacity-80 transition-opacity"
370
+ >
371
+ <AlertTriangle className="w-3.5 h-3.5 shrink-0" />
372
+ <span><span className="font-mono">{health.warningEvents}</span> Warning Events</span>
373
+ </button>
374
+ )}
375
+ {problems.length > 0 && (
376
+ <button
377
+ onClick={onUnhealthyClick}
378
+ title="View timeline of unhealthy/degraded workload events"
379
+ className="badge status-unhealthy w-fit gap-1.5 hover:opacity-80 transition-opacity"
380
+ >
381
+ <AlertTriangle className="w-3.5 h-3.5 shrink-0" />
382
+ <span>View unhealthy workload events</span>
383
+ </button>
384
+ )}
385
+ </div>
386
+
387
+ {/* Center column: Resources (aligned with health rings) */}
388
+ <div className="w-1/2 grid grid-cols-3 items-center justify-items-center px-4">
389
+ {secondaryResources.map((res) => (
390
+ <button
391
+ key={res.kind}
392
+ onClick={() => onNavigateToKind(res.kind, res.group)}
393
+ className="flex items-center gap-1.5 px-2 py-1 rounded hover:bg-theme-hover transition-colors text-sm whitespace-nowrap"
394
+ >
395
+ {isRestricted(res.kind) ? (
396
+ <>
397
+ <Shield className="w-3.5 h-3.5 text-amber-400/60" />
398
+ <span className="text-theme-text-disabled">{res.label}</span>
399
+ </>
400
+ ) : (
401
+ <>
402
+ <res.icon className={clsx('w-3.5 h-3.5', res.hasIssues ? 'text-yellow-500' : 'text-theme-text-tertiary')} />
403
+ <span className="text-theme-text-primary font-medium font-mono">{res.total}</span>
404
+ <span className="text-theme-text-secondary">{res.label}</span>
405
+ </>
406
+ )}
407
+ </button>
408
+ ))}
409
+ </div>
410
+
411
+ {/* Right column: Browse All (aligned with resource utilization) */}
412
+ <div className="flex items-center justify-center w-1/4 shrink-0 pl-4 border-l border-theme-border/50">
413
+ <button
414
+ onClick={onNavigateToView}
415
+ className="flex items-center gap-2 text-base font-medium text-theme-text-secondary hover:text-theme-text-primary transition-colors"
416
+ >
417
+ Browse All Resources
418
+ <ArrowRight className="w-5 h-5" />
419
+ </button>
420
+ </div>
421
+ </div>
422
+ </div>
423
+ )
424
+ }
425
+
426
+ function RestrictedRing({ label }: { label: string }) {
427
+ const radius = 36
428
+ const circumference = 2 * Math.PI * radius
429
+ const arcLength = 0.75 * circumference
430
+ const gapLength = circumference - arcLength
431
+ return (
432
+ <div className="flex flex-col items-center gap-2">
433
+ <div className="relative w-[88px] h-[88px] flex items-center justify-center">
434
+ <svg width={88} height={88} viewBox="0 0 88 88" className="absolute inset-0">
435
+ <circle
436
+ cx={44}
437
+ cy={44}
438
+ r={radius}
439
+ fill="none"
440
+ stroke="currentColor"
441
+ strokeWidth={8}
442
+ strokeDasharray={`6 4 ${arcLength - 10} ${gapLength + 10}`}
443
+ strokeLinecap="round"
444
+ transform="rotate(135 44 44)"
445
+ className="text-theme-border"
446
+ />
447
+ </svg>
448
+ <Shield className="w-6 h-6 text-amber-400" />
449
+ </div>
450
+ <span className="text-xs font-semibold uppercase tracking-wider text-theme-text-secondary">{label}</span>
451
+ <span className="text-[11px] text-amber-400">Restricted</span>
452
+ </div>
453
+ )
454
+ }
455
+
456
+ function ResourceBar({
457
+ label,
458
+ used,
459
+ total,
460
+ percent,
461
+ }: {
462
+ label: string
463
+ used: string
464
+ total: string
465
+ percent: number
466
+ }) {
467
+ const barColor = percent > 85 ? 'bg-red-500' : percent > 60 ? 'bg-yellow-500' : 'bg-green-500'
468
+
469
+ return (
470
+ <div>
471
+ <div className="flex justify-between items-baseline mb-0.5">
472
+ <span className="text-[10px] text-theme-text-tertiary font-mono">{label}: {used} / {total}</span>
473
+ <span className="text-[10px] font-medium text-theme-text-secondary font-mono">{percent}%</span>
474
+ </div>
475
+ <div className="h-2 bg-theme-border rounded overflow-hidden">
476
+ <div
477
+ className={clsx('h-full transition-all', barColor)}
478
+ style={{ width: `${Math.min(percent, 100)}%` }}
479
+ />
480
+ </div>
481
+ </div>
482
+ )
483
+ }