@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,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
|
+
}
|