@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,545 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
import { useOpenCostSummary, useOpenCostWorkloads, useOpenCostNodes } from '../../api/client'
|
|
3
|
+
import type { OpenCostNamespaceCost, OpenCostWorkloadCost, OpenCostNodeCost } from '../../api/client'
|
|
4
|
+
import { ArrowLeft, ChevronDown, ChevronRight, DollarSign, HelpCircle, Loader2, Server, X } from 'lucide-react'
|
|
5
|
+
import { CostTrendChart } from './CostTrendChart'
|
|
6
|
+
|
|
7
|
+
interface CostViewProps {
|
|
8
|
+
onBack: () => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function CostView({ onBack }: CostViewProps) {
|
|
12
|
+
const { data, isLoading } = useOpenCostSummary()
|
|
13
|
+
const { data: nodeData } = useOpenCostNodes()
|
|
14
|
+
const [showHelp, setShowHelp] = useState(false)
|
|
15
|
+
|
|
16
|
+
if (isLoading) {
|
|
17
|
+
return (
|
|
18
|
+
<div className="flex-1 flex items-center justify-center">
|
|
19
|
+
<div className="flex flex-col items-center gap-3">
|
|
20
|
+
<Loader2 className="w-6 h-6 animate-spin text-theme-text-tertiary" />
|
|
21
|
+
<span className="text-sm text-theme-text-tertiary">Loading cost data...</span>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!data || !data.available) {
|
|
28
|
+
const reason = data?.reason
|
|
29
|
+
const message = reason === 'no_prometheus'
|
|
30
|
+
? 'Prometheus not found — OpenCost requires Prometheus or VictoriaMetrics'
|
|
31
|
+
: reason === 'no_metrics'
|
|
32
|
+
? 'OpenCost metrics not found — Prometheus is available but no cost metrics were detected'
|
|
33
|
+
: reason === 'query_error'
|
|
34
|
+
? 'Cost data temporarily unavailable — Prometheus was found but queries failed'
|
|
35
|
+
: 'OpenCost not detected — install OpenCost for cost visibility'
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div className="flex-1 flex items-center justify-center">
|
|
39
|
+
<div className="flex flex-col items-center gap-3 text-theme-text-secondary">
|
|
40
|
+
<DollarSign className="w-8 h-8 text-theme-text-tertiary/40" />
|
|
41
|
+
<p className="text-sm">{message}</p>
|
|
42
|
+
<button
|
|
43
|
+
onClick={onBack}
|
|
44
|
+
className="text-xs text-skyhook-400 hover:text-skyhook-300 transition-colors"
|
|
45
|
+
>
|
|
46
|
+
Back to Dashboard
|
|
47
|
+
</button>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const hourlyCost = data.totalHourlyCost ?? 0
|
|
54
|
+
const monthlyCost = hourlyCost * 730
|
|
55
|
+
const namespaces = data.namespaces ?? []
|
|
56
|
+
const totalCpu = namespaces.reduce((sum, ns) => sum + ns.cpuCost, 0)
|
|
57
|
+
const totalMem = namespaces.reduce((sum, ns) => sum + ns.memoryCost, 0)
|
|
58
|
+
const totalStorage = data.totalStorageCost ?? 0
|
|
59
|
+
const hasStorage = totalStorage > 0
|
|
60
|
+
const hasEfficiency = (data.clusterEfficiency ?? 0) > 0
|
|
61
|
+
|
|
62
|
+
// Compute split percentages (CPU + Memory + optional Storage)
|
|
63
|
+
const allocTotal = totalCpu + totalMem + totalStorage
|
|
64
|
+
const cpuPct = allocTotal > 0 ? (totalCpu / allocTotal) * 100 : 50
|
|
65
|
+
const memPct = allocTotal > 0 ? (totalMem / allocTotal) * 100 : 50
|
|
66
|
+
const storagePct = allocTotal > 0 ? (totalStorage / allocTotal) * 100 : 0
|
|
67
|
+
|
|
68
|
+
const nodes = nodeData?.available ? nodeData.nodes ?? [] : []
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div className="flex-1 overflow-y-auto">
|
|
72
|
+
<div className="max-w-[1100px] mx-auto px-6 py-6 space-y-6">
|
|
73
|
+
{/* Header */}
|
|
74
|
+
<div className="flex items-center justify-between">
|
|
75
|
+
<div className="flex items-center gap-3">
|
|
76
|
+
<button
|
|
77
|
+
onClick={onBack}
|
|
78
|
+
className="flex items-center gap-1.5 text-sm text-theme-text-secondary hover:text-theme-text-primary transition-colors"
|
|
79
|
+
>
|
|
80
|
+
<ArrowLeft className="w-4 h-4" />
|
|
81
|
+
Dashboard
|
|
82
|
+
</button>
|
|
83
|
+
<div className="w-px h-5 bg-theme-border" />
|
|
84
|
+
<div className="flex items-center gap-2">
|
|
85
|
+
<DollarSign className="w-5 h-5 text-indigo-500" />
|
|
86
|
+
<h1 className="text-lg font-semibold text-theme-text-primary">Cost Insights</h1>
|
|
87
|
+
</div>
|
|
88
|
+
<span className="text-theme-text-quaternary">·</span>
|
|
89
|
+
<button
|
|
90
|
+
onClick={() => setShowHelp(true)}
|
|
91
|
+
className="flex items-center gap-1 text-xs text-theme-text-tertiary hover:text-indigo-400 cursor-help transition-colors duration-150"
|
|
92
|
+
>
|
|
93
|
+
<HelpCircle className="w-3.5 h-3.5" />
|
|
94
|
+
How this works
|
|
95
|
+
</button>
|
|
96
|
+
</div>
|
|
97
|
+
<div className="flex items-center gap-4">
|
|
98
|
+
{hasEfficiency && (
|
|
99
|
+
<div className="flex flex-col items-end gap-0.5">
|
|
100
|
+
<div className="flex items-center gap-2 text-sm">
|
|
101
|
+
<span className={efficiencyColor(data.clusterEfficiency ?? 0)}>
|
|
102
|
+
{(data.clusterEfficiency ?? 0).toFixed(0)}% efficient
|
|
103
|
+
</span>
|
|
104
|
+
</div>
|
|
105
|
+
<span className="text-[10px] text-theme-text-tertiary">
|
|
106
|
+
~{formatCost((data.totalIdleCost ?? 0) * 730)}/mo unused capacity
|
|
107
|
+
</span>
|
|
108
|
+
</div>
|
|
109
|
+
)}
|
|
110
|
+
<div className="flex flex-col items-end">
|
|
111
|
+
<div className="flex items-baseline gap-3">
|
|
112
|
+
<div className="flex items-baseline gap-1">
|
|
113
|
+
<span className="text-2xl font-bold text-theme-text-primary tabular-nums">
|
|
114
|
+
{formatCost(hourlyCost)}
|
|
115
|
+
</span>
|
|
116
|
+
<span className="text-xs text-theme-text-tertiary">/hr</span>
|
|
117
|
+
</div>
|
|
118
|
+
<div className="flex items-baseline gap-1 text-theme-text-secondary">
|
|
119
|
+
<span className="text-sm font-medium tabular-nums">~{formatCost(monthlyCost)}</span>
|
|
120
|
+
<span className="text-[10px] text-theme-text-tertiary">/mo</span>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
<span className="text-[10px] text-theme-text-quaternary">based on last 1h average</span>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
{/* CPU vs Memory (vs Storage) split bar */}
|
|
129
|
+
<div className="rounded-lg border border-theme-border bg-theme-surface/50 p-4">
|
|
130
|
+
<div className="flex items-center justify-between mb-2">
|
|
131
|
+
<span className="text-xs font-medium text-theme-text-secondary">Cluster Resource Cost</span>
|
|
132
|
+
<div className="flex items-center gap-4 text-xs text-theme-text-tertiary">
|
|
133
|
+
<span className="flex items-center gap-1.5">
|
|
134
|
+
<span className="w-2.5 h-2.5 rounded-sm bg-blue-500" />
|
|
135
|
+
CPU {formatCost(totalCpu)}/hr
|
|
136
|
+
</span>
|
|
137
|
+
<span className="flex items-center gap-1.5">
|
|
138
|
+
<span className="w-2.5 h-2.5 rounded-sm bg-purple-500" />
|
|
139
|
+
Memory {formatCost(totalMem)}/hr
|
|
140
|
+
</span>
|
|
141
|
+
{hasStorage && (
|
|
142
|
+
<span className="flex items-center gap-1.5">
|
|
143
|
+
<span className="w-2.5 h-2.5 rounded-sm bg-teal-500" />
|
|
144
|
+
Storage {formatCost(totalStorage)}/hr
|
|
145
|
+
</span>
|
|
146
|
+
)}
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
<div className="h-3 rounded-full overflow-hidden bg-theme-hover flex">
|
|
150
|
+
<div
|
|
151
|
+
className="h-full bg-blue-500 transition-all duration-300"
|
|
152
|
+
style={{ width: `${cpuPct}%` }}
|
|
153
|
+
/>
|
|
154
|
+
<div
|
|
155
|
+
className="h-full bg-purple-500 transition-all duration-300"
|
|
156
|
+
style={{ width: `${memPct}%` }}
|
|
157
|
+
/>
|
|
158
|
+
{hasStorage && (
|
|
159
|
+
<div
|
|
160
|
+
className="h-full bg-teal-500 transition-all duration-300"
|
|
161
|
+
style={{ width: `${storagePct}%` }}
|
|
162
|
+
/>
|
|
163
|
+
)}
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
{/* Cost trend chart */}
|
|
168
|
+
<CostTrendChart />
|
|
169
|
+
|
|
170
|
+
{/* Namespace cost table */}
|
|
171
|
+
<div className="rounded-lg border border-theme-border bg-theme-surface/50">
|
|
172
|
+
<div className="px-4 py-3 border-b border-theme-border">
|
|
173
|
+
<div className="flex items-center justify-between">
|
|
174
|
+
<div>
|
|
175
|
+
<span className="text-sm font-semibold text-theme-text-primary">Namespace Breakdown</span>
|
|
176
|
+
<span className="text-[10px] text-theme-text-quaternary ml-2">current hourly rates</span>
|
|
177
|
+
</div>
|
|
178
|
+
<span className="text-xs text-theme-text-tertiary">{namespaces.length} namespaces</span>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
{/* Table header */}
|
|
183
|
+
<div className="grid grid-cols-[minmax(180px,1fr)_90px_90px_80px_minmax(160px,1fr)_120px] gap-2 px-4 py-2 border-b border-theme-border text-[11px] font-medium text-theme-text-tertiary uppercase tracking-wider">
|
|
184
|
+
<span>Namespace</span>
|
|
185
|
+
<span className="text-right">Hourly</span>
|
|
186
|
+
<span className="text-right cursor-help" title="Projected from current hourly rate — not historical spend">Monthly*</span>
|
|
187
|
+
<span className="text-right cursor-help" title="% of reserved resources actually being used, weighted by cost">Efficiency</span>
|
|
188
|
+
<span>CPU / Memory</span>
|
|
189
|
+
<span className="text-right">Cost Split</span>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
{/* Namespace rows */}
|
|
193
|
+
<div className="divide-y divide-theme-border/50">
|
|
194
|
+
{namespaces.map((ns) => (
|
|
195
|
+
<NamespaceCostRow key={ns.name} ns={ns} maxCost={namespaces[0]?.hourlyCost ?? 0} hasStorage={hasStorage} />
|
|
196
|
+
))}
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
{/* Node cost table */}
|
|
201
|
+
{nodes.length > 0 && <NodeCostTable nodes={nodes} />}
|
|
202
|
+
|
|
203
|
+
{/* Footer */}
|
|
204
|
+
<div className="flex items-center justify-between text-xs text-theme-text-tertiary pb-4">
|
|
205
|
+
<span>
|
|
206
|
+
{data.currency ?? 'USD'} · costs based on last 1h average · *monthly estimates assume 730 hrs/mo
|
|
207
|
+
</span>
|
|
208
|
+
<span className="text-indigo-500 font-medium">Powered by OpenCost</span>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
{/* Help dialog */}
|
|
213
|
+
{showHelp && <CostHelpDialog onClose={() => setShowHelp(false)} />}
|
|
214
|
+
</div>
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function NamespaceCostRow({ ns, maxCost, hasStorage }: { ns: OpenCostNamespaceCost; maxCost: number; hasStorage: boolean }) {
|
|
219
|
+
const [expanded, setExpanded] = useState(false)
|
|
220
|
+
const monthlyCost = ns.hourlyCost * 730
|
|
221
|
+
const allocTotal = ns.cpuCost + ns.memoryCost + (ns.storageCost ?? 0)
|
|
222
|
+
const cpuPct = allocTotal > 0 ? (ns.cpuCost / allocTotal) * 100 : 50
|
|
223
|
+
const memPct = allocTotal > 0 ? (ns.memoryCost / allocTotal) * 100 : 50
|
|
224
|
+
const barWidth = maxCost > 0 ? (ns.hourlyCost / maxCost) * 100 : 0
|
|
225
|
+
const eff = ns.efficiency ?? 0
|
|
226
|
+
const hasEff = eff > 0
|
|
227
|
+
|
|
228
|
+
return (
|
|
229
|
+
<div>
|
|
230
|
+
<button
|
|
231
|
+
onClick={() => setExpanded(!expanded)}
|
|
232
|
+
className="w-full grid grid-cols-[minmax(180px,1fr)_90px_90px_80px_minmax(160px,1fr)_120px] gap-2 px-4 py-2.5 text-left hover:bg-theme-hover/50 transition-colors group"
|
|
233
|
+
>
|
|
234
|
+
<span className="flex items-center gap-1.5 min-w-0">
|
|
235
|
+
{expanded ? (
|
|
236
|
+
<ChevronDown className="w-3.5 h-3.5 text-theme-text-tertiary shrink-0" />
|
|
237
|
+
) : (
|
|
238
|
+
<ChevronRight className="w-3.5 h-3.5 text-theme-text-tertiary shrink-0" />
|
|
239
|
+
)}
|
|
240
|
+
<span className="text-sm text-theme-text-primary truncate font-medium">{ns.name}</span>
|
|
241
|
+
</span>
|
|
242
|
+
<span className="text-sm text-theme-text-primary tabular-nums text-right">{formatCost(ns.hourlyCost)}</span>
|
|
243
|
+
<span className="text-sm text-theme-text-secondary tabular-nums text-right">~{formatCost(monthlyCost)}</span>
|
|
244
|
+
<span className="text-right flex items-center justify-end gap-1">
|
|
245
|
+
{hasEff ? (
|
|
246
|
+
<>
|
|
247
|
+
<span className={`text-xs font-medium tabular-nums ${efficiencyColor(eff)}`}>
|
|
248
|
+
{eff.toFixed(0)}%
|
|
249
|
+
</span>
|
|
250
|
+
|
|
251
|
+
</>
|
|
252
|
+
) : (
|
|
253
|
+
<span className="text-xs text-theme-text-quaternary">-</span>
|
|
254
|
+
)}
|
|
255
|
+
</span>
|
|
256
|
+
<span className="flex items-center gap-2">
|
|
257
|
+
<div className="flex-1 h-2 rounded-full overflow-hidden bg-theme-hover flex" style={{ maxWidth: `${Math.max(barWidth, 3)}%` }}>
|
|
258
|
+
<div className="h-full bg-blue-500/70" style={{ width: `${cpuPct}%` }} />
|
|
259
|
+
<div className="h-full bg-purple-500/70" style={{ width: `${memPct}%` }} />
|
|
260
|
+
{hasStorage && (ns.storageCost ?? 0) > 0 && (
|
|
261
|
+
<div className="h-full bg-teal-500/70" style={{ width: `${100 - cpuPct - memPct}%` }} />
|
|
262
|
+
)}
|
|
263
|
+
</div>
|
|
264
|
+
</span>
|
|
265
|
+
<span className="text-[11px] text-theme-text-tertiary tabular-nums text-right">
|
|
266
|
+
{formatCost(ns.cpuCost)} / {formatCost(ns.memoryCost)}
|
|
267
|
+
{hasStorage && (ns.storageCost ?? 0) > 0 && ` / ${formatCost(ns.storageCost ?? 0)}`}
|
|
268
|
+
</span>
|
|
269
|
+
</button>
|
|
270
|
+
|
|
271
|
+
{/* Expanded workload rows */}
|
|
272
|
+
{expanded && (
|
|
273
|
+
<WorkloadRows namespace={ns.name} />
|
|
274
|
+
)}
|
|
275
|
+
</div>
|
|
276
|
+
)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function WorkloadRows({ namespace }: { namespace: string }) {
|
|
280
|
+
const { data, isLoading } = useOpenCostWorkloads(namespace)
|
|
281
|
+
|
|
282
|
+
if (isLoading) {
|
|
283
|
+
return (
|
|
284
|
+
<div className="px-4 py-3 flex items-center gap-2 text-xs text-theme-text-tertiary bg-theme-elevated/30">
|
|
285
|
+
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
|
286
|
+
Loading workloads...
|
|
287
|
+
</div>
|
|
288
|
+
)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const workloads = data?.workloads ?? []
|
|
292
|
+
if (workloads.length === 0) {
|
|
293
|
+
return (
|
|
294
|
+
<div className="px-4 py-3 text-xs text-theme-text-tertiary bg-theme-elevated/30 pl-10">
|
|
295
|
+
No workload cost data available
|
|
296
|
+
</div>
|
|
297
|
+
)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return (
|
|
301
|
+
<div className="bg-theme-elevated/30 border-t border-theme-border/30">
|
|
302
|
+
{workloads.map((wl) => (
|
|
303
|
+
<WorkloadCostRow key={`${wl.kind}-${wl.name}`} wl={wl} maxCost={workloads[0]?.hourlyCost ?? 0} />
|
|
304
|
+
))}
|
|
305
|
+
</div>
|
|
306
|
+
)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function WorkloadCostRow({ wl, maxCost }: { wl: OpenCostWorkloadCost; maxCost: number }) {
|
|
310
|
+
const monthlyCost = wl.hourlyCost * 730
|
|
311
|
+
const cpuPct = wl.hourlyCost > 0 ? (wl.cpuCost / wl.hourlyCost) * 100 : 50
|
|
312
|
+
const barWidth = maxCost > 0 ? (wl.hourlyCost / maxCost) * 100 : 0
|
|
313
|
+
const eff = wl.efficiency ?? 0
|
|
314
|
+
const hasEff = eff > 0
|
|
315
|
+
const kindLabel = wl.kind === 'standalone' ? 'pod' : wl.kind
|
|
316
|
+
|
|
317
|
+
return (
|
|
318
|
+
<div className="grid grid-cols-[minmax(180px,1fr)_90px_90px_80px_minmax(160px,1fr)_120px] gap-2 px-4 py-2 text-left">
|
|
319
|
+
<span className="flex items-center gap-1.5 min-w-0 pl-5">
|
|
320
|
+
<span className="text-[10px] text-theme-text-tertiary bg-theme-surface px-1 py-0.5 rounded shrink-0">{kindLabel}</span>
|
|
321
|
+
<span className="text-xs text-theme-text-secondary truncate">{wl.name}</span>
|
|
322
|
+
{wl.replicas > 1 && (
|
|
323
|
+
<span className="text-[10px] text-theme-text-tertiary shrink-0">{wl.replicas}x</span>
|
|
324
|
+
)}
|
|
325
|
+
</span>
|
|
326
|
+
<span className="text-xs text-theme-text-secondary tabular-nums text-right">{formatCost(wl.hourlyCost)}</span>
|
|
327
|
+
<span className="text-xs text-theme-text-tertiary tabular-nums text-right">~{formatCost(monthlyCost)}</span>
|
|
328
|
+
<span className="text-right flex items-center justify-end gap-1">
|
|
329
|
+
{hasEff ? (
|
|
330
|
+
<>
|
|
331
|
+
<span className={`text-[10px] font-medium tabular-nums ${efficiencyColor(eff)}`}>
|
|
332
|
+
{eff.toFixed(0)}%
|
|
333
|
+
</span>
|
|
334
|
+
{eff < 25 && <span className="text-[9px] text-red-400">low</span>}
|
|
335
|
+
</>
|
|
336
|
+
) : (
|
|
337
|
+
<span className="text-[10px] text-theme-text-quaternary">-</span>
|
|
338
|
+
)}
|
|
339
|
+
</span>
|
|
340
|
+
<span className="flex items-center gap-2">
|
|
341
|
+
<div className="flex-1 h-1.5 rounded-full overflow-hidden bg-theme-hover flex" style={{ maxWidth: `${Math.max(barWidth, 3)}%` }}>
|
|
342
|
+
<div className="h-full bg-blue-500/50" style={{ width: `${cpuPct}%` }} />
|
|
343
|
+
<div className="h-full bg-purple-500/50" style={{ width: `${100 - cpuPct}%` }} />
|
|
344
|
+
</div>
|
|
345
|
+
</span>
|
|
346
|
+
<span className="text-[10px] text-theme-text-tertiary tabular-nums text-right">
|
|
347
|
+
{formatCost(wl.cpuCost)} / {formatCost(wl.memoryCost)}
|
|
348
|
+
</span>
|
|
349
|
+
</div>
|
|
350
|
+
)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function NodeCostTable({ nodes }: { nodes: OpenCostNodeCost[] }) {
|
|
354
|
+
return (
|
|
355
|
+
<div className="rounded-lg border border-theme-border bg-theme-surface/50">
|
|
356
|
+
<div className="px-4 py-3 border-b border-theme-border">
|
|
357
|
+
<div className="flex items-center justify-between">
|
|
358
|
+
<div>
|
|
359
|
+
<div className="flex items-center gap-2">
|
|
360
|
+
<Server className="w-4 h-4 text-theme-text-tertiary" />
|
|
361
|
+
<span className="text-sm font-semibold text-theme-text-primary">Node Costs</span>
|
|
362
|
+
<span className="text-[10px] text-theme-text-quaternary">current pricing</span>
|
|
363
|
+
</div>
|
|
364
|
+
<p className="text-[11px] text-theme-text-tertiary mt-0.5 ml-6">
|
|
365
|
+
Per-machine cloud pricing — namespace costs above show how this capacity is allocated
|
|
366
|
+
</p>
|
|
367
|
+
</div>
|
|
368
|
+
<span className="text-xs text-theme-text-tertiary">{nodes.length} nodes</span>
|
|
369
|
+
</div>
|
|
370
|
+
</div>
|
|
371
|
+
|
|
372
|
+
{/* Table header */}
|
|
373
|
+
<div className="grid grid-cols-[minmax(200px,1fr)_minmax(120px,1fr)_90px_100px_140px] gap-2 px-4 py-2 border-b border-theme-border text-[11px] font-medium text-theme-text-tertiary uppercase tracking-wider">
|
|
374
|
+
<span>Node</span>
|
|
375
|
+
<span>Instance Type</span>
|
|
376
|
+
<span className="text-right">Hourly</span>
|
|
377
|
+
<span className="text-right cursor-help" title="Projected from current hourly rate — not historical spend">Monthly*</span>
|
|
378
|
+
<span className="text-right">CPU / Memory</span>
|
|
379
|
+
</div>
|
|
380
|
+
|
|
381
|
+
{/* Node rows */}
|
|
382
|
+
<div className="divide-y divide-theme-border/50">
|
|
383
|
+
{nodes.map((node) => (
|
|
384
|
+
<NodeCostRow key={node.name} node={node} />
|
|
385
|
+
))}
|
|
386
|
+
</div>
|
|
387
|
+
</div>
|
|
388
|
+
)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function NodeCostRow({ node }: { node: OpenCostNodeCost }) {
|
|
392
|
+
const monthlyCost = node.hourlyCost * 730
|
|
393
|
+
|
|
394
|
+
return (
|
|
395
|
+
<div className="grid grid-cols-[minmax(200px,1fr)_minmax(120px,1fr)_90px_100px_140px] gap-2 px-4 py-2.5">
|
|
396
|
+
<span className="text-sm text-theme-text-primary truncate font-medium" title={node.name}>
|
|
397
|
+
{node.name}
|
|
398
|
+
</span>
|
|
399
|
+
<span className="text-xs text-theme-text-secondary truncate">
|
|
400
|
+
{node.instanceType || '-'}
|
|
401
|
+
{node.region && <span className="text-theme-text-quaternary ml-1.5">({node.region})</span>}
|
|
402
|
+
</span>
|
|
403
|
+
<span className="text-sm text-theme-text-primary tabular-nums text-right">{formatCost(node.hourlyCost)}</span>
|
|
404
|
+
<span className="text-sm text-theme-text-secondary tabular-nums text-right">~{formatCost(monthlyCost)}</span>
|
|
405
|
+
<span className="text-[11px] text-theme-text-tertiary tabular-nums text-right">
|
|
406
|
+
{formatCost(node.cpuCost)} / {formatCost(node.memoryCost)}
|
|
407
|
+
</span>
|
|
408
|
+
</div>
|
|
409
|
+
)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// --- Help dialog ---
|
|
413
|
+
|
|
414
|
+
function CostHelpDialog({ onClose }: { onClose: () => void }) {
|
|
415
|
+
useEffect(() => {
|
|
416
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
417
|
+
if (e.key === 'Escape') onClose()
|
|
418
|
+
}
|
|
419
|
+
document.addEventListener('keydown', handleKeyDown)
|
|
420
|
+
return () => document.removeEventListener('keydown', handleKeyDown)
|
|
421
|
+
}, [onClose])
|
|
422
|
+
|
|
423
|
+
return (
|
|
424
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
425
|
+
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={onClose} />
|
|
426
|
+
<div className="relative dialog max-w-lg w-full mx-4 max-h-[80vh] overflow-y-auto">
|
|
427
|
+
{/* Header */}
|
|
428
|
+
<div className="flex items-center justify-between p-4 border-b border-theme-border sticky top-0 bg-theme-surface rounded-t-lg">
|
|
429
|
+
<div className="flex items-center gap-2">
|
|
430
|
+
<HelpCircle className="w-5 h-5 text-indigo-500" />
|
|
431
|
+
<h2 className="text-base font-semibold text-theme-text-primary">Understanding Cost Data</h2>
|
|
432
|
+
</div>
|
|
433
|
+
<button
|
|
434
|
+
onClick={onClose}
|
|
435
|
+
className="p-1 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded"
|
|
436
|
+
>
|
|
437
|
+
<X className="w-5 h-5" />
|
|
438
|
+
</button>
|
|
439
|
+
</div>
|
|
440
|
+
|
|
441
|
+
<div className="p-4 space-y-5 text-sm text-theme-text-secondary">
|
|
442
|
+
{/* Where costs come from */}
|
|
443
|
+
<section>
|
|
444
|
+
<h3 className="text-sm font-semibold text-theme-text-primary mb-1.5">Where do these costs come from?</h3>
|
|
445
|
+
<p>
|
|
446
|
+
Cost data comes from <strong>OpenCost</strong>, an open-source tool that combines your cloud provider's
|
|
447
|
+
pricing (how much each node costs per hour) with Kubernetes resource allocation data. This gives you
|
|
448
|
+
a dollar value for each workload running on your cluster.
|
|
449
|
+
</p>
|
|
450
|
+
</section>
|
|
451
|
+
|
|
452
|
+
{/* What costs represent */}
|
|
453
|
+
<section>
|
|
454
|
+
<h3 className="text-sm font-semibold text-theme-text-primary mb-1.5">What does "hourly cost" mean?</h3>
|
|
455
|
+
<p>
|
|
456
|
+
Each workload <strong>requests</strong> a certain amount of CPU and memory when it's deployed.
|
|
457
|
+
These requests reserve capacity on a node — that reserved capacity has a cost based on
|
|
458
|
+
the node's cloud pricing, whether the workload actually uses it or not.
|
|
459
|
+
</p>
|
|
460
|
+
<p className="mt-1.5">
|
|
461
|
+
The hourly cost shown here is based on what your workloads have <strong>reserved</strong> (requested),
|
|
462
|
+
not what they're actually consuming. Monthly estimates simply multiply the current hourly rate by 730 hours.
|
|
463
|
+
</p>
|
|
464
|
+
</section>
|
|
465
|
+
|
|
466
|
+
{/* Efficiency */}
|
|
467
|
+
<section>
|
|
468
|
+
<h3 className="text-sm font-semibold text-theme-text-primary mb-1.5">What is efficiency?</h3>
|
|
469
|
+
<p>
|
|
470
|
+
Efficiency compares what you're <strong>actually using</strong> versus what you've <strong>reserved</strong>,
|
|
471
|
+
weighted by cost. If a namespace reserves $1/hr of resources but only uses $0.40 worth, it's 40% efficient —
|
|
472
|
+
the other $0.60/hr is idle capacity you're paying for but not using.
|
|
473
|
+
</p>
|
|
474
|
+
<p className="mt-2 text-theme-text-tertiary text-xs">
|
|
475
|
+
Some over-provisioning is normal and healthy — it gives your workloads room to handle
|
|
476
|
+
traffic spikes without running out of resources. Don't aim for 100%.
|
|
477
|
+
</p>
|
|
478
|
+
<div className="mt-2 space-y-1">
|
|
479
|
+
<div className="flex items-center gap-2">
|
|
480
|
+
<span className="w-2 h-2 rounded-full bg-emerald-400" />
|
|
481
|
+
<span><strong>50%+</strong> — well-utilized</span>
|
|
482
|
+
</div>
|
|
483
|
+
<div className="flex items-center gap-2">
|
|
484
|
+
<span className="w-2 h-2 rounded-full bg-amber-400" />
|
|
485
|
+
<span><strong>25–50%</strong> — typical for most clusters, some room to optimize</span>
|
|
486
|
+
</div>
|
|
487
|
+
<div className="flex items-center gap-2">
|
|
488
|
+
<span className="w-2 h-2 rounded-full bg-red-400" />
|
|
489
|
+
<span><strong>Below 25%</strong> — worth investigating, may be significantly over-provisioned</span>
|
|
490
|
+
</div>
|
|
491
|
+
</div>
|
|
492
|
+
</section>
|
|
493
|
+
|
|
494
|
+
{/* Time context */}
|
|
495
|
+
<section>
|
|
496
|
+
<h3 className="text-sm font-semibold text-theme-text-primary mb-1.5">How fresh is this data?</h3>
|
|
497
|
+
<p>
|
|
498
|
+
Cost rates, efficiency, and breakdowns are <strong>snapshots based on the last 1 hour</strong> of data.
|
|
499
|
+
They update automatically every minute. The trend chart is the only historical view — it shows how
|
|
500
|
+
total cost has changed over the selected time range (6 hours, 24 hours, or 7 days).
|
|
501
|
+
</p>
|
|
502
|
+
<p className="mt-1.5">
|
|
503
|
+
Because costs are based on a 1-hour window, short-lived spikes or dips may not be reflected.
|
|
504
|
+
The trend chart gives you the longer-term picture.
|
|
505
|
+
</p>
|
|
506
|
+
</section>
|
|
507
|
+
|
|
508
|
+
{/* Node costs */}
|
|
509
|
+
<section>
|
|
510
|
+
<h3 className="text-sm font-semibold text-theme-text-primary mb-1.5">What are node costs?</h3>
|
|
511
|
+
<p>
|
|
512
|
+
Node costs show the hourly price of each machine in your cluster, based on instance type and
|
|
513
|
+
cloud pricing. This is the total capacity cost — the namespace and workload breakdowns above
|
|
514
|
+
show how that capacity is allocated across your workloads.
|
|
515
|
+
</p>
|
|
516
|
+
</section>
|
|
517
|
+
</div>
|
|
518
|
+
</div>
|
|
519
|
+
</div>
|
|
520
|
+
)
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// --- Utilities ---
|
|
524
|
+
|
|
525
|
+
function efficiencyColor(efficiency: number): string {
|
|
526
|
+
if (efficiency >= 50) return 'text-emerald-400'
|
|
527
|
+
if (efficiency >= 25) return 'text-amber-400'
|
|
528
|
+
return 'text-red-400'
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function formatCost(value: number): string {
|
|
532
|
+
if (value >= 1000) {
|
|
533
|
+
return `$${(value / 1000).toFixed(1)}k`
|
|
534
|
+
}
|
|
535
|
+
if (value >= 1) {
|
|
536
|
+
return `$${value.toFixed(2)}`
|
|
537
|
+
}
|
|
538
|
+
if (value >= 0.01) {
|
|
539
|
+
return `$${value.toFixed(3)}`
|
|
540
|
+
}
|
|
541
|
+
if (value > 0) {
|
|
542
|
+
return `$${value.toFixed(4)}`
|
|
543
|
+
}
|
|
544
|
+
return '$0.00'
|
|
545
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { BottomDock as K8sBottomDock, DockTab, useDock } from '@skyhook-io/k8s-ui'
|
|
2
|
+
import { TerminalTab } from './TerminalTab'
|
|
3
|
+
import { LogsTab } from './LogsTab'
|
|
4
|
+
import { WorkloadLogsTab } from './WorkloadLogsTab'
|
|
5
|
+
import { NodeTerminalTab } from './NodeTerminalTab'
|
|
6
|
+
import { LocalTerminalTab } from './LocalTerminalTab'
|
|
7
|
+
import { TrafficFlowListTab } from './TrafficFlowListTab'
|
|
8
|
+
import { useFlowSearch } from '../traffic/TrafficFlowListContext'
|
|
9
|
+
import { Search } from 'lucide-react'
|
|
10
|
+
|
|
11
|
+
function renderTabContent(tab: DockTab, isActive: boolean) {
|
|
12
|
+
if (tab.type === 'terminal') {
|
|
13
|
+
return (
|
|
14
|
+
<TerminalTab
|
|
15
|
+
namespace={tab.namespace!}
|
|
16
|
+
podName={tab.podName!}
|
|
17
|
+
containerName={tab.containerName!}
|
|
18
|
+
containers={tab.containers!}
|
|
19
|
+
isActive={isActive}
|
|
20
|
+
/>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (tab.type === 'logs') {
|
|
25
|
+
return (
|
|
26
|
+
<LogsTab
|
|
27
|
+
namespace={tab.namespace!}
|
|
28
|
+
podName={tab.podName!}
|
|
29
|
+
containers={tab.containers!}
|
|
30
|
+
initialContainer={tab.containerName}
|
|
31
|
+
/>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (tab.type === 'workload-logs') {
|
|
36
|
+
return (
|
|
37
|
+
<WorkloadLogsTab
|
|
38
|
+
namespace={tab.namespace!}
|
|
39
|
+
workloadKind={tab.workloadKind!}
|
|
40
|
+
workloadName={tab.workloadName!}
|
|
41
|
+
/>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (tab.type === 'node-terminal') {
|
|
46
|
+
return (
|
|
47
|
+
<NodeTerminalTab
|
|
48
|
+
nodeName={tab.nodeName!}
|
|
49
|
+
isActive={isActive}
|
|
50
|
+
/>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (tab.type === 'local-terminal') {
|
|
55
|
+
return (
|
|
56
|
+
<LocalTerminalTab isActive={isActive} initialCommand={tab.initialCommand} />
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (tab.type === 'traffic-flows') {
|
|
61
|
+
return <TrafficFlowListTab />
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function TrafficFlowSearchHeader() {
|
|
68
|
+
const [search, setSearch] = useFlowSearch()
|
|
69
|
+
return (
|
|
70
|
+
<div className="flex-1 flex items-center ml-2">
|
|
71
|
+
<div className="relative">
|
|
72
|
+
<Search className="absolute left-1.5 top-1/2 -translate-y-1/2 w-3 h-3 text-theme-text-tertiary" />
|
|
73
|
+
<input
|
|
74
|
+
type="text"
|
|
75
|
+
value={search}
|
|
76
|
+
onChange={e => setSearch(e.target.value)}
|
|
77
|
+
placeholder="Filter flows..."
|
|
78
|
+
className="w-48 pl-6 pr-2 py-1 text-[11px] rounded bg-theme-elevated border border-theme-border text-theme-text-primary placeholder:text-theme-text-tertiary focus:outline-none focus:ring-1 focus:ring-blue-500/50"
|
|
79
|
+
/>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function renderTabHeaderExtra(tab: DockTab) {
|
|
86
|
+
if (tab.type === 'traffic-flows') {
|
|
87
|
+
return <TrafficFlowSearchHeader />
|
|
88
|
+
}
|
|
89
|
+
return null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function BottomDock() {
|
|
93
|
+
const { leftOffset, tabs } = useDock()
|
|
94
|
+
const hasTrafficFlows = tabs.some(t => t.type === 'traffic-flows')
|
|
95
|
+
return <K8sBottomDock renderTabContent={renderTabContent} renderTabHeaderExtra={renderTabHeaderExtra} leftOffset={leftOffset} defaultHeight={hasTrafficFlows ? 300 : undefined} />
|
|
96
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Re-exported from @skyhook-io/k8s-ui — source of truth is radar/packages/k8s-ui/src/components/dock/DockContext.tsx
|
|
2
|
+
export {
|
|
3
|
+
DockProvider,
|
|
4
|
+
useDock,
|
|
5
|
+
useOpenTerminal,
|
|
6
|
+
useOpenLogs,
|
|
7
|
+
useOpenWorkloadLogs,
|
|
8
|
+
useOpenNodeTerminal,
|
|
9
|
+
useOpenLocalTerminal,
|
|
10
|
+
} from '@skyhook-io/k8s-ui'
|
|
11
|
+
export type { DockTab, DockTabType, DockContextValue } from '@skyhook-io/k8s-ui'
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { LocalTerminalTab as SharedLocalTerminalTab } from '@skyhook-io/k8s-ui'
|
|
2
|
+
import { getWsUrl } from '../../api/config'
|
|
3
|
+
|
|
4
|
+
interface LocalTerminalTabProps {
|
|
5
|
+
isActive?: boolean
|
|
6
|
+
initialCommand?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function LocalTerminalTab({ isActive, initialCommand }: LocalTerminalTabProps) {
|
|
10
|
+
const createSession = () =>
|
|
11
|
+
Promise.resolve({
|
|
12
|
+
wsUrl: getWsUrl('/local-terminal'),
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<SharedLocalTerminalTab
|
|
17
|
+
isActive={isActive}
|
|
18
|
+
createSession={createSession}
|
|
19
|
+
initialCommand={initialCommand}
|
|
20
|
+
/>
|
|
21
|
+
)
|
|
22
|
+
}
|