@skyhook-io/k8s-ui 1.0.0
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/package.json +63 -0
- package/src/components/dock/BottomDock.tsx +204 -0
- package/src/components/dock/DockContext.tsx +212 -0
- package/src/components/dock/TerminalTab.tsx +333 -0
- package/src/components/dock/index.ts +3 -0
- package/src/components/gitops/GitOpsActions.tsx +227 -0
- package/src/components/gitops/GitOpsStatusBadge.tsx +221 -0
- package/src/components/gitops/ManagedResourcesList.tsx +210 -0
- package/src/components/gitops/SyncCountdown.tsx +146 -0
- package/src/components/gitops/index.ts +4 -0
- package/src/components/logs/JsonLogLine.tsx +90 -0
- package/src/components/logs/LogCore.tsx +553 -0
- package/src/components/logs/LogToolbarSelects.tsx +74 -0
- package/src/components/logs/LogsViewer.tsx +146 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +263 -0
- package/src/components/logs/index.ts +13 -0
- package/src/components/logs/useLogBuffer.ts +138 -0
- package/src/components/logs/useLogSearch.ts +151 -0
- package/src/components/logs/useLogStream.ts +61 -0
- package/src/components/resources/ResourcesView.tsx +5494 -0
- package/src/components/resources/index.ts +17 -0
- package/src/components/resources/renderers/AlertRenderer.tsx +166 -0
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +233 -0
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +89 -0
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +194 -0
- package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +79 -0
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +68 -0
- package/src/components/resources/renderers/CertificateRenderer.tsx +209 -0
- package/src/components/resources/renderers/CertificateRequestRenderer.tsx +123 -0
- package/src/components/resources/renderers/ChallengeRenderer.tsx +111 -0
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +164 -0
- package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +140 -0
- package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +153 -0
- package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +221 -0
- package/src/components/resources/renderers/ConfigMapRenderer.tsx +124 -0
- package/src/components/resources/renderers/CronJobRenderer.tsx +86 -0
- package/src/components/resources/renderers/EventRenderer.tsx +175 -0
- package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +119 -0
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +172 -0
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +242 -0
- package/src/components/resources/renderers/GRPCRouteRenderer.tsx +263 -0
- package/src/components/resources/renderers/GatewayClassRenderer.tsx +68 -0
- package/src/components/resources/renderers/GatewayRenderer.tsx +218 -0
- package/src/components/resources/renderers/GenericRenderer.tsx +476 -0
- package/src/components/resources/renderers/GitRepositoryRenderer.tsx +154 -0
- package/src/components/resources/renderers/HPARenderer.tsx +164 -0
- package/src/components/resources/renderers/HTTPRouteRenderer.tsx +303 -0
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +167 -0
- package/src/components/resources/renderers/IngressClassRenderer.tsx +49 -0
- package/src/components/resources/renderers/IngressRenderer.tsx +104 -0
- package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +175 -0
- package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +176 -0
- package/src/components/resources/renderers/IstioGatewayRenderer.tsx +125 -0
- package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +84 -0
- package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +112 -0
- package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +288 -0
- package/src/components/resources/renderers/JobRenderer.tsx +103 -0
- package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +157 -0
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +188 -0
- package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +148 -0
- package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +90 -0
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +136 -0
- package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +151 -0
- package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +70 -0
- package/src/components/resources/renderers/KnativeEventingRenderer.tsx +309 -0
- package/src/components/resources/renderers/KnativeFlowRenderer.tsx +142 -0
- package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +232 -0
- package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +97 -0
- package/src/components/resources/renderers/KnativeRouteRenderer.tsx +74 -0
- package/src/components/resources/renderers/KnativeServiceRenderer.tsx +138 -0
- package/src/components/resources/renderers/KnativeSourceRenderer.tsx +220 -0
- package/src/components/resources/renderers/KustomizationRenderer.tsx +151 -0
- package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +352 -0
- package/src/components/resources/renderers/LeaseRenderer.tsx +93 -0
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +208 -0
- package/src/components/resources/renderers/NodeRenderer.tsx +282 -0
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +153 -0
- package/src/components/resources/renderers/OrderRenderer.tsx +108 -0
- package/src/components/resources/renderers/PVCRenderer.tsx +98 -0
- package/src/components/resources/renderers/PersistentVolumeRenderer.tsx +138 -0
- package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +126 -0
- package/src/components/resources/renderers/PodMonitorRenderer.tsx +63 -0
- package/src/components/resources/renderers/PodRenderer.tsx +677 -0
- package/src/components/resources/renderers/PriorityClassRenderer.tsx +50 -0
- package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +62 -0
- package/src/components/resources/renderers/ReplicaSetRenderer.tsx +71 -0
- package/src/components/resources/renderers/RoleBindingRenderer.tsx +97 -0
- package/src/components/resources/renderers/RoleRenderer.tsx +192 -0
- package/src/components/resources/renderers/RolloutRenderer.tsx +227 -0
- package/src/components/resources/renderers/RuntimeClassRenderer.tsx +75 -0
- package/src/components/resources/renderers/SbomReportRenderer.tsx +98 -0
- package/src/components/resources/renderers/SealedSecretRenderer.tsx +121 -0
- package/src/components/resources/renderers/SecretRenderer.tsx +321 -0
- package/src/components/resources/renderers/SecretStoreRenderer.tsx +111 -0
- package/src/components/resources/renderers/ServiceAccountRenderer.tsx +81 -0
- package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +65 -0
- package/src/components/resources/renderers/ServiceRenderer.tsx +98 -0
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +195 -0
- package/src/components/resources/renderers/StorageClassRenderer.tsx +82 -0
- package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +257 -0
- package/src/components/resources/renderers/VPARenderer.tsx +193 -0
- package/src/components/resources/renderers/VeleroBSLRenderer.tsx +73 -0
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +193 -0
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +161 -0
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +134 -0
- package/src/components/resources/renderers/VeleroVSLRenderer.tsx +36 -0
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +347 -0
- package/src/components/resources/renderers/WebhookConfigRenderer.tsx +156 -0
- package/src/components/resources/renderers/WorkflowRenderer.tsx +202 -0
- package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +91 -0
- package/src/components/resources/renderers/WorkloadRenderer.tsx +311 -0
- package/src/components/resources/renderers/argo-cells.tsx +118 -0
- package/src/components/resources/renderers/certmanager-cells.tsx +164 -0
- package/src/components/resources/renderers/cnpg-cells.tsx +161 -0
- package/src/components/resources/renderers/eso-cells.tsx +115 -0
- package/src/components/resources/renderers/flux-cells.tsx +253 -0
- package/src/components/resources/renderers/index.ts +105 -0
- package/src/components/resources/renderers/istio-cells.tsx +210 -0
- package/src/components/resources/renderers/karpenter-cells.tsx +107 -0
- package/src/components/resources/renderers/keda-cells.tsx +94 -0
- package/src/components/resources/renderers/knative-cells.tsx +396 -0
- package/src/components/resources/renderers/kyverno-cells.tsx +85 -0
- package/src/components/resources/renderers/prometheus-cells.tsx +74 -0
- package/src/components/resources/renderers/traefik-cells.tsx +120 -0
- package/src/components/resources/renderers/trivy-cells.tsx +153 -0
- package/src/components/resources/renderers/trivy-shared.tsx +67 -0
- package/src/components/resources/renderers/velero-cells.tsx +164 -0
- package/src/components/resources/resource-utils-argo.ts +155 -0
- package/src/components/resources/resource-utils-certmanager.ts +198 -0
- package/src/components/resources/resource-utils-cnpg.ts +344 -0
- package/src/components/resources/resource-utils-eso.ts +385 -0
- package/src/components/resources/resource-utils-flux.ts +186 -0
- package/src/components/resources/resource-utils-istio.ts +372 -0
- package/src/components/resources/resource-utils-karpenter.ts +150 -0
- package/src/components/resources/resource-utils-keda.ts +203 -0
- package/src/components/resources/resource-utils-knative.ts +218 -0
- package/src/components/resources/resource-utils-kyverno.ts +224 -0
- package/src/components/resources/resource-utils-prometheus.ts +159 -0
- package/src/components/resources/resource-utils-traefik.ts +180 -0
- package/src/components/resources/resource-utils-trivy.ts +146 -0
- package/src/components/resources/resource-utils-velero.ts +293 -0
- package/src/components/resources/resource-utils.ts +1558 -0
- package/src/components/shared/EditableYamlView.tsx +302 -0
- package/src/components/shared/ResourceActionsBar.tsx +924 -0
- package/src/components/shared/ResourceRendererDispatch.tsx +557 -0
- package/src/components/shared/index.ts +3 -0
- package/src/components/timeline/index.ts +1 -0
- package/src/components/timeline/shared.tsx +614 -0
- package/src/components/topology/GroupNode.tsx +198 -0
- package/src/components/topology/K8sResourceNode.tsx +440 -0
- package/src/components/topology/TopologyControls.tsx +62 -0
- package/src/components/topology/TopologyFilterSidebar.tsx +377 -0
- package/src/components/topology/TopologyGraph.tsx +748 -0
- package/src/components/topology/TopologySearch.tsx +304 -0
- package/src/components/topology/index.ts +7 -0
- package/src/components/topology/layout.ts +734 -0
- package/src/components/topology/layout.worker.ts +272 -0
- package/src/components/topology/topology.css +129 -0
- package/src/components/ui/CodeViewer.tsx +451 -0
- package/src/components/ui/ConfirmDialog.tsx +188 -0
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +57 -0
- package/src/components/ui/HealthRing.tsx +72 -0
- package/src/components/ui/MetricsChart.tsx +259 -0
- package/src/components/ui/ResourceBar.tsx +68 -0
- package/src/components/ui/Toast.tsx +225 -0
- package/src/components/ui/Tooltip.tsx +165 -0
- package/src/components/ui/YamlEditor.tsx +280 -0
- package/src/components/ui/drawer-components.tsx +792 -0
- package/src/components/ui/index.ts +10 -0
- package/src/components/workload/ResourceDetailDrawer.tsx +158 -0
- package/src/components/workload/WorkloadView.tsx +1109 -0
- package/src/components/workload/index.ts +2 -0
- package/src/hooks/index.ts +9 -0
- package/src/hooks/useAnimatedUnmount.ts +30 -0
- package/src/hooks/useKeyboardShortcuts.tsx +336 -0
- package/src/hooks/useRefreshAnimation.ts +58 -0
- package/src/index.ts +35 -0
- package/src/types/core.ts +894 -0
- package/src/types/gitops.ts +458 -0
- package/src/types/index.ts +2 -0
- package/src/utils/animation.ts +40 -0
- package/src/utils/api-resources.ts +185 -0
- package/src/utils/badge-colors.ts +399 -0
- package/src/utils/context-name.ts +72 -0
- package/src/utils/download.ts +19 -0
- package/src/utils/format.ts +233 -0
- package/src/utils/index.ts +10 -0
- package/src/utils/log-format.ts +300 -0
- package/src/utils/navigation.ts +119 -0
- package/src/utils/resource-hierarchy.ts +630 -0
- package/src/utils/resource-icons.ts +206 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
interface Segment {
|
|
2
|
+
value: number
|
|
3
|
+
color: string
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
interface HealthRingProps {
|
|
7
|
+
segments: Segment[]
|
|
8
|
+
size?: number
|
|
9
|
+
strokeWidth?: number
|
|
10
|
+
label?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function HealthRing({ segments, size = 48, strokeWidth = 5, label }: HealthRingProps) {
|
|
14
|
+
const total = segments.reduce((sum, s) => sum + s.value, 0)
|
|
15
|
+
if (total === 0) {
|
|
16
|
+
return (
|
|
17
|
+
<svg width={size} height={size} viewBox="0 0 100 100" className="shrink-0">
|
|
18
|
+
<circle cx="50" cy="50" r="40" fill="none" stroke="currentColor" strokeWidth={strokeWidth} className="text-theme-border" />
|
|
19
|
+
{label && (
|
|
20
|
+
<text x="50" y="50" textAnchor="middle" dominantBaseline="central" className="fill-theme-text-tertiary text-[22px] font-semibold">
|
|
21
|
+
0
|
|
22
|
+
</text>
|
|
23
|
+
)}
|
|
24
|
+
</svg>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const radius = 40
|
|
29
|
+
const circumference = 2 * Math.PI * radius
|
|
30
|
+
let accumulatedOffset = 0
|
|
31
|
+
|
|
32
|
+
// Rotate -90deg so arcs start at 12 o'clock
|
|
33
|
+
const arcs = segments
|
|
34
|
+
.filter(s => s.value > 0)
|
|
35
|
+
.map((seg) => {
|
|
36
|
+
const fraction = seg.value / total
|
|
37
|
+
const dashLength = fraction * circumference
|
|
38
|
+
const gapLength = circumference - dashLength
|
|
39
|
+
const offset = -accumulatedOffset * circumference
|
|
40
|
+
accumulatedOffset += fraction
|
|
41
|
+
return { color: seg.color, dashLength, gapLength, offset }
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<svg width={size} height={size} viewBox="0 0 100 100" className="shrink-0">
|
|
46
|
+
{/* Background ring */}
|
|
47
|
+
<circle cx="50" cy="50" r={radius} fill="none" stroke="currentColor" strokeWidth={strokeWidth} className="text-theme-border opacity-30" />
|
|
48
|
+
{/* Segments */}
|
|
49
|
+
{arcs.map((arc, i) => (
|
|
50
|
+
<circle
|
|
51
|
+
key={i}
|
|
52
|
+
cx="50"
|
|
53
|
+
cy="50"
|
|
54
|
+
r={radius}
|
|
55
|
+
fill="none"
|
|
56
|
+
stroke={arc.color}
|
|
57
|
+
strokeWidth={strokeWidth}
|
|
58
|
+
strokeDasharray={`${arc.dashLength} ${arc.gapLength}`}
|
|
59
|
+
strokeDashoffset={arc.offset}
|
|
60
|
+
strokeLinecap="butt"
|
|
61
|
+
transform="rotate(-90 50 50)"
|
|
62
|
+
/>
|
|
63
|
+
))}
|
|
64
|
+
{/* Center label */}
|
|
65
|
+
{label && (
|
|
66
|
+
<text x="50" y="50" textAnchor="middle" dominantBaseline="central" className="fill-theme-text-primary text-[22px] font-semibold">
|
|
67
|
+
{label}
|
|
68
|
+
</text>
|
|
69
|
+
)}
|
|
70
|
+
</svg>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import { clsx } from 'clsx'
|
|
3
|
+
import type { MetricsDataPoint } from '../../types/core'
|
|
4
|
+
import { formatCPUNanocores, formatMemoryBytes, parseCPUToNanocores, parseMemoryToBytes } from '../../utils/format'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Round up a value to a "nice" number for chart axes.
|
|
8
|
+
* Nice numbers are 1, 2, 5 multiplied by powers of 10.
|
|
9
|
+
*/
|
|
10
|
+
function niceRoundUp(value: number): number {
|
|
11
|
+
if (value <= 0) return 0
|
|
12
|
+
|
|
13
|
+
// Find the order of magnitude
|
|
14
|
+
const magnitude = Math.pow(10, Math.floor(Math.log10(value)))
|
|
15
|
+
const normalized = value / magnitude // Will be between 1 and 10
|
|
16
|
+
|
|
17
|
+
// Round up to nice intervals: 1, 2, 5, 10
|
|
18
|
+
let nice: number
|
|
19
|
+
if (normalized <= 1) {
|
|
20
|
+
nice = 1
|
|
21
|
+
} else if (normalized <= 2) {
|
|
22
|
+
nice = 2
|
|
23
|
+
} else if (normalized <= 5) {
|
|
24
|
+
nice = 5
|
|
25
|
+
} else {
|
|
26
|
+
nice = 10
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return nice * magnitude
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface MetricsChartProps {
|
|
33
|
+
dataPoints: MetricsDataPoint[]
|
|
34
|
+
type: 'cpu' | 'memory'
|
|
35
|
+
height?: number
|
|
36
|
+
className?: string
|
|
37
|
+
showAxis?: boolean
|
|
38
|
+
/** K8s resource limit string (e.g., "500m", "1Gi") */
|
|
39
|
+
limit?: string
|
|
40
|
+
/** K8s resource request string (e.g., "100m", "256Mi") */
|
|
41
|
+
request?: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function MetricsChart({
|
|
45
|
+
dataPoints,
|
|
46
|
+
type,
|
|
47
|
+
height = 60,
|
|
48
|
+
className,
|
|
49
|
+
showAxis = true,
|
|
50
|
+
limit,
|
|
51
|
+
request,
|
|
52
|
+
}: MetricsChartProps) {
|
|
53
|
+
// Parse limit and request to the same unit as data (nanocores or bytes)
|
|
54
|
+
const parseValue = type === 'cpu' ? parseCPUToNanocores : parseMemoryToBytes
|
|
55
|
+
const limitValue = limit ? parseValue(limit) : undefined
|
|
56
|
+
const requestValue = request ? parseValue(request) : undefined
|
|
57
|
+
|
|
58
|
+
const { values, chartMax, dataMax, current, timeRange } = useMemo(() => {
|
|
59
|
+
if (!dataPoints || dataPoints.length === 0) {
|
|
60
|
+
return { values: [], chartMax: 0, dataMax: 0, current: 0, timeRange: '' }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const vals = dataPoints.map(d => type === 'cpu' ? d.cpu : d.memory)
|
|
64
|
+
const maxVal = Math.max(...vals)
|
|
65
|
+
const currentVal = vals[vals.length - 1]
|
|
66
|
+
|
|
67
|
+
// Chart max should include limit if present and higher than data
|
|
68
|
+
let chartMaxVal = maxVal
|
|
69
|
+
if (limitValue && limitValue > chartMaxVal) {
|
|
70
|
+
chartMaxVal = limitValue
|
|
71
|
+
}
|
|
72
|
+
// Round up to a nice number for clean axis labels
|
|
73
|
+
chartMaxVal = niceRoundUp(chartMaxVal * 1.05) // Small padding before rounding
|
|
74
|
+
|
|
75
|
+
// Calculate time range
|
|
76
|
+
const firstTime = new Date(dataPoints[0].timestamp)
|
|
77
|
+
const lastTime = new Date(dataPoints[dataPoints.length - 1].timestamp)
|
|
78
|
+
const diffMinutes = Math.round((lastTime.getTime() - firstTime.getTime()) / 60000)
|
|
79
|
+
const range = diffMinutes > 60 ? `${Math.round(diffMinutes / 60)}h` : `${diffMinutes}m`
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
values: vals,
|
|
83
|
+
chartMax: chartMaxVal,
|
|
84
|
+
dataMax: maxVal,
|
|
85
|
+
current: currentVal,
|
|
86
|
+
timeRange: range,
|
|
87
|
+
}
|
|
88
|
+
}, [dataPoints, type, limitValue])
|
|
89
|
+
|
|
90
|
+
if (values.length === 0) {
|
|
91
|
+
return (
|
|
92
|
+
<div className={clsx('flex items-center justify-center text-theme-text-tertiary text-xs', className)} style={{ height }}>
|
|
93
|
+
No metrics data yet
|
|
94
|
+
</div>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const color = type === 'cpu' ? 'bg-blue-500' : 'bg-purple-500'
|
|
99
|
+
const format = type === 'cpu' ? formatCPUNanocores : formatMemoryBytes
|
|
100
|
+
|
|
101
|
+
// Calculate line positions (from bottom, as percentage)
|
|
102
|
+
const limitPercent = limitValue && chartMax > 0 ? (limitValue / chartMax) * 100 : undefined
|
|
103
|
+
const requestPercent = requestValue && chartMax > 0 ? (requestValue / chartMax) * 100 : undefined
|
|
104
|
+
|
|
105
|
+
// Y-axis values
|
|
106
|
+
const yAxisMax = chartMax
|
|
107
|
+
const yAxisMid = chartMax / 2
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<div className={clsx('flex flex-col', className)}>
|
|
111
|
+
{/* Chart with Y-axis */}
|
|
112
|
+
<div className="flex">
|
|
113
|
+
{/* Y-axis labels */}
|
|
114
|
+
<div className="flex flex-col justify-between text-[9px] text-theme-text-tertiary pr-1 w-12 text-right" style={{ height }}>
|
|
115
|
+
<span className="leading-none">{format(yAxisMax)}</span>
|
|
116
|
+
<span className="leading-none">{format(yAxisMid)}</span>
|
|
117
|
+
<span className="leading-none">0</span>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
{/* Chart area */}
|
|
121
|
+
<div className="relative flex-1" style={{ height }}>
|
|
122
|
+
{/* Bars */}
|
|
123
|
+
<div className="absolute inset-0 flex items-end">
|
|
124
|
+
{values.map((value, i) => {
|
|
125
|
+
const normalizedHeight = chartMax > 0 ? (value / chartMax) * 100 : 0
|
|
126
|
+
const barHeight = Math.max((normalizedHeight / 100) * height, 2)
|
|
127
|
+
return (
|
|
128
|
+
<div
|
|
129
|
+
key={i}
|
|
130
|
+
className="flex-1 px-px flex items-end"
|
|
131
|
+
title={`${format(value)} at ${new Date(dataPoints[i].timestamp).toLocaleTimeString()}`}
|
|
132
|
+
>
|
|
133
|
+
<div
|
|
134
|
+
className={clsx(color, 'w-full rounded-t-sm opacity-70 hover:opacity-100 transition-opacity')}
|
|
135
|
+
style={{ height: barHeight }}
|
|
136
|
+
/>
|
|
137
|
+
</div>
|
|
138
|
+
)
|
|
139
|
+
})}
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
{/* Limit line */}
|
|
143
|
+
{limitPercent !== undefined && limitPercent <= 100 && (
|
|
144
|
+
<div
|
|
145
|
+
className="absolute left-0 right-0 border-t-2 border-red-500 border-dashed pointer-events-none"
|
|
146
|
+
style={{ bottom: `${limitPercent}%` }}
|
|
147
|
+
title={`Limit: ${limit}`}
|
|
148
|
+
/>
|
|
149
|
+
)}
|
|
150
|
+
|
|
151
|
+
{/* Request line */}
|
|
152
|
+
{requestPercent !== undefined && requestPercent <= 100 && (
|
|
153
|
+
<div
|
|
154
|
+
className="absolute left-0 right-0 border-t-2 border-yellow-500 border-dashed pointer-events-none"
|
|
155
|
+
style={{ bottom: `${requestPercent}%` }}
|
|
156
|
+
title={`Request: ${request}`}
|
|
157
|
+
/>
|
|
158
|
+
)}
|
|
159
|
+
|
|
160
|
+
{/* Grid lines */}
|
|
161
|
+
<div className="absolute inset-0 flex flex-col justify-between pointer-events-none">
|
|
162
|
+
<div className="border-b border-theme-border/20" />
|
|
163
|
+
<div className="border-b border-theme-border/20" />
|
|
164
|
+
<div className="border-b border-theme-border/10" />
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
{/* Right-side labels for limit/request */}
|
|
169
|
+
{(limitPercent !== undefined || requestPercent !== undefined) && (
|
|
170
|
+
<div className="relative w-8 pl-1" style={{ height }}>
|
|
171
|
+
{limitPercent !== undefined && limitPercent <= 100 && (
|
|
172
|
+
<span
|
|
173
|
+
className="absolute text-[9px] text-red-400 leading-none"
|
|
174
|
+
style={{ bottom: `${limitPercent}%`, transform: 'translateY(50%)' }}
|
|
175
|
+
>
|
|
176
|
+
limit
|
|
177
|
+
</span>
|
|
178
|
+
)}
|
|
179
|
+
{requestPercent !== undefined && requestPercent <= 100 && (
|
|
180
|
+
<span
|
|
181
|
+
className="absolute text-[9px] text-yellow-400 leading-none"
|
|
182
|
+
style={{ bottom: `${requestPercent}%`, transform: 'translateY(50%)' }}
|
|
183
|
+
>
|
|
184
|
+
req
|
|
185
|
+
</span>
|
|
186
|
+
)}
|
|
187
|
+
</div>
|
|
188
|
+
)}
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
{/* X-axis labels */}
|
|
192
|
+
{showAxis && (
|
|
193
|
+
<div className="flex justify-between text-[10px] text-theme-text-tertiary mt-1 ml-12 mr-8">
|
|
194
|
+
<span>{timeRange} ago</span>
|
|
195
|
+
<span>now</span>
|
|
196
|
+
</div>
|
|
197
|
+
)}
|
|
198
|
+
|
|
199
|
+
{/* Current value */}
|
|
200
|
+
<div className="flex items-baseline gap-2 mt-1 ml-12">
|
|
201
|
+
<span className={clsx(
|
|
202
|
+
'text-sm font-medium',
|
|
203
|
+
type === 'cpu' ? 'text-blue-400' : 'text-purple-400'
|
|
204
|
+
)}>
|
|
205
|
+
{format(current)}
|
|
206
|
+
</span>
|
|
207
|
+
<span className="text-[10px] text-theme-text-tertiary">
|
|
208
|
+
(max: {format(dataMax)})
|
|
209
|
+
</span>
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Compact sparkline version for inline use
|
|
216
|
+
interface SparklineProps {
|
|
217
|
+
dataPoints: MetricsDataPoint[]
|
|
218
|
+
type: 'cpu' | 'memory'
|
|
219
|
+
width?: number
|
|
220
|
+
height?: number
|
|
221
|
+
className?: string
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function MetricsSparkline({
|
|
225
|
+
dataPoints,
|
|
226
|
+
type,
|
|
227
|
+
width = 80,
|
|
228
|
+
height = 24,
|
|
229
|
+
className,
|
|
230
|
+
}: SparklineProps) {
|
|
231
|
+
const values = useMemo(() => {
|
|
232
|
+
if (!dataPoints || dataPoints.length === 0) return []
|
|
233
|
+
return dataPoints.map(d => type === 'cpu' ? d.cpu : d.memory)
|
|
234
|
+
}, [dataPoints, type])
|
|
235
|
+
|
|
236
|
+
if (values.length < 2) {
|
|
237
|
+
return null
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const max = Math.max(...values)
|
|
241
|
+
const points = values.map((v, i) => {
|
|
242
|
+
const x = (i / (values.length - 1)) * width
|
|
243
|
+
const y = max > 0 ? height - (v / max) * height : height
|
|
244
|
+
return `${x},${y}`
|
|
245
|
+
}).join(' ')
|
|
246
|
+
|
|
247
|
+
const color = type === 'cpu' ? '#3b82f6' : '#a855f7'
|
|
248
|
+
|
|
249
|
+
return (
|
|
250
|
+
<svg width={width} height={height} className={className}>
|
|
251
|
+
<polyline
|
|
252
|
+
fill="none"
|
|
253
|
+
stroke={color}
|
|
254
|
+
strokeWidth="1.5"
|
|
255
|
+
points={points}
|
|
256
|
+
/>
|
|
257
|
+
</svg>
|
|
258
|
+
)
|
|
259
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { clsx } from 'clsx'
|
|
2
|
+
import { type ReactNode } from 'react'
|
|
3
|
+
import { Tooltip } from './Tooltip'
|
|
4
|
+
|
|
5
|
+
type ColorScheme = 'utilization' | 'count'
|
|
6
|
+
|
|
7
|
+
interface ResourceBarProps {
|
|
8
|
+
/** Formatted usage string (e.g., "847m", "2.1Gi", "17") */
|
|
9
|
+
used: string
|
|
10
|
+
/** Formatted total/allocatable string (e.g., "1930m", "7.6Gi", "110") */
|
|
11
|
+
total: string
|
|
12
|
+
/** Usage percentage (0–100) */
|
|
13
|
+
percent: number
|
|
14
|
+
/** Color scheme: "utilization" (green/yellow/red) or "count" (blue/yellow) */
|
|
15
|
+
colorScheme?: ColorScheme
|
|
16
|
+
/** Optional marker line position (0–100), e.g., request as % of limit */
|
|
17
|
+
markerPercent?: number
|
|
18
|
+
/** Optional tooltip content shown on hover */
|
|
19
|
+
tooltip?: ReactNode
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getBarColor(percent: number, scheme: ColorScheme): string {
|
|
23
|
+
if (scheme === 'count') {
|
|
24
|
+
return percent > 90 ? 'bg-yellow-500' : 'bg-blue-500'
|
|
25
|
+
}
|
|
26
|
+
if (percent > 85) return 'bg-red-500'
|
|
27
|
+
if (percent > 60) return 'bg-yellow-500'
|
|
28
|
+
return 'bg-green-500'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function ResourceBar({ used, total, percent, colorScheme = 'utilization', markerPercent, tooltip }: ResourceBarProps) {
|
|
32
|
+
const bar = (
|
|
33
|
+
<div className="flex flex-col gap-0.5 min-w-0">
|
|
34
|
+
<div className="flex items-baseline justify-between gap-1">
|
|
35
|
+
<span className="text-xs font-mono text-theme-text-secondary truncate">
|
|
36
|
+
{used} / {total}
|
|
37
|
+
</span>
|
|
38
|
+
<span className="text-[10px] font-mono text-theme-text-tertiary shrink-0">
|
|
39
|
+
{Math.round(percent)}%
|
|
40
|
+
</span>
|
|
41
|
+
</div>
|
|
42
|
+
<div className="relative">
|
|
43
|
+
<div className="h-1.5 rounded-full border border-theme-border bg-theme-bg-elevated overflow-hidden">
|
|
44
|
+
<div
|
|
45
|
+
className={clsx('h-full rounded-full transition-[width] duration-300 ease-out', getBarColor(percent, colorScheme))}
|
|
46
|
+
style={{ width: `${Math.min(percent, 100)}%` }}
|
|
47
|
+
/>
|
|
48
|
+
</div>
|
|
49
|
+
{markerPercent != null && markerPercent > 0 && markerPercent <= 100 && (
|
|
50
|
+
<div
|
|
51
|
+
className="absolute -top-[1px] h-[calc(100%+2px)] w-[2px] bg-theme-text-primary/70"
|
|
52
|
+
style={{ left: `${markerPercent}%` }}
|
|
53
|
+
/>
|
|
54
|
+
)}
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if (tooltip) {
|
|
60
|
+
return (
|
|
61
|
+
<Tooltip content={tooltip} delay={200} position="top" wrapperClassName="w-full min-w-0">
|
|
62
|
+
{bar}
|
|
63
|
+
</Tooltip>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return bar
|
|
68
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { createContext, useContext, useState, useCallback, ReactNode, useEffect } from 'react'
|
|
2
|
+
import { DURATION_TOAST_EXIT } from '../../utils/animation'
|
|
3
|
+
import { Check, Terminal, X, AlertTriangle } from 'lucide-react'
|
|
4
|
+
import { clsx } from 'clsx'
|
|
5
|
+
|
|
6
|
+
interface Toast {
|
|
7
|
+
id: string
|
|
8
|
+
message: string
|
|
9
|
+
detail?: string
|
|
10
|
+
command?: string
|
|
11
|
+
type?: 'success' | 'info' | 'warning' | 'error'
|
|
12
|
+
position?: { x: number; y: number }
|
|
13
|
+
dismissing?: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface ToastContextType {
|
|
17
|
+
showToast: (message: string, options?: { detail?: string; command?: string; type?: Toast['type']; position?: { x: number; y: number } }) => void
|
|
18
|
+
showCopied: (command: string, label?: string, event?: React.MouseEvent) => void
|
|
19
|
+
showError: (message: string, detail?: string) => void
|
|
20
|
+
showSuccess: (message: string, detail?: string) => void
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const ToastContext = createContext<ToastContextType | null>(null)
|
|
24
|
+
|
|
25
|
+
// Singleton pattern for showing toasts outside React components (e.g., in API error handlers)
|
|
26
|
+
class ToastManager {
|
|
27
|
+
private static instance: ToastManager
|
|
28
|
+
private showErrorFn: ((message: string, detail?: string) => void) | null = null
|
|
29
|
+
private showSuccessFn: ((message: string, detail?: string) => void) | null = null
|
|
30
|
+
|
|
31
|
+
static getInstance(): ToastManager {
|
|
32
|
+
if (!ToastManager.instance) {
|
|
33
|
+
ToastManager.instance = new ToastManager()
|
|
34
|
+
}
|
|
35
|
+
return ToastManager.instance
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
register(showError: typeof this.showErrorFn, showSuccess: typeof this.showSuccessFn) {
|
|
39
|
+
this.showErrorFn = showError
|
|
40
|
+
this.showSuccessFn = showSuccess
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
unregister() {
|
|
44
|
+
this.showErrorFn = null
|
|
45
|
+
this.showSuccessFn = null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
showError(message: string, detail?: string) {
|
|
49
|
+
this.showErrorFn ? this.showErrorFn(message, detail) : console.error('[Toast]', message, detail)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
showSuccess(message: string, detail?: string) {
|
|
53
|
+
this.showSuccessFn ? this.showSuccessFn(message, detail) : console.log('[Toast]', message, detail)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const toastManager = ToastManager.getInstance()
|
|
58
|
+
|
|
59
|
+
export function showApiError(message: string, detail?: string) {
|
|
60
|
+
toastManager.showError(message, detail)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function showApiSuccess(message: string, detail?: string) {
|
|
64
|
+
toastManager.showSuccess(message, detail)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function useToast() {
|
|
68
|
+
const context = useContext(ToastContext)
|
|
69
|
+
if (!context) {
|
|
70
|
+
throw new Error('useToast must be used within ToastProvider')
|
|
71
|
+
}
|
|
72
|
+
return context
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function ToastProvider({ children }: { children: ReactNode }) {
|
|
76
|
+
const [toasts, setToasts] = useState<Toast[]>([])
|
|
77
|
+
|
|
78
|
+
const animateDismiss = useCallback((id: string) => {
|
|
79
|
+
setToasts(prev => prev.map(t => t.id === id ? { ...t, dismissing: true } : t))
|
|
80
|
+
setTimeout(() => {
|
|
81
|
+
setToasts(prev => prev.filter(t => t.id !== id))
|
|
82
|
+
}, DURATION_TOAST_EXIT)
|
|
83
|
+
}, [])
|
|
84
|
+
|
|
85
|
+
const showToast = useCallback((message: string, options?: { detail?: string; command?: string; type?: Toast['type']; position?: { x: number; y: number } }) => {
|
|
86
|
+
const id = Math.random().toString(36).slice(2)
|
|
87
|
+
const toast: Toast = { id, message, ...options }
|
|
88
|
+
|
|
89
|
+
setToasts(prev => [...prev, toast])
|
|
90
|
+
|
|
91
|
+
// Auto-dismiss: errors stay longer (8s), others 5s
|
|
92
|
+
const dismissTime = options?.type === 'error' ? 8000 : 5000
|
|
93
|
+
setTimeout(() => {
|
|
94
|
+
animateDismiss(id)
|
|
95
|
+
}, dismissTime)
|
|
96
|
+
}, [animateDismiss])
|
|
97
|
+
|
|
98
|
+
const showCopied = useCallback((command: string, label?: string, event?: React.MouseEvent) => {
|
|
99
|
+
navigator.clipboard.writeText(command)
|
|
100
|
+
|
|
101
|
+
// Get position from click event
|
|
102
|
+
let position: { x: number; y: number } | undefined
|
|
103
|
+
if (event) {
|
|
104
|
+
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect()
|
|
105
|
+
position = { x: rect.left, y: rect.bottom + 8 }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
showToast(label || 'Copied to clipboard', { command, type: 'success', position })
|
|
109
|
+
}, [showToast])
|
|
110
|
+
|
|
111
|
+
const showError = useCallback((message: string, detail?: string) => {
|
|
112
|
+
showToast(message, { detail, type: 'error' })
|
|
113
|
+
}, [showToast])
|
|
114
|
+
|
|
115
|
+
const showSuccess = useCallback((message: string, detail?: string) => {
|
|
116
|
+
showToast(message, { detail, type: 'success' })
|
|
117
|
+
}, [showToast])
|
|
118
|
+
|
|
119
|
+
const dismissToast = useCallback((id: string) => {
|
|
120
|
+
animateDismiss(id)
|
|
121
|
+
}, [animateDismiss])
|
|
122
|
+
|
|
123
|
+
// Wire up singleton for use outside React components
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
toastManager.register(showError, showSuccess)
|
|
126
|
+
return () => toastManager.unregister()
|
|
127
|
+
}, [showError, showSuccess])
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<ToastContext.Provider value={{ showToast, showCopied, showError, showSuccess }}>
|
|
131
|
+
{children}
|
|
132
|
+
|
|
133
|
+
{/* Render toasts */}
|
|
134
|
+
{toasts.map(toast => (
|
|
135
|
+
<ToastItem key={toast.id} toast={toast} onDismiss={() => dismissToast(toast.id)} />
|
|
136
|
+
))}
|
|
137
|
+
</ToastContext.Provider>
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function ToastItem({ toast, onDismiss }: { toast: Toast; onDismiss: () => void }) {
|
|
142
|
+
// Calculate position - either near button or default to bottom-right
|
|
143
|
+
const style: React.CSSProperties = toast.position
|
|
144
|
+
? {
|
|
145
|
+
position: 'fixed',
|
|
146
|
+
left: Math.min(toast.position.x, window.innerWidth - 420),
|
|
147
|
+
top: toast.position.y,
|
|
148
|
+
zIndex: 50,
|
|
149
|
+
}
|
|
150
|
+
: {
|
|
151
|
+
position: 'fixed',
|
|
152
|
+
bottom: 16,
|
|
153
|
+
right: 16,
|
|
154
|
+
zIndex: 50,
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const isError = toast.type === 'error'
|
|
158
|
+
const isSuccess = toast.type === 'success'
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<div
|
|
162
|
+
className={clsx(
|
|
163
|
+
'flex items-start gap-3 p-3 rounded-lg shadow-2xl border',
|
|
164
|
+
'w-[400px] max-w-[calc(100vw-32px)]',
|
|
165
|
+
toast.dismissing ? 'animate-out' : 'animate-in',
|
|
166
|
+
isError
|
|
167
|
+
? 'bg-red-950/90 border-red-800/50'
|
|
168
|
+
: isSuccess
|
|
169
|
+
? 'bg-emerald-950/90 border-emerald-700/50 dark:bg-emerald-950/90 dark:border-emerald-700/50'
|
|
170
|
+
: 'bg-theme-surface border-theme-border'
|
|
171
|
+
)}
|
|
172
|
+
style={style}
|
|
173
|
+
>
|
|
174
|
+
{/* Icon */}
|
|
175
|
+
<div className={clsx(
|
|
176
|
+
'w-8 h-8 rounded-full flex items-center justify-center shrink-0',
|
|
177
|
+
isError ? 'bg-red-500/20' :
|
|
178
|
+
isSuccess ? 'bg-green-500/20' : 'bg-blue-500/20'
|
|
179
|
+
)}>
|
|
180
|
+
{isError ? (
|
|
181
|
+
<AlertTriangle className="w-4 h-4 text-red-400" />
|
|
182
|
+
) : toast.command ? (
|
|
183
|
+
<Terminal className={clsx('w-4 h-4', isSuccess ? 'text-green-400' : 'text-blue-400')} />
|
|
184
|
+
) : (
|
|
185
|
+
<Check className="w-4 h-4 text-green-400" />
|
|
186
|
+
)}
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
{/* Content */}
|
|
190
|
+
<div className="flex-1 min-w-0">
|
|
191
|
+
<div className="flex items-center gap-2">
|
|
192
|
+
<span className={clsx('text-sm font-medium', isError ? 'text-red-200' : isSuccess ? 'text-emerald-100' : 'text-theme-text-primary')}>
|
|
193
|
+
{toast.message}
|
|
194
|
+
</span>
|
|
195
|
+
{!isError && <Check className={clsx('w-3.5 h-3.5 shrink-0', isSuccess ? 'text-emerald-300' : 'text-green-400')} />}
|
|
196
|
+
</div>
|
|
197
|
+
{toast.detail && (
|
|
198
|
+
<p className={clsx('mt-1 text-xs', isError ? 'text-red-300/80' : isSuccess ? 'text-emerald-300/80' : 'text-theme-text-secondary')}>
|
|
199
|
+
{toast.detail}
|
|
200
|
+
</p>
|
|
201
|
+
)}
|
|
202
|
+
{toast.command && (
|
|
203
|
+
<code className="block mt-1.5 text-xs text-theme-text-secondary font-mono bg-theme-base rounded px-2 py-1.5 whitespace-pre-wrap break-all">
|
|
204
|
+
{toast.command}
|
|
205
|
+
</code>
|
|
206
|
+
)}
|
|
207
|
+
</div>
|
|
208
|
+
|
|
209
|
+
{/* Dismiss button */}
|
|
210
|
+
<button
|
|
211
|
+
onClick={onDismiss}
|
|
212
|
+
className={clsx(
|
|
213
|
+
'p-1 rounded shrink-0',
|
|
214
|
+
isError
|
|
215
|
+
? 'text-red-400 hover:text-red-300 hover:bg-red-900/50'
|
|
216
|
+
: isSuccess
|
|
217
|
+
? 'text-emerald-400 hover:text-emerald-300 hover:bg-emerald-900/50'
|
|
218
|
+
: 'text-theme-text-tertiary hover:text-theme-text-primary hover:bg-theme-elevated'
|
|
219
|
+
)}
|
|
220
|
+
>
|
|
221
|
+
<X className="w-4 h-4" />
|
|
222
|
+
</button>
|
|
223
|
+
</div>
|
|
224
|
+
)
|
|
225
|
+
}
|