@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,687 @@
1
+ import { useState, useMemo, useRef, useCallback } from 'react'
2
+ import { clsx } from 'clsx'
3
+ import { BarChart3, Wifi, WifiOff, Loader2 } from 'lucide-react'
4
+ import {
5
+ usePrometheusStatus,
6
+ usePrometheusConnect,
7
+ usePrometheusResourceMetrics,
8
+ type PrometheusMetricCategory,
9
+ type PrometheusTimeRange,
10
+ type PrometheusSeries,
11
+ } from '../../api/client'
12
+
13
+ // ============================================================================
14
+ // Types & Constants
15
+ // ============================================================================
16
+
17
+ const SUPPORTED_KINDS = new Set([
18
+ 'Pod', 'Deployment', 'StatefulSet', 'DaemonSet', 'ReplicaSet', 'Job', 'CronJob', 'Node',
19
+ ])
20
+
21
+ interface CategoryDef {
22
+ key: PrometheusMetricCategory
23
+ label: string
24
+ color: string // tailwind text class
25
+ chartColor: string // hex for SVG
26
+ fillColor: string // hex with alpha for SVG fill
27
+ }
28
+
29
+ const WORKLOAD_CATEGORIES: CategoryDef[] = [
30
+ { key: 'cpu', label: 'CPU', color: 'text-blue-400', chartColor: '#60a5fa', fillColor: '#60a5fa22' },
31
+ { key: 'memory', label: 'Memory', color: 'text-purple-400', chartColor: '#c084fc', fillColor: '#c084fc22' },
32
+ { key: 'network_rx', label: 'Net RX', color: 'text-emerald-400', chartColor: '#34d399', fillColor: '#34d39922' },
33
+ { key: 'network_tx', label: 'Net TX', color: 'text-orange-400', chartColor: '#fb923c', fillColor: '#fb923c22' },
34
+ { key: 'filesystem', label: 'Disk I/O', color: 'text-amber-400', chartColor: '#fbbf24', fillColor: '#fbbf2422' },
35
+ ]
36
+
37
+ const NODE_CATEGORIES: CategoryDef[] = [
38
+ { key: 'cpu', label: 'CPU', color: 'text-blue-400', chartColor: '#60a5fa', fillColor: '#60a5fa22' },
39
+ { key: 'memory', label: 'Memory', color: 'text-purple-400', chartColor: '#c084fc', fillColor: '#c084fc22' },
40
+ { key: 'filesystem', label: 'Disk', color: 'text-amber-400', chartColor: '#fbbf24', fillColor: '#fbbf2422' },
41
+ ]
42
+
43
+ // Distinct colors for multi-series charts (up to 10 series).
44
+ // Uses 500-level shades for adequate contrast on both dark (#1e293b) and light (#ffffff) surfaces.
45
+ const SERIES_COLORS = [
46
+ '#3b82f6', // blue-500
47
+ '#10b981', // emerald-500
48
+ '#f97316', // orange-500
49
+ '#a855f7', // purple-500
50
+ '#ec4899', // pink-500
51
+ '#eab308', // yellow-500
52
+ '#06b6d4', // cyan-500
53
+ '#84cc16', // lime-500
54
+ '#ef4444', // red-500
55
+ '#6366f1', // indigo-500
56
+ ]
57
+
58
+ const TIME_RANGES: { value: PrometheusTimeRange; label: string }[] = [
59
+ { value: '10m', label: '10m' },
60
+ { value: '30m', label: '30m' },
61
+ { value: '1h', label: '1h' },
62
+ { value: '3h', label: '3h' },
63
+ { value: '6h', label: '6h' },
64
+ { value: '12h', label: '12h' },
65
+ { value: '24h', label: '24h' },
66
+ { value: '7d', label: '7d' },
67
+ ]
68
+
69
+ // ============================================================================
70
+ // Main Component
71
+ // ============================================================================
72
+
73
+ interface PrometheusChartsProps {
74
+ kind: string
75
+ namespace: string
76
+ name: string
77
+ /** When true, show "no data" empty state instead of hiding. Defaults to false (hide when no data). */
78
+ showEmptyState?: boolean
79
+ }
80
+
81
+ export function PrometheusCharts({ kind, namespace, name, showEmptyState = false }: PrometheusChartsProps) {
82
+ const { data: status, isLoading: statusLoading } = usePrometheusStatus()
83
+ const connectMutation = usePrometheusConnect()
84
+
85
+ const categories = kind === 'Node' ? NODE_CATEGORIES : WORKLOAD_CATEGORIES
86
+ const [activeCategory, setActiveCategory] = useState<PrometheusMetricCategory>('cpu')
87
+ const [timeRange, setTimeRange] = useState<PrometheusTimeRange>('1h')
88
+
89
+ const isConnected = status?.connected === true
90
+ const isSupported = SUPPORTED_KINDS.has(kind)
91
+
92
+ // Fetch metrics when connected
93
+ const { data: metrics, isLoading: metricsLoading, error: metricsError } = usePrometheusResourceMetrics(
94
+ kind, namespace, name, activeCategory, timeRange,
95
+ isConnected && isSupported,
96
+ )
97
+
98
+ if (!isSupported) {
99
+ return null
100
+ }
101
+
102
+ // Loading state — checking Prometheus availability (only show when explicitly requested)
103
+ if (statusLoading) {
104
+ if (!showEmptyState) return null
105
+ return (
106
+ <div className="flex items-center justify-center py-12 text-theme-text-tertiary">
107
+ <Loader2 className="w-5 h-5 animate-spin mr-2" />
108
+ Checking Prometheus availability...
109
+ </div>
110
+ )
111
+ }
112
+
113
+ // When embedded in Overview (showEmptyState=false), hide when not connected or no data
114
+ if (!showEmptyState) {
115
+ if (!isConnected) return null
116
+ if (!metricsLoading && !metricsError && !metrics?.result?.series?.length) return null
117
+ }
118
+
119
+ if (!isConnected) {
120
+ return (
121
+ <div className="flex flex-col items-center justify-center py-12 gap-4">
122
+ <WifiOff className="w-10 h-10 text-theme-text-quaternary" />
123
+ <div className="text-center">
124
+ <p className="text-sm text-theme-text-secondary mb-1">Prometheus not connected</p>
125
+ <p className="text-xs text-theme-text-tertiary mb-4">
126
+ {status?.error || 'Connect to view historical CPU, memory, and network metrics'}
127
+ </p>
128
+ <button
129
+ onClick={() => connectMutation.mutate()}
130
+ disabled={connectMutation.isPending}
131
+ className="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg btn-brand"
132
+ >
133
+ {connectMutation.isPending ? (
134
+ <Loader2 className="w-4 h-4 animate-spin" />
135
+ ) : (
136
+ <Wifi className="w-4 h-4" />
137
+ )}
138
+ Discover Prometheus
139
+ </button>
140
+ </div>
141
+ </div>
142
+ )
143
+ }
144
+
145
+ const activeCategoryDef = categories.find(c => c.key === activeCategory) || categories[0]
146
+
147
+ return (
148
+ <div className="flex flex-col h-full">
149
+ {/* Toolbar */}
150
+ <div className="shrink-0 flex items-center justify-between px-4 py-2.5 border-b border-theme-border bg-theme-surface/50">
151
+ {/* Category tabs */}
152
+ <div className="flex items-center gap-1">
153
+ <BarChart3 className="w-4 h-4 text-theme-text-tertiary mr-2" />
154
+ {categories.map(cat => (
155
+ <button
156
+ key={cat.key}
157
+ onClick={() => setActiveCategory(cat.key)}
158
+ className={clsx(
159
+ 'px-2.5 py-1 text-xs font-medium rounded-md transition-colors',
160
+ activeCategory === cat.key
161
+ ? 'bg-theme-elevated text-theme-text-primary shadow-sm'
162
+ : 'text-theme-text-tertiary hover:text-theme-text-secondary hover:bg-theme-elevated/50'
163
+ )}
164
+ >
165
+ {cat.label}
166
+ </button>
167
+ ))}
168
+ </div>
169
+
170
+ {/* Time range selector */}
171
+ <select
172
+ value={timeRange}
173
+ onChange={e => setTimeRange(e.target.value as PrometheusTimeRange)}
174
+ className="px-2 py-1 text-xs rounded-md bg-theme-elevated border border-theme-border text-theme-text-secondary focus:outline-none focus:ring-1 focus:ring-blue-500/50"
175
+ >
176
+ {TIME_RANGES.map(tr => (
177
+ <option key={tr.value} value={tr.value}>{tr.label}</option>
178
+ ))}
179
+ </select>
180
+ </div>
181
+
182
+ {/* Chart area — fixed min-height prevents layout shift while loading */}
183
+ <div className="min-h-[280px] p-4">
184
+ {metricsLoading ? (
185
+ <div className="flex items-center justify-center min-h-[240px] text-theme-text-tertiary">
186
+ <Loader2 className="w-5 h-5 animate-spin mr-2" />
187
+ Loading metrics...
188
+ </div>
189
+ ) : metricsError ? (
190
+ <div className="flex items-center justify-center h-full text-red-400 text-sm">
191
+ Failed to load metrics: {(metricsError as Error).message}
192
+ </div>
193
+ ) : metrics?.result?.series?.length ? (
194
+ <div className="h-full flex flex-col gap-4">
195
+ {/* Summary stats */}
196
+ <MetricsSummary
197
+ series={metrics.result.series}
198
+ category={activeCategoryDef}
199
+ unit={metrics.unit}
200
+ />
201
+
202
+ {/* Main chart */}
203
+ <div className="flex-1 min-h-0">
204
+ <AreaChart
205
+ series={metrics.result.series}
206
+ color={activeCategoryDef.chartColor}
207
+ fillColor={activeCategoryDef.fillColor}
208
+ unit={metrics.unit}
209
+ />
210
+ </div>
211
+
212
+ {/* Per-pod legend for workload-level queries */}
213
+ {metrics.result.series.length > 1 && (
214
+ <SeriesLegend series={metrics.result.series} color={activeCategoryDef.chartColor} />
215
+ )}
216
+ </div>
217
+ ) : (
218
+ <div className="flex flex-col items-center justify-center h-full text-theme-text-tertiary">
219
+ <BarChart3 className="w-8 h-8 mb-2 opacity-40" />
220
+ <p className="text-sm">No data for this time range</p>
221
+ <p className="text-xs text-theme-text-quaternary mt-1">
222
+ Try a different time range or check that metrics are being collected
223
+ </p>
224
+ {metrics?.hint && (
225
+ <p className="mt-3 px-3 py-2 w-full max-w-lg text-xs text-yellow-700 dark:text-yellow-400 bg-yellow-500/10 border border-yellow-500/30 rounded">
226
+ {metrics.hint}
227
+ </p>
228
+ )}
229
+ {metrics?.query && (
230
+ <details className="mt-3 w-full max-w-lg text-left">
231
+ <summary className="text-xs text-theme-text-quaternary cursor-pointer hover:text-theme-text-tertiary">
232
+ Diagnostics: show PromQL query
233
+ </summary>
234
+ <div className="mt-2 p-2 bg-theme-base border border-theme-border rounded text-xs font-mono text-theme-text-secondary break-all">
235
+ {metrics.query}
236
+ </div>
237
+ <p className="mt-1.5 text-xs text-theme-text-quaternary">
238
+ This query returned no results. Verify in your Prometheus UI that the metric names and labels
239
+ ({activeCategoryDef.key === 'cpu' ? 'pod, namespace, container' : 'pod, namespace'}) exist.
240
+ Custom label relabeling in your Prometheus configuration may require adjustments.
241
+ </p>
242
+ </details>
243
+ )}
244
+ </div>
245
+ )}
246
+ </div>
247
+ </div>
248
+ )
249
+ }
250
+
251
+ // ============================================================================
252
+ // Sub-Components
253
+ // ============================================================================
254
+
255
+ function MetricsSummary({ series, category, unit }: {
256
+ series: PrometheusSeries[]
257
+ category: CategoryDef
258
+ unit: string
259
+ }) {
260
+ const stats = useMemo(() => {
261
+ // Aggregate all data points across series
262
+ const allValues: number[] = []
263
+ for (const s of series) {
264
+ for (const dp of s.dataPoints) {
265
+ allValues.push(dp.value)
266
+ }
267
+ }
268
+ if (allValues.length === 0) return null
269
+
270
+ // Latest = sum of each series' most recent data point
271
+ const lastValues = series.map(s => s.dataPoints[s.dataPoints.length - 1]?.value ?? 0)
272
+ const current = lastValues.reduce((a, b) => a + b, 0)
273
+ const max = Math.max(...allValues)
274
+ const avg = allValues.reduce((a, b) => a + b, 0) / allValues.length
275
+
276
+ return { current, max, avg }
277
+ }, [series])
278
+
279
+ if (!stats) return null
280
+
281
+ return (
282
+ <div className="flex items-center gap-6">
283
+ <StatPill label="Current" value={formatMetricValue(stats.current, unit)} className={category.color} />
284
+ <StatPill label="Average" value={formatMetricValue(stats.avg, unit)} className="text-theme-text-secondary" />
285
+ <StatPill label="Peak" value={formatMetricValue(stats.max, unit)} className="text-theme-text-secondary" />
286
+ </div>
287
+ )
288
+ }
289
+
290
+ function StatPill({ label, value, className }: { label: string; value: string; className?: string }) {
291
+ return (
292
+ <div className="flex items-baseline gap-1.5">
293
+ <span className="text-xs text-theme-text-quaternary uppercase tracking-wide">{label}</span>
294
+ <span className={clsx('text-sm font-semibold tabular-nums', className)}>{value}</span>
295
+ </div>
296
+ )
297
+ }
298
+
299
+ // ============================================================================
300
+ // Area Chart (pure SVG, no dependencies)
301
+ // ============================================================================
302
+
303
+ function seriesColor(index: number, fallback: string): string {
304
+ return SERIES_COLORS[index % SERIES_COLORS.length] ?? fallback
305
+ }
306
+
307
+ function seriesFill(index: number, fallback: string): string {
308
+ return (SERIES_COLORS[index % SERIES_COLORS.length] ?? fallback) + '22'
309
+ }
310
+
311
+ // Compute short labels that strip the shared prefix so pods are distinguishable.
312
+ // e.g. ["backend-podinfo-849bd668f9-4tzkg", "backend-podinfo-849bd668f9-5z79f"] → ["4tzkg", "5z79f"]
313
+ function computeShortLabels(labels: string[]): string[] {
314
+ if (labels.length <= 1) return labels
315
+ // Find longest common prefix
316
+ let prefix = labels[0]
317
+ for (let i = 1; i < labels.length; i++) {
318
+ while (!labels[i].startsWith(prefix)) {
319
+ prefix = prefix.slice(0, -1)
320
+ }
321
+ }
322
+ // Trim to last separator (- or /) for cleaner cuts
323
+ const lastSep = Math.max(prefix.lastIndexOf('-'), prefix.lastIndexOf('/'))
324
+ if (lastSep > 0) prefix = prefix.slice(0, lastSep + 1)
325
+
326
+ const suffixes = labels.map(l => l.slice(prefix.length))
327
+ // If stripping made them empty or all the same, fall back to originals
328
+ if (suffixes.some(s => s === '') || new Set(suffixes).size !== suffixes.length) return labels
329
+ return suffixes
330
+ }
331
+
332
+ function AreaChart({ series, color, fillColor, unit }: {
333
+ series: PrometheusSeries[]
334
+ color: string
335
+ fillColor: string
336
+ unit: string
337
+ }) {
338
+ const svgRef = useRef<SVGSVGElement>(null)
339
+ const [hoverX, setHoverX] = useState<number | null>(null)
340
+ const multiSeries = series.length > 1
341
+
342
+ const chartData = useMemo(() => {
343
+ if (!series.length) return null
344
+
345
+ // Merge all series into a single timeline for the X axis
346
+ let minTs = Infinity
347
+ let maxTs = -Infinity
348
+ let maxVal = 0
349
+
350
+ for (const s of series) {
351
+ for (const dp of s.dataPoints) {
352
+ if (dp.timestamp < minTs) minTs = dp.timestamp
353
+ if (dp.timestamp > maxTs) maxTs = dp.timestamp
354
+ if (dp.value > maxVal) maxVal = dp.value
355
+ }
356
+ }
357
+
358
+ if (minTs === maxTs) maxTs = minTs + 60
359
+ if (maxVal === 0) {
360
+ // Use a small unit-appropriate default so the Y-axis isn't misleadingly large
361
+ maxVal = unit === 'cores' ? 0.01 : unit === 'bytes' ? 1024 * 1024 : unit === 'bytes/s' ? 1024 : 1
362
+ }
363
+
364
+ const padding = maxVal * 0.1
365
+ const yMax = maxVal + padding
366
+
367
+ return { minTs, maxTs, yMax, series }
368
+ }, [series, unit])
369
+
370
+ if (!chartData) return null
371
+
372
+ const { minTs, maxTs, yMax } = chartData
373
+ const width = 1000
374
+ const height = 300
375
+ const marginLeft = 60
376
+ const marginRight = 20
377
+ const marginTop = 10
378
+ const marginBottom = 30
379
+ const plotWidth = width - marginLeft - marginRight
380
+ const plotHeight = height - marginTop - marginBottom
381
+
382
+ const toX = (ts: number) => marginLeft + ((ts - minTs) / (maxTs - minTs)) * plotWidth
383
+ const toY = (val: number) => marginTop + plotHeight - (val / yMax) * plotHeight
384
+
385
+ // Y axis ticks
386
+ const yTicks = useMemo(() => {
387
+ const count = 4
388
+ return Array.from({ length: count + 1 }, (_, i) => {
389
+ const val = (yMax / count) * i
390
+ return { val, y: toY(val), label: formatMetricValue(val, unit) }
391
+ })
392
+ }, [yMax, unit])
393
+
394
+ // X axis ticks
395
+ const xTicks = useMemo(() => {
396
+ const count = 6
397
+ return Array.from({ length: count + 1 }, (_, i) => {
398
+ const ts = minTs + ((maxTs - minTs) / count) * i
399
+ return { ts, x: toX(ts), label: formatTimestamp(ts) }
400
+ })
401
+ }, [minTs, maxTs])
402
+
403
+ // Build paths for each series
404
+ const paths = useMemo(() => {
405
+ return chartData.series.map((s, seriesIdx) => {
406
+ if (s.dataPoints.length < 2) return null
407
+ const points = s.dataPoints.map(dp => ({ x: toX(dp.timestamp), y: toY(dp.value) }))
408
+
409
+ const linePath = points.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x},${p.y}`).join(' ')
410
+
411
+ // Area path: line + close to bottom
412
+ const areaPath = linePath +
413
+ ` L${points[points.length - 1].x},${marginTop + plotHeight}` +
414
+ ` L${points[0].x},${marginTop + plotHeight} Z`
415
+
416
+ return {
417
+ linePath,
418
+ areaPath,
419
+ strokeColor: multiSeries ? seriesColor(seriesIdx, color) : color,
420
+ areaFillColor: multiSeries ? seriesFill(seriesIdx, fillColor) : fillColor,
421
+ key: seriesIdx,
422
+ }
423
+ }).filter(Boolean)
424
+ }, [chartData])
425
+
426
+ // Hover data: find nearest data point per series at the hovered X position
427
+ const hoverData = useMemo(() => {
428
+ if (hoverX === null) return null
429
+ const clampedX = Math.max(marginLeft, Math.min(marginLeft + plotWidth, hoverX))
430
+ const frac = (clampedX - marginLeft) / plotWidth
431
+ const ts = minTs + frac * (maxTs - minTs)
432
+
433
+ const validSeries = chartData.series
434
+ .map((s, i) => ({ s, i }))
435
+ .filter(({ s }) => s.dataPoints.length >= 2)
436
+
437
+ const fullLabels = validSeries.map(({ s, i }) =>
438
+ s.labels.pod || s.labels.instance || s.labels.node || `series-${i}`
439
+ )
440
+ const shortLabels = computeShortLabels(fullLabels)
441
+
442
+ const points = validSeries.map(({ s, i }, vi) => {
443
+ let closest = s.dataPoints[0]
444
+ let closestDist = Infinity
445
+ for (const dp of s.dataPoints) {
446
+ const dist = Math.abs(dp.timestamp - ts)
447
+ if (dist < closestDist) {
448
+ closestDist = dist
449
+ closest = dp
450
+ }
451
+ }
452
+ return {
453
+ label: shortLabels[vi],
454
+ fullLabel: fullLabels[vi],
455
+ value: closest.value,
456
+ y: toY(closest.value),
457
+ color: multiSeries ? seriesColor(i, color) : color,
458
+ }
459
+ })
460
+
461
+ return { ts, x: clampedX, points }
462
+ }, [hoverX, chartData])
463
+
464
+ // Convert client mouse coordinates to SVG viewBox coordinates
465
+ const handleMouseMove = useCallback((e: React.MouseEvent<SVGRectElement>) => {
466
+ const svg = svgRef.current
467
+ if (!svg) return
468
+ const ctm = svg.getScreenCTM()
469
+ if (!ctm) return
470
+ setHoverX((e.clientX - ctm.e) / ctm.a)
471
+ }, [])
472
+
473
+ return (
474
+ <div className="relative">
475
+ <svg
476
+ ref={svgRef}
477
+ viewBox={`0 0 ${width} ${height}`}
478
+ className="w-full h-full"
479
+ preserveAspectRatio="xMidYMid meet"
480
+ >
481
+ {/* Grid lines */}
482
+ {yTicks.map((tick, i) => (
483
+ <line
484
+ key={`grid-${i}`}
485
+ x1={marginLeft}
486
+ y1={tick.y}
487
+ x2={width - marginRight}
488
+ y2={tick.y}
489
+ stroke="currentColor"
490
+ className="text-theme-border/30"
491
+ strokeWidth="1"
492
+ strokeDasharray={i === 0 ? undefined : '4 4'}
493
+ />
494
+ ))}
495
+
496
+ {/* Y axis labels */}
497
+ {yTicks.map((tick, i) => (
498
+ <text
499
+ key={`ylabel-${i}`}
500
+ x={marginLeft - 8}
501
+ y={tick.y + 4}
502
+ textAnchor="end"
503
+ className="fill-theme-text-secondary"
504
+ fontSize="11"
505
+ fontFamily="ui-monospace, monospace"
506
+ >
507
+ {tick.label}
508
+ </text>
509
+ ))}
510
+
511
+ {/* X axis labels */}
512
+ {xTicks.map((tick, i) => (
513
+ <text
514
+ key={`xlabel-${i}`}
515
+ x={tick.x}
516
+ y={height - 4}
517
+ textAnchor="middle"
518
+ className="fill-theme-text-secondary"
519
+ fontSize="11"
520
+ fontFamily="ui-monospace, monospace"
521
+ >
522
+ {tick.label}
523
+ </text>
524
+ ))}
525
+
526
+ {/* Area fills */}
527
+ {paths.map(p => p && (
528
+ <path
529
+ key={`area-${p.key}`}
530
+ d={p.areaPath}
531
+ fill={p.areaFillColor}
532
+ />
533
+ ))}
534
+
535
+ {/* Lines */}
536
+ {paths.map(p => p && (
537
+ <path
538
+ key={`line-${p.key}`}
539
+ d={p.linePath}
540
+ fill="none"
541
+ stroke={p.strokeColor}
542
+ strokeWidth="2"
543
+ strokeLinejoin="round"
544
+ />
545
+ ))}
546
+
547
+ {/* Hover crosshair + dots */}
548
+ {hoverData && (
549
+ <>
550
+ <line
551
+ x1={hoverData.x} y1={marginTop}
552
+ x2={hoverData.x} y2={marginTop + plotHeight}
553
+ stroke="currentColor"
554
+ className="text-theme-text-tertiary"
555
+ strokeWidth="1"
556
+ strokeDasharray="4 4"
557
+ />
558
+ {hoverData.points.map((p, i) => (
559
+ <circle
560
+ key={i}
561
+ cx={hoverData.x} cy={p.y}
562
+ r="4"
563
+ fill={p.color}
564
+ stroke="var(--color-theme-surface, #1a1a2e)"
565
+ strokeWidth="2"
566
+ />
567
+ ))}
568
+ </>
569
+ )}
570
+
571
+ {/* Invisible overlay for mouse events — must be last for event capture */}
572
+ <rect
573
+ x={marginLeft} y={marginTop}
574
+ width={plotWidth} height={plotHeight}
575
+ fill="transparent"
576
+ style={{ cursor: 'crosshair' }}
577
+ onMouseMove={handleMouseMove}
578
+ onMouseLeave={() => setHoverX(null)}
579
+ />
580
+ </svg>
581
+
582
+ {/* Tooltip positioned outside SVG for proper HTML rendering */}
583
+ {hoverData && (
584
+ <div
585
+ className="absolute top-0 pointer-events-none z-10"
586
+ style={{
587
+ left: `${(hoverData.x / width) * 100}%`,
588
+ transform: hoverData.x > width * 0.65 ? 'translateX(calc(-100% - 12px))' : 'translateX(12px)',
589
+ }}
590
+ >
591
+ <div className="bg-theme-surface border border-theme-border rounded-lg shadow-lg px-3 py-2 text-xs whitespace-nowrap">
592
+ <div className="text-theme-text-tertiary mb-1.5 font-mono">
593
+ {new Date(hoverData.ts * 1000).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })}
594
+ </div>
595
+ {hoverData.points.map((p, i) => (
596
+ <div key={i} className="flex items-center gap-2 py-0.5">
597
+ <div
598
+ className="w-2 h-2 rounded-full shrink-0"
599
+ style={{ backgroundColor: p.color }}
600
+ />
601
+ <span className="text-theme-text-secondary font-mono" title={p.fullLabel}>
602
+ {p.label}
603
+ </span>
604
+ <span className="text-theme-text-primary font-semibold ml-auto pl-3 tabular-nums">
605
+ {formatMetricValue(p.value, unit)}
606
+ </span>
607
+ </div>
608
+ ))}
609
+ </div>
610
+ </div>
611
+ )}
612
+ </div>
613
+ )
614
+ }
615
+
616
+ function SeriesLegend({ series, color }: { series: PrometheusSeries[]; color: string }) {
617
+ const labels = series.map((s, i) => s.labels.pod || s.labels.instance || `series-${i}`)
618
+ return (
619
+ <div className="flex flex-wrap gap-x-4 gap-y-1 px-1">
620
+ {series.slice(0, 10).map((_, i) => {
621
+ const shortName = labels[i].length > 40 ? '...' + labels[i].slice(-37) : labels[i]
622
+ return (
623
+ <div key={i} className="flex items-center gap-1.5 text-xs text-theme-text-tertiary">
624
+ <div
625
+ className="w-2.5 h-2.5 rounded-full shrink-0"
626
+ style={{ backgroundColor: seriesColor(i, color) }}
627
+ />
628
+ <span className="truncate" title={labels[i]}>{shortName}</span>
629
+ </div>
630
+ )
631
+ })}
632
+ {series.length > 10 && (
633
+ <span className="text-xs text-theme-text-quaternary">+{series.length - 10} more</span>
634
+ )}
635
+ </div>
636
+ )
637
+ }
638
+
639
+ // ============================================================================
640
+ // Formatters
641
+ // ============================================================================
642
+
643
+ function formatMetricValue(value: number, unit: string): string {
644
+ if (value === 0) return '0'
645
+
646
+ switch (unit) {
647
+ case 'cores': {
648
+ if (value < 0.0001) return '< 0.1m'
649
+ if (value < 0.001) return `${(value * 1000).toFixed(1)}m`
650
+ if (value < 1) return `${(value * 1000).toFixed(0)}m`
651
+ return `${value.toFixed(2)}`
652
+ }
653
+ case 'bytes': {
654
+ if (value < 1) return '< 1 B'
655
+ if (value < 1024) return `${value.toFixed(0)} B`
656
+ if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KiB`
657
+ if (value < 1024 * 1024 * 1024) return `${(value / (1024 * 1024)).toFixed(1)} MiB`
658
+ return `${(value / (1024 * 1024 * 1024)).toFixed(2)} GiB`
659
+ }
660
+ case 'bytes/s': {
661
+ if (value < 1) return '< 1 B/s'
662
+ if (value < 1024) return `${value.toFixed(0)} B/s`
663
+ if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KiB/s`
664
+ if (value < 1024 * 1024 * 1024) return `${(value / (1024 * 1024)).toFixed(1)} MiB/s`
665
+ return `${(value / (1024 * 1024 * 1024)).toFixed(2)} GiB/s`
666
+ }
667
+ default:
668
+ if (value < 0.01) return value.toExponential(1)
669
+ if (value < 1) return value.toFixed(3)
670
+ if (value < 100) return value.toFixed(2)
671
+ if (value < 10000) return value.toFixed(0)
672
+ return `${(value / 1000).toFixed(1)}k`
673
+ }
674
+ }
675
+
676
+ function formatTimestamp(unix: number): string {
677
+ const d = new Date(unix * 1000)
678
+ return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
679
+ }
680
+
681
+ // ============================================================================
682
+ // Export helper to check if a kind is supported
683
+ // ============================================================================
684
+
685
+ export function isPrometheusSupported(kind: string): boolean {
686
+ return SUPPORTED_KINDS.has(kind)
687
+ }