@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.
- package/README.md +67 -0
- package/package.json +80 -0
- package/src/App.tsx +1538 -0
- package/src/RadarApp.tsx +145 -0
- package/src/api/apiResources.ts +28 -0
- package/src/api/client.ts +2583 -0
- package/src/api/config.ts +116 -0
- package/src/api/traffic.ts +139 -0
- package/src/components/ConnectionErrorView.tsx +272 -0
- package/src/components/ContextSwitcher.tsx +481 -0
- package/src/components/DebugOverlay.tsx +94 -0
- package/src/components/UserMenu.tsx +87 -0
- package/src/components/audit/AuditSettingsDialog.tsx +162 -0
- package/src/components/audit/AuditView.tsx +123 -0
- package/src/components/cost/CostTrendChart.tsx +388 -0
- package/src/components/cost/CostView.tsx +545 -0
- package/src/components/dock/BottomDock.tsx +96 -0
- package/src/components/dock/DockContext.tsx +11 -0
- package/src/components/dock/LocalTerminalTab.tsx +22 -0
- package/src/components/dock/LogsTab.tsx +26 -0
- package/src/components/dock/NodeTerminalTab.tsx +50 -0
- package/src/components/dock/TerminalTab.tsx +42 -0
- package/src/components/dock/TrafficFlowListTab.tsx +18 -0
- package/src/components/dock/WorkloadLogsTab.tsx +23 -0
- package/src/components/dock/index.ts +2 -0
- package/src/components/gitops/GitOpsActions.tsx +1 -0
- package/src/components/gitops/GitOpsStatusBadge.tsx +1 -0
- package/src/components/gitops/ManagedResourcesList.tsx +1 -0
- package/src/components/gitops/SyncCountdown.tsx +1 -0
- package/src/components/gitops/index.ts +4 -0
- package/src/components/helm/ChartBrowser.tsx +580 -0
- package/src/components/helm/HelmReleaseDrawer.tsx +774 -0
- package/src/components/helm/HelmView.tsx +475 -0
- package/src/components/helm/InstallWizard.tsx +1060 -0
- package/src/components/helm/ManifestDiffViewer.tsx +91 -0
- package/src/components/helm/ManifestViewer.tsx +61 -0
- package/src/components/helm/OwnedResources.tsx +465 -0
- package/src/components/helm/RevisionHistory.tsx +167 -0
- package/src/components/helm/ValuesDiffPreview.tsx +190 -0
- package/src/components/helm/ValuesViewer.tsx +365 -0
- package/src/components/helm/helm-utils.ts +37 -0
- package/src/components/home/ActivitySummary.tsx +262 -0
- package/src/components/home/CertificateHealthCard.tsx +105 -0
- package/src/components/home/ClusterHealthCard.tsx +483 -0
- package/src/components/home/CostCard.tsx +112 -0
- package/src/components/home/HealthRing.tsx +1 -0
- package/src/components/home/HelmSummary.tsx +129 -0
- package/src/components/home/HomeView.tsx +224 -0
- package/src/components/home/MCPSetupDialog.tsx +417 -0
- package/src/components/home/NetworkPolicyCoverageCard.tsx +109 -0
- package/src/components/home/TopologyPreview.tsx +219 -0
- package/src/components/home/TrafficSummary.tsx +154 -0
- package/src/components/logs/JsonLogLine.tsx +1 -0
- package/src/components/logs/LogCore.tsx +2 -0
- package/src/components/logs/LogsViewer.tsx +44 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +40 -0
- package/src/components/logs/useLogBuffer.ts +2 -0
- package/src/components/logs/useLogSearch.ts +1 -0
- package/src/components/portforward/PortForwardButton.tsx +375 -0
- package/src/components/portforward/PortForwardManager.tsx +871 -0
- package/src/components/resource/PrometheusCharts.tsx +687 -0
- package/src/components/resource-drawer/ResourceDrawer.tsx +214 -0
- package/src/components/resources/ImageFilesystemModal.tsx +745 -0
- package/src/components/resources/PodFilesystemModal.tsx +407 -0
- package/src/components/resources/ResourceDetailDrawer.tsx +43 -0
- package/src/components/resources/ResourcesView.tsx +190 -0
- package/src/components/resources/drawer-components.tsx +1 -0
- package/src/components/resources/file-browser-utils.ts +35 -0
- package/src/components/resources/renderers/AlertRenderer.tsx +1 -0
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +17 -0
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +1 -0
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +1 -0
- package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +1 -0
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +1 -0
- package/src/components/resources/renderers/CertificateRenderer.tsx +1 -0
- package/src/components/resources/renderers/CertificateRequestRenderer.tsx +1 -0
- package/src/components/resources/renderers/ChallengeRenderer.tsx +1 -0
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +1 -0
- package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +1 -0
- package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +1 -0
- package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +1 -0
- package/src/components/resources/renderers/ConfigMapRenderer.tsx +1 -0
- package/src/components/resources/renderers/CronJobRenderer.tsx +1 -0
- package/src/components/resources/renderers/EventRenderer.tsx +1 -0
- package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +1 -0
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +1 -0
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +1 -0
- package/src/components/resources/renderers/GRPCRouteRenderer.tsx +1 -0
- package/src/components/resources/renderers/GatewayClassRenderer.tsx +1 -0
- package/src/components/resources/renderers/GatewayRenderer.tsx +1 -0
- package/src/components/resources/renderers/GenericRenderer.tsx +1 -0
- package/src/components/resources/renderers/GitRepositoryRenderer.tsx +1 -0
- package/src/components/resources/renderers/HPARenderer.tsx +1 -0
- package/src/components/resources/renderers/HTTPRouteRenderer.tsx +1 -0
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +1 -0
- package/src/components/resources/renderers/IngressClassRenderer.tsx +1 -0
- package/src/components/resources/renderers/IngressRenderer.tsx +1 -0
- package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +1 -0
- package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +1 -0
- package/src/components/resources/renderers/IstioGatewayRenderer.tsx +1 -0
- package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +1 -0
- package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +1 -0
- package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +1 -0
- package/src/components/resources/renderers/JobRenderer.tsx +1 -0
- package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +1 -0
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +1 -0
- package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +1 -0
- package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +1 -0
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +1 -0
- package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +1 -0
- package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +1 -0
- package/src/components/resources/renderers/KnativeEventingRenderer.tsx +1 -0
- package/src/components/resources/renderers/KnativeFlowRenderer.tsx +1 -0
- package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +1 -0
- package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +1 -0
- package/src/components/resources/renderers/KnativeRouteRenderer.tsx +1 -0
- package/src/components/resources/renderers/KnativeServiceRenderer.tsx +1 -0
- package/src/components/resources/renderers/KnativeSourceRenderer.tsx +1 -0
- package/src/components/resources/renderers/KustomizationRenderer.tsx +1 -0
- package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +1 -0
- package/src/components/resources/renderers/LeaseRenderer.tsx +1 -0
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +1 -0
- package/src/components/resources/renderers/NodeRenderer.tsx +44 -0
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +1 -0
- package/src/components/resources/renderers/OrderRenderer.tsx +1 -0
- package/src/components/resources/renderers/PVCRenderer.tsx +1 -0
- package/src/components/resources/renderers/PersistentVolumeRenderer.tsx +1 -0
- package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +1 -0
- package/src/components/resources/renderers/PodMonitorRenderer.tsx +1 -0
- package/src/components/resources/renderers/PodRenderer.tsx +94 -0
- package/src/components/resources/renderers/PriorityClassRenderer.tsx +1 -0
- package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +1 -0
- package/src/components/resources/renderers/ReplicaSetRenderer.tsx +1 -0
- package/src/components/resources/renderers/RoleBindingRenderer.tsx +1 -0
- package/src/components/resources/renderers/RoleRenderer.tsx +1 -0
- package/src/components/resources/renderers/RolloutRenderer.tsx +1 -0
- package/src/components/resources/renderers/RuntimeClassRenderer.tsx +1 -0
- package/src/components/resources/renderers/SbomReportRenderer.tsx +1 -0
- package/src/components/resources/renderers/SealedSecretRenderer.tsx +1 -0
- package/src/components/resources/renderers/SecretRenderer.tsx +1 -0
- package/src/components/resources/renderers/SecretStoreRenderer.tsx +1 -0
- package/src/components/resources/renderers/ServiceAccountRenderer.tsx +1 -0
- package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +1 -0
- package/src/components/resources/renderers/ServiceRenderer.tsx +26 -0
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +1 -0
- package/src/components/resources/renderers/StorageClassRenderer.tsx +1 -0
- package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +1 -0
- package/src/components/resources/renderers/VPARenderer.tsx +1 -0
- package/src/components/resources/renderers/VeleroBSLRenderer.tsx +1 -0
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +1 -0
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +1 -0
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +1 -0
- package/src/components/resources/renderers/VeleroVSLRenderer.tsx +1 -0
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +1 -0
- package/src/components/resources/renderers/WebhookConfigRenderer.tsx +1 -0
- package/src/components/resources/renderers/WorkflowRenderer.tsx +1 -0
- package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +1 -0
- package/src/components/resources/renderers/WorkloadRenderer.tsx +52 -0
- package/src/components/resources/renderers/argo-cells.tsx +1 -0
- package/src/components/resources/renderers/certmanager-cells.tsx +1 -0
- package/src/components/resources/renderers/cnpg-cells.tsx +1 -0
- package/src/components/resources/renderers/eso-cells.tsx +1 -0
- package/src/components/resources/renderers/flux-cells.tsx +1 -0
- package/src/components/resources/renderers/index.ts +91 -0
- package/src/components/resources/renderers/istio-cells.tsx +1 -0
- package/src/components/resources/renderers/karpenter-cells.tsx +1 -0
- package/src/components/resources/renderers/keda-cells.tsx +1 -0
- package/src/components/resources/renderers/knative-cells.tsx +1 -0
- package/src/components/resources/renderers/kyverno-cells.tsx +1 -0
- package/src/components/resources/renderers/prometheus-cells.tsx +1 -0
- package/src/components/resources/renderers/traefik-cells.tsx +1 -0
- package/src/components/resources/renderers/trivy-cells.tsx +1 -0
- package/src/components/resources/renderers/trivy-shared.tsx +1 -0
- package/src/components/resources/renderers/velero-cells.tsx +1 -0
- package/src/components/resources/resource-utils-argo.ts +2 -0
- package/src/components/resources/resource-utils-certmanager.ts +2 -0
- package/src/components/resources/resource-utils-cnpg.ts +2 -0
- package/src/components/resources/resource-utils-eso.ts +2 -0
- package/src/components/resources/resource-utils-flux.ts +2 -0
- package/src/components/resources/resource-utils-istio.ts +2 -0
- package/src/components/resources/resource-utils-karpenter.ts +2 -0
- package/src/components/resources/resource-utils-keda.ts +2 -0
- package/src/components/resources/resource-utils-knative.ts +2 -0
- package/src/components/resources/resource-utils-kyverno.ts +2 -0
- package/src/components/resources/resource-utils-prometheus.ts +2 -0
- package/src/components/resources/resource-utils-traefik.ts +1 -0
- package/src/components/resources/resource-utils-trivy.ts +2 -0
- package/src/components/resources/resource-utils-velero.ts +2 -0
- package/src/components/resources/resource-utils.ts +5 -0
- package/src/components/settings/SettingsDialog.tsx +537 -0
- package/src/components/shared/CreateResourceDialog.tsx +17 -0
- package/src/components/shared/EditableYamlView.tsx +24 -0
- package/src/components/shared/LargeClusterNamespacePicker.tsx +70 -0
- package/src/components/shared/ResourceRendererDispatch.tsx +31 -0
- package/src/components/timeline/DiffViewer.tsx +1 -0
- package/src/components/timeline/TimelineList.tsx +69 -0
- package/src/components/timeline/TimelineSwimlanes.tsx +1308 -0
- package/src/components/timeline/TimelineView.tsx +157 -0
- package/src/components/timeline/shared.tsx +1 -0
- package/src/components/traffic/TrafficFilterSidebar.tsx +571 -0
- package/src/components/traffic/TrafficFlowList.tsx +415 -0
- package/src/components/traffic/TrafficFlowListContext.tsx +68 -0
- package/src/components/traffic/TrafficGraph.tsx +1546 -0
- package/src/components/traffic/TrafficView.tsx +1213 -0
- package/src/components/traffic/TrafficWizard.tsx +386 -0
- package/src/components/traffic/index.ts +3 -0
- package/src/components/ui/CodeViewer.tsx +8 -0
- package/src/components/ui/CommandPalette.tsx +460 -0
- package/src/components/ui/ConfirmDialog.tsx +1 -0
- package/src/components/ui/DiagnosticsOverlay.tsx +619 -0
- package/src/components/ui/ErrorBoundary.tsx +46 -0
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +1 -0
- package/src/components/ui/Markdown.tsx +108 -0
- package/src/components/ui/MetricsChart.tsx +1 -0
- package/src/components/ui/NamespaceSelector.tsx +436 -0
- package/src/components/ui/ResourceBar.tsx +1 -0
- package/src/components/ui/ShortcutHelpOverlay.tsx +301 -0
- package/src/components/ui/Toast.tsx +1 -0
- package/src/components/ui/Tooltip.tsx +1 -0
- package/src/components/ui/UpdateNotification.tsx +299 -0
- package/src/components/ui/YamlEditor.tsx +1 -0
- package/src/components/workload/WorkloadView.tsx +532 -0
- package/src/context/ConnectionContext.tsx +173 -0
- package/src/context/ContextSwitchContext.tsx +56 -0
- package/src/context/NavCustomization.tsx +62 -0
- package/src/context/ThemeContext.tsx +97 -0
- package/src/contexts/CapabilitiesContext.tsx +130 -0
- package/src/hooks/useAnimatedUnmount.ts +1 -0
- package/src/hooks/useDesktopDownload.ts +41 -0
- package/src/hooks/useEventSource.ts +262 -0
- package/src/hooks/useFavorites.ts +69 -0
- package/src/hooks/useKeyboardShortcuts.tsx +7 -0
- package/src/hooks/useRefreshAnimation.ts +1 -0
- package/src/index.css +243 -0
- package/src/index.ts +17 -0
- package/src/main.tsx +158 -0
- package/src/types/gitops.ts +2 -0
- package/src/types.ts +3 -0
- package/src/utils/animation.ts +2 -0
- package/src/utils/badge-colors.ts +2 -0
- package/src/utils/context-name.ts +2 -0
- package/src/utils/desktop-download.ts +66 -0
- package/src/utils/desktop-open-folder.ts +21 -0
- package/src/utils/format.ts +2 -0
- package/src/utils/log-format.ts +12 -0
- package/src/utils/navigation.ts +23 -0
- package/src/utils/resource-hierarchy.ts +2 -0
- package/src/utils/resource-icons.ts +2 -0
- package/src/utils/skeleton-yaml.ts +2 -0
- package/src/utils/traffic-colors.ts +54 -0
- 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
|
+
}
|