@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.
Files changed (193) hide show
  1. package/package.json +63 -0
  2. package/src/components/dock/BottomDock.tsx +204 -0
  3. package/src/components/dock/DockContext.tsx +212 -0
  4. package/src/components/dock/TerminalTab.tsx +333 -0
  5. package/src/components/dock/index.ts +3 -0
  6. package/src/components/gitops/GitOpsActions.tsx +227 -0
  7. package/src/components/gitops/GitOpsStatusBadge.tsx +221 -0
  8. package/src/components/gitops/ManagedResourcesList.tsx +210 -0
  9. package/src/components/gitops/SyncCountdown.tsx +146 -0
  10. package/src/components/gitops/index.ts +4 -0
  11. package/src/components/logs/JsonLogLine.tsx +90 -0
  12. package/src/components/logs/LogCore.tsx +553 -0
  13. package/src/components/logs/LogToolbarSelects.tsx +74 -0
  14. package/src/components/logs/LogsViewer.tsx +146 -0
  15. package/src/components/logs/WorkloadLogsViewer.tsx +263 -0
  16. package/src/components/logs/index.ts +13 -0
  17. package/src/components/logs/useLogBuffer.ts +138 -0
  18. package/src/components/logs/useLogSearch.ts +151 -0
  19. package/src/components/logs/useLogStream.ts +61 -0
  20. package/src/components/resources/ResourcesView.tsx +5494 -0
  21. package/src/components/resources/index.ts +17 -0
  22. package/src/components/resources/renderers/AlertRenderer.tsx +166 -0
  23. package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +233 -0
  24. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +89 -0
  25. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +194 -0
  26. package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +79 -0
  27. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +68 -0
  28. package/src/components/resources/renderers/CertificateRenderer.tsx +209 -0
  29. package/src/components/resources/renderers/CertificateRequestRenderer.tsx +123 -0
  30. package/src/components/resources/renderers/ChallengeRenderer.tsx +111 -0
  31. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +164 -0
  32. package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +140 -0
  33. package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +153 -0
  34. package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +221 -0
  35. package/src/components/resources/renderers/ConfigMapRenderer.tsx +124 -0
  36. package/src/components/resources/renderers/CronJobRenderer.tsx +86 -0
  37. package/src/components/resources/renderers/EventRenderer.tsx +175 -0
  38. package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +119 -0
  39. package/src/components/resources/renderers/ExternalSecretRenderer.tsx +172 -0
  40. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +242 -0
  41. package/src/components/resources/renderers/GRPCRouteRenderer.tsx +263 -0
  42. package/src/components/resources/renderers/GatewayClassRenderer.tsx +68 -0
  43. package/src/components/resources/renderers/GatewayRenderer.tsx +218 -0
  44. package/src/components/resources/renderers/GenericRenderer.tsx +476 -0
  45. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +154 -0
  46. package/src/components/resources/renderers/HPARenderer.tsx +164 -0
  47. package/src/components/resources/renderers/HTTPRouteRenderer.tsx +303 -0
  48. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +167 -0
  49. package/src/components/resources/renderers/IngressClassRenderer.tsx +49 -0
  50. package/src/components/resources/renderers/IngressRenderer.tsx +104 -0
  51. package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +175 -0
  52. package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +176 -0
  53. package/src/components/resources/renderers/IstioGatewayRenderer.tsx +125 -0
  54. package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +84 -0
  55. package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +112 -0
  56. package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +288 -0
  57. package/src/components/resources/renderers/JobRenderer.tsx +103 -0
  58. package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +157 -0
  59. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +188 -0
  60. package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +148 -0
  61. package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +90 -0
  62. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +136 -0
  63. package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +151 -0
  64. package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +70 -0
  65. package/src/components/resources/renderers/KnativeEventingRenderer.tsx +309 -0
  66. package/src/components/resources/renderers/KnativeFlowRenderer.tsx +142 -0
  67. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +232 -0
  68. package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +97 -0
  69. package/src/components/resources/renderers/KnativeRouteRenderer.tsx +74 -0
  70. package/src/components/resources/renderers/KnativeServiceRenderer.tsx +138 -0
  71. package/src/components/resources/renderers/KnativeSourceRenderer.tsx +220 -0
  72. package/src/components/resources/renderers/KustomizationRenderer.tsx +151 -0
  73. package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +352 -0
  74. package/src/components/resources/renderers/LeaseRenderer.tsx +93 -0
  75. package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +208 -0
  76. package/src/components/resources/renderers/NodeRenderer.tsx +282 -0
  77. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +153 -0
  78. package/src/components/resources/renderers/OrderRenderer.tsx +108 -0
  79. package/src/components/resources/renderers/PVCRenderer.tsx +98 -0
  80. package/src/components/resources/renderers/PersistentVolumeRenderer.tsx +138 -0
  81. package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +126 -0
  82. package/src/components/resources/renderers/PodMonitorRenderer.tsx +63 -0
  83. package/src/components/resources/renderers/PodRenderer.tsx +677 -0
  84. package/src/components/resources/renderers/PriorityClassRenderer.tsx +50 -0
  85. package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +62 -0
  86. package/src/components/resources/renderers/ReplicaSetRenderer.tsx +71 -0
  87. package/src/components/resources/renderers/RoleBindingRenderer.tsx +97 -0
  88. package/src/components/resources/renderers/RoleRenderer.tsx +192 -0
  89. package/src/components/resources/renderers/RolloutRenderer.tsx +227 -0
  90. package/src/components/resources/renderers/RuntimeClassRenderer.tsx +75 -0
  91. package/src/components/resources/renderers/SbomReportRenderer.tsx +98 -0
  92. package/src/components/resources/renderers/SealedSecretRenderer.tsx +121 -0
  93. package/src/components/resources/renderers/SecretRenderer.tsx +321 -0
  94. package/src/components/resources/renderers/SecretStoreRenderer.tsx +111 -0
  95. package/src/components/resources/renderers/ServiceAccountRenderer.tsx +81 -0
  96. package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +65 -0
  97. package/src/components/resources/renderers/ServiceRenderer.tsx +98 -0
  98. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +195 -0
  99. package/src/components/resources/renderers/StorageClassRenderer.tsx +82 -0
  100. package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +257 -0
  101. package/src/components/resources/renderers/VPARenderer.tsx +193 -0
  102. package/src/components/resources/renderers/VeleroBSLRenderer.tsx +73 -0
  103. package/src/components/resources/renderers/VeleroBackupRenderer.tsx +193 -0
  104. package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +161 -0
  105. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +134 -0
  106. package/src/components/resources/renderers/VeleroVSLRenderer.tsx +36 -0
  107. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +347 -0
  108. package/src/components/resources/renderers/WebhookConfigRenderer.tsx +156 -0
  109. package/src/components/resources/renderers/WorkflowRenderer.tsx +202 -0
  110. package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +91 -0
  111. package/src/components/resources/renderers/WorkloadRenderer.tsx +311 -0
  112. package/src/components/resources/renderers/argo-cells.tsx +118 -0
  113. package/src/components/resources/renderers/certmanager-cells.tsx +164 -0
  114. package/src/components/resources/renderers/cnpg-cells.tsx +161 -0
  115. package/src/components/resources/renderers/eso-cells.tsx +115 -0
  116. package/src/components/resources/renderers/flux-cells.tsx +253 -0
  117. package/src/components/resources/renderers/index.ts +105 -0
  118. package/src/components/resources/renderers/istio-cells.tsx +210 -0
  119. package/src/components/resources/renderers/karpenter-cells.tsx +107 -0
  120. package/src/components/resources/renderers/keda-cells.tsx +94 -0
  121. package/src/components/resources/renderers/knative-cells.tsx +396 -0
  122. package/src/components/resources/renderers/kyverno-cells.tsx +85 -0
  123. package/src/components/resources/renderers/prometheus-cells.tsx +74 -0
  124. package/src/components/resources/renderers/traefik-cells.tsx +120 -0
  125. package/src/components/resources/renderers/trivy-cells.tsx +153 -0
  126. package/src/components/resources/renderers/trivy-shared.tsx +67 -0
  127. package/src/components/resources/renderers/velero-cells.tsx +164 -0
  128. package/src/components/resources/resource-utils-argo.ts +155 -0
  129. package/src/components/resources/resource-utils-certmanager.ts +198 -0
  130. package/src/components/resources/resource-utils-cnpg.ts +344 -0
  131. package/src/components/resources/resource-utils-eso.ts +385 -0
  132. package/src/components/resources/resource-utils-flux.ts +186 -0
  133. package/src/components/resources/resource-utils-istio.ts +372 -0
  134. package/src/components/resources/resource-utils-karpenter.ts +150 -0
  135. package/src/components/resources/resource-utils-keda.ts +203 -0
  136. package/src/components/resources/resource-utils-knative.ts +218 -0
  137. package/src/components/resources/resource-utils-kyverno.ts +224 -0
  138. package/src/components/resources/resource-utils-prometheus.ts +159 -0
  139. package/src/components/resources/resource-utils-traefik.ts +180 -0
  140. package/src/components/resources/resource-utils-trivy.ts +146 -0
  141. package/src/components/resources/resource-utils-velero.ts +293 -0
  142. package/src/components/resources/resource-utils.ts +1558 -0
  143. package/src/components/shared/EditableYamlView.tsx +302 -0
  144. package/src/components/shared/ResourceActionsBar.tsx +924 -0
  145. package/src/components/shared/ResourceRendererDispatch.tsx +557 -0
  146. package/src/components/shared/index.ts +3 -0
  147. package/src/components/timeline/index.ts +1 -0
  148. package/src/components/timeline/shared.tsx +614 -0
  149. package/src/components/topology/GroupNode.tsx +198 -0
  150. package/src/components/topology/K8sResourceNode.tsx +440 -0
  151. package/src/components/topology/TopologyControls.tsx +62 -0
  152. package/src/components/topology/TopologyFilterSidebar.tsx +377 -0
  153. package/src/components/topology/TopologyGraph.tsx +748 -0
  154. package/src/components/topology/TopologySearch.tsx +304 -0
  155. package/src/components/topology/index.ts +7 -0
  156. package/src/components/topology/layout.ts +734 -0
  157. package/src/components/topology/layout.worker.ts +272 -0
  158. package/src/components/topology/topology.css +129 -0
  159. package/src/components/ui/CodeViewer.tsx +451 -0
  160. package/src/components/ui/ConfirmDialog.tsx +188 -0
  161. package/src/components/ui/ForceDeleteConfirmDialog.tsx +57 -0
  162. package/src/components/ui/HealthRing.tsx +72 -0
  163. package/src/components/ui/MetricsChart.tsx +259 -0
  164. package/src/components/ui/ResourceBar.tsx +68 -0
  165. package/src/components/ui/Toast.tsx +225 -0
  166. package/src/components/ui/Tooltip.tsx +165 -0
  167. package/src/components/ui/YamlEditor.tsx +280 -0
  168. package/src/components/ui/drawer-components.tsx +792 -0
  169. package/src/components/ui/index.ts +10 -0
  170. package/src/components/workload/ResourceDetailDrawer.tsx +158 -0
  171. package/src/components/workload/WorkloadView.tsx +1109 -0
  172. package/src/components/workload/index.ts +2 -0
  173. package/src/hooks/index.ts +9 -0
  174. package/src/hooks/useAnimatedUnmount.ts +30 -0
  175. package/src/hooks/useKeyboardShortcuts.tsx +336 -0
  176. package/src/hooks/useRefreshAnimation.ts +58 -0
  177. package/src/index.ts +35 -0
  178. package/src/types/core.ts +894 -0
  179. package/src/types/gitops.ts +458 -0
  180. package/src/types/index.ts +2 -0
  181. package/src/utils/animation.ts +40 -0
  182. package/src/utils/api-resources.ts +185 -0
  183. package/src/utils/badge-colors.ts +399 -0
  184. package/src/utils/context-name.ts +72 -0
  185. package/src/utils/download.ts +19 -0
  186. package/src/utils/format.ts +233 -0
  187. package/src/utils/index.ts +10 -0
  188. package/src/utils/log-format.ts +300 -0
  189. package/src/utils/navigation.ts +119 -0
  190. package/src/utils/resource-hierarchy.ts +630 -0
  191. package/src/utils/resource-icons.ts +206 -0
  192. package/src/vite-env.d.ts +1 -0
  193. 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
+ }