@skyhook-io/radar-app 0.2.2 → 0.3.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.
Files changed (177) hide show
  1. package/README.md +7 -1
  2. package/package.json +33 -25
  3. package/src/App.tsx +1449 -382
  4. package/src/RadarApp.tsx +132 -19
  5. package/src/api/apiResources.ts +1 -1
  6. package/src/api/client.argoResourceSync.test.ts +69 -0
  7. package/src/api/client.delta.test.ts +89 -0
  8. package/src/api/client.deltaSync.test.ts +216 -0
  9. package/src/api/client.metrics.test.ts +106 -0
  10. package/src/api/client.rightsizing.test.ts +32 -0
  11. package/src/api/client.ts +2730 -271
  12. package/src/api/client.yaml.test.ts +45 -0
  13. package/src/api/diagnose.ts +289 -0
  14. package/src/api/quotas.ts +16 -0
  15. package/src/api/rbac.ts +57 -0
  16. package/src/api/timelineSource.test.ts +217 -0
  17. package/src/api/timelineSource.ts +582 -0
  18. package/src/components/ConnectionErrorView.tsx +186 -70
  19. package/src/components/ContextSwitcher.tsx +63 -18
  20. package/src/components/DebugOverlay.tsx +5 -3
  21. package/src/components/NamespaceSwitcher.tsx +41 -0
  22. package/src/components/UserMenu.tsx +69 -21
  23. package/src/components/applications/ApplicationsView.tsx +936 -0
  24. package/src/components/audit/AuditSettingsDialog.tsx +79 -17
  25. package/src/components/audit/AuditView.tsx +65 -62
  26. package/src/components/compare/CompareViewRoute.tsx +124 -0
  27. package/src/components/compare/useCompareCandidates.ts +27 -0
  28. package/src/components/compare/useCompareLauncher.tsx +79 -0
  29. package/src/components/cost/ApplicationCostTab.test.ts +204 -0
  30. package/src/components/cost/ApplicationCostTab.tsx +571 -0
  31. package/src/components/cost/CostTrendChart.tsx +106 -75
  32. package/src/components/cost/CostView.test.ts +12 -0
  33. package/src/components/cost/CostView.tsx +507 -223
  34. package/src/components/cost/CostViewTabs.test.tsx +21 -0
  35. package/src/components/cost/CostViewTabs.tsx +40 -0
  36. package/src/components/cost/CurrentAllocationUse.test.ts +21 -0
  37. package/src/components/cost/CurrentAllocationUse.tsx +126 -0
  38. package/src/components/cost/WorkloadCostTab.test.ts +153 -0
  39. package/src/components/cost/WorkloadCostTab.tsx +372 -0
  40. package/src/components/cost/cloud-console.test.ts +39 -0
  41. package/src/components/cost/cloud-console.ts +81 -0
  42. package/src/components/cost/errors.ts +8 -0
  43. package/src/components/cost/format.test.ts +27 -0
  44. package/src/components/cost/format.ts +46 -0
  45. package/src/components/cost/kinds.ts +5 -0
  46. package/src/components/curl/ServiceCurlButton.tsx +445 -0
  47. package/src/components/diagnose/AISettings.tsx +147 -0
  48. package/src/components/diagnose/DiagnoseContext.tsx +495 -0
  49. package/src/components/diagnose/DiagnoseSurface.tsx +394 -0
  50. package/src/components/diagnose/Home.tsx +163 -0
  51. package/src/components/diagnose/InvestigationView.tsx +622 -0
  52. package/src/components/diagnose/LocalDiagnoseAction.tsx +162 -0
  53. package/src/components/diagnose/launch.ts +65 -0
  54. package/src/components/diagnose/parts.tsx +1756 -0
  55. package/src/components/dock/BottomDock.tsx +2 -3
  56. package/src/components/dock/DockContext.tsx +1 -0
  57. package/src/components/dock/TerminalTab.tsx +1 -1
  58. package/src/components/dock/WorkloadLogsTab.tsx +21 -5
  59. package/src/components/dock/index.ts +1 -1
  60. package/src/components/execution/BatchExecutionView.test.ts +170 -0
  61. package/src/components/execution/BatchExecutionView.tsx +1329 -0
  62. package/src/components/execution/batch-run-actions.test.ts +48 -0
  63. package/src/components/execution/batch-run-actions.ts +24 -0
  64. package/src/components/execution/batch-timeline.test.ts +57 -0
  65. package/src/components/execution/batch-timeline.ts +46 -0
  66. package/src/components/execution/execution-definition.test.ts +208 -0
  67. package/src/components/execution/execution-definition.ts +245 -0
  68. package/src/components/gitops/ArgoResourceDiffLoader.tsx +23 -0
  69. package/src/components/gitops/GitOpsView.tsx +1042 -0
  70. package/src/components/gitops/RevisionMetaChip.tsx +63 -0
  71. package/src/components/helm/ChartBrowser.tsx +87 -31
  72. package/src/components/helm/HelmCompareRoute.tsx +1341 -0
  73. package/src/components/helm/HelmReleaseDrawer.test.ts +17 -0
  74. package/src/components/helm/HelmReleaseDrawer.tsx +1073 -102
  75. package/src/components/helm/HelmView.tsx +237 -96
  76. package/src/components/helm/InstallWizard.tsx +94 -38
  77. package/src/components/helm/ManifestDiffViewer.tsx +8 -27
  78. package/src/components/helm/OwnedResources.tsx +34 -59
  79. package/src/components/helm/RevisionHistory.tsx +52 -3
  80. package/src/components/helm/RoleGatedPanel.tsx +3 -3
  81. package/src/components/helm/TrackChartSourceDialog.tsx +185 -0
  82. package/src/components/helm/ValuesDiffPreview.tsx +17 -7
  83. package/src/components/helm/ValuesViewer.tsx +49 -53
  84. package/src/components/helm/helm-utils.ts +4 -0
  85. package/src/components/home/ActivitySummary.tsx +4 -1
  86. package/src/components/home/ClusterHealthCard.tsx +56 -42
  87. package/src/components/home/CostCard.tsx +21 -36
  88. package/src/components/home/GitOpsControllersCard.tsx +110 -0
  89. package/src/components/home/HelmSummary.tsx +3 -1
  90. package/src/components/home/HomeView.tsx +339 -105
  91. package/src/components/home/MCPSetupDialog.tsx +29 -87
  92. package/src/components/home/TrafficSummary.tsx +2 -2
  93. package/src/components/home/mcpToolCatalog.ts +333 -0
  94. package/src/components/issues/IssuesPane.tsx +151 -0
  95. package/src/components/logs/LogsViewer.tsx +4 -1
  96. package/src/components/logs/ScheduledWorkloadLogsViewer.tsx +135 -0
  97. package/src/components/logs/WorkloadLogsViewer.tsx +4 -1
  98. package/src/components/nav/PrimaryNavRail.tsx +285 -0
  99. package/src/components/portforward/PortForwardButton.tsx +118 -47
  100. package/src/components/portforward/PortForwardManager.tsx +253 -131
  101. package/src/components/resource/HPACharts.tsx +237 -0
  102. package/src/components/resource/PVCUsageBar.tsx +59 -0
  103. package/src/components/resource/PrometheusCharts.tsx +160 -584
  104. package/src/components/resource/PrometheusChartsGrid.tsx +270 -0
  105. package/src/components/resource/RestartChart.tsx +133 -0
  106. package/src/components/resource/RightsizingStrip.test.ts +109 -0
  107. package/src/components/resource/RightsizingStrip.tsx +363 -0
  108. package/src/components/resource-drawer/ResourceDrawer.tsx +3 -1
  109. package/src/components/resources/CompositeRenderer.tsx +101 -0
  110. package/src/components/resources/ImageFilesystemModal.tsx +19 -12
  111. package/src/components/resources/PodFilesystemModal.tsx +6 -5
  112. package/src/components/resources/ResourceDetailDrawer.tsx +13 -3
  113. package/src/components/resources/ResourcesView.tsx +194 -17
  114. package/src/components/resources/renderers/CronWorkflowRenderer.tsx +1 -0
  115. package/src/components/resources/renderers/HPARenderer.tsx +20 -1
  116. package/src/components/resources/renderers/NamespaceRenderer.tsx +31 -0
  117. package/src/components/resources/renderers/NodeRenderer.tsx +10 -4
  118. package/src/components/resources/renderers/PVCRenderer.tsx +19 -1
  119. package/src/components/resources/renderers/PodRenderer.tsx +30 -6
  120. package/src/components/resources/renderers/RoleBindingRenderer.tsx +45 -1
  121. package/src/components/resources/renderers/RoleRenderer.tsx +27 -1
  122. package/src/components/resources/renderers/ServiceAccountRenderer.tsx +28 -1
  123. package/src/components/resources/renderers/ServiceRenderer.tsx +81 -8
  124. package/src/components/resources/renderers/WorkloadRenderer.tsx +51 -4
  125. package/src/components/resources/renderers/index.ts +2 -0
  126. package/src/components/resources/resource-utils.ts +2 -1
  127. package/src/components/rightsizing/RightsizingScanView.tsx +938 -0
  128. package/src/components/rightsizing/copy.test.ts +56 -0
  129. package/src/components/rightsizing/model.test.ts +227 -0
  130. package/src/components/rightsizing/model.ts +158 -0
  131. package/src/components/rightsizing/presentation.test.ts +104 -0
  132. package/src/components/rightsizing/presentation.ts +94 -0
  133. package/src/components/settings/MyPermissionsDialog.tsx +241 -0
  134. package/src/components/settings/SettingsDialog.tsx +1505 -165
  135. package/src/components/shared/CreateResourceDialog.tsx +9 -2
  136. package/src/components/shared/LargeClusterNamespacePicker.tsx +3 -3
  137. package/src/components/timeline/LocalTimelineScrubber.tsx +212 -0
  138. package/src/components/timeline/RetainedTimelineScrubber.tsx +311 -0
  139. package/src/components/timeline/TimelineList.tsx +86 -13
  140. package/src/components/timeline/TimelineSwimlanes.tsx +9 -1299
  141. package/src/components/timeline/TimelineView.tsx +873 -24
  142. package/src/components/timeline/TimelineView.urlparams.test.ts +335 -0
  143. package/src/components/traffic/TrafficFilterSidebar.tsx +10 -45
  144. package/src/components/traffic/TrafficFlowList.tsx +29 -15
  145. package/src/components/traffic/TrafficGraph.tsx +42 -24
  146. package/src/components/traffic/TrafficView.tsx +32 -19
  147. package/src/components/ui/CommandPalette.tsx +8 -215
  148. package/src/components/ui/DiagnosticsOverlay.tsx +219 -9
  149. package/src/components/ui/Markdown.tsx +3 -3
  150. package/src/components/ui/Omnibar.tsx +602 -0
  151. package/src/components/ui/RadarOmnibar.tsx +52 -0
  152. package/src/components/ui/SearchSyntaxHelp.tsx +89 -0
  153. package/src/components/ui/ShortcutHelpOverlay.tsx +3 -2
  154. package/src/components/ui/UpdateNotification.tsx +48 -36
  155. package/src/components/ui/command-items.ts +178 -0
  156. package/src/components/workload/WorkloadView.tsx +1342 -158
  157. package/src/context/ConnectionContext.tsx +146 -21
  158. package/src/context/DiagnoseCustomization.tsx +93 -0
  159. package/src/context/NavCustomization.tsx +75 -0
  160. package/src/context/TimelineSource.tsx +50 -0
  161. package/src/contexts/CapabilitiesContext.tsx +32 -8
  162. package/src/filter/FilterLocationBridge.tsx +30 -0
  163. package/src/hooks/useClusterLoadState.ts +73 -0
  164. package/src/hooks/useDocumentTitle.ts +25 -0
  165. package/src/hooks/useEventSource.ts +6 -0
  166. package/src/hooks/useKeyboardShortcuts.tsx +1 -0
  167. package/src/hooks/useMediaQuery.ts +21 -0
  168. package/src/hooks/useNavRailPinned.ts +46 -0
  169. package/src/hooks/useRecentResources.ts +49 -0
  170. package/src/index.css +162 -1
  171. package/src/index.ts +73 -1
  172. package/src/main.tsx +7 -5
  173. package/src/types/clusterLoadState.ts +33 -0
  174. package/src/types.ts +2 -0
  175. package/src/utils/auditBadges.ts +53 -0
  176. package/src/utils/navigation.ts +64 -1
  177. package/src/components/ui/NamespaceSelector.tsx +0 -436
@@ -1,11 +1,8 @@
1
1
  import { useState, useMemo, useRef, useCallback } from 'react'
2
2
  import { clsx } from 'clsx'
3
3
  import { Loader2, TrendingUp } from 'lucide-react'
4
- import {
5
- useOpenCostTrend,
6
- type CostTimeRange,
7
- type OpenCostTrendSeries,
8
- } from '../../api/client'
4
+ import { useOpenCostTrend, type CostTimeRange, type OpenCostTrendSeries } from '../../api/client'
5
+ import { formatCostAxis, formatCostPerHour } from './format'
9
6
 
10
7
  const SERIES_COLORS = [
11
8
  '#3b82f6', // blue-500
@@ -34,7 +31,7 @@ export function CostTrendChart() {
34
31
  <div className="rounded-lg border border-theme-border bg-theme-surface/50 p-4">
35
32
  <div className="flex items-center justify-center h-[200px] text-theme-text-tertiary">
36
33
  <Loader2 className="w-5 h-5 animate-spin mr-2" />
37
- Loading cost trend...
34
+ Loading cost trend
38
35
  </div>
39
36
  </div>
40
37
  )
@@ -49,24 +46,12 @@ export function CostTrendChart() {
49
46
  <div className="flex items-center justify-between px-4 py-2.5 border-b border-theme-border">
50
47
  <div className="flex items-center gap-2">
51
48
  <TrendingUp className="w-4 h-4 text-theme-text-tertiary" />
52
- <span className="text-xs font-medium text-theme-text-secondary">Cost Trend</span>
53
- </div>
54
- <div className="flex items-center gap-1">
55
- {TIME_RANGES.map(tr => (
56
- <button
57
- key={tr.value}
58
- onClick={() => setTimeRange(tr.value)}
59
- className={clsx(
60
- 'px-2 py-1 text-xs rounded-md transition-colors',
61
- timeRange === tr.value
62
- ? 'bg-skyhook-600/20 text-blue-400 font-medium'
63
- : 'text-theme-text-quaternary hover:text-theme-text-tertiary'
64
- )}
65
- >
66
- {tr.label}
67
- </button>
68
- ))}
49
+ <div>
50
+ <div className="text-xs font-medium text-theme-text-secondary">Cost rate trend</div>
51
+ <div className="text-[10px] text-theme-text-tertiary">Historical OpenCost allocation rate ($/hr)</div>
52
+ </div>
69
53
  </div>
54
+ <CostTimeRangeSelector value={timeRange} onChange={setTimeRange} />
70
55
  </div>
71
56
  <div className="p-4">
72
57
  <StackedAreaChart series={data.series} />
@@ -76,7 +61,7 @@ export function CostTrendChart() {
76
61
  )
77
62
  }
78
63
 
79
- function StackedAreaChart({ series }: { series: OpenCostTrendSeries[] }) {
64
+ export function StackedAreaChart({ series }: { series: OpenCostTrendSeries[] }) {
80
65
  const svgRef = useRef<SVGSVGElement>(null)
81
66
  const [hoverX, setHoverX] = useState<number | null>(null)
82
67
 
@@ -106,7 +91,7 @@ function StackedAreaChart({ series }: { series: OpenCostTrendSeries[] }) {
106
91
  const minTs = timestamps[0]
107
92
  const maxTs = timestamps[timestamps.length - 1]
108
93
 
109
- const seriesLookups = series.map(s => {
94
+ const seriesLookups = series.map((s) => {
110
95
  const map = new Map<number, number>()
111
96
  for (const dp of s.dataPoints) {
112
97
  map.set(dp.timestamp, dp.value)
@@ -150,21 +135,47 @@ function StackedAreaChart({ series }: { series: OpenCostTrendSeries[] }) {
150
135
 
151
136
  // Build stacked area paths
152
137
  const paths = series.map((_, si) => {
153
- const topPoints = timestamps.map((ts, ti) => ({ x: toX(ts), y: toY(stacked[si][ti]) }))
154
- const bottomPoints = si > 0
155
- ? timestamps.map((ts, ti) => ({ x: toX(ts), y: toY(stacked[si - 1][ti]) }))
156
- : timestamps.map(ts => ({ x: toX(ts), y: toY(0) }))
138
+ const topPoints = timestamps.map((ts, ti) => ({
139
+ x: toX(ts),
140
+ y: toY(stacked[si][ti]),
141
+ }))
142
+ const bottomPoints =
143
+ si > 0
144
+ ? timestamps.map((ts, ti) => ({
145
+ x: toX(ts),
146
+ y: toY(stacked[si - 1][ti]),
147
+ }))
148
+ : timestamps.map((ts) => ({ x: toX(ts), y: toY(0) }))
157
149
 
158
150
  const topPath = topPoints.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x},${p.y}`).join(' ')
159
- const bottomPath = [...bottomPoints].reverse().map((p, i) => `${i === 0 ? 'L' : 'L'}${p.x},${p.y}`).join(' ')
151
+ const bottomPath = [...bottomPoints]
152
+ .reverse()
153
+ .map((p, i) => `${i === 0 ? 'L' : 'L'}${p.x},${p.y}`)
154
+ .join(' ')
160
155
  const areaPath = topPath + ' ' + bottomPath + ' Z'
161
156
  const linePath = topPoints.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x},${p.y}`).join(' ')
162
157
 
163
- return { areaPath, linePath, color: SERIES_COLORS[si % SERIES_COLORS.length] }
158
+ return {
159
+ areaPath,
160
+ linePath,
161
+ color: SERIES_COLORS[si % SERIES_COLORS.length],
162
+ }
164
163
  })
165
164
 
166
- return { timestamps, stacked, minTs, maxTs, yMax, seriesLookups, toX, toY, yTicks, xTicks, paths }
167
- }, [series])
165
+ return {
166
+ timestamps,
167
+ stacked,
168
+ minTs,
169
+ maxTs,
170
+ yMax,
171
+ seriesLookups,
172
+ toX,
173
+ toY,
174
+ yTicks,
175
+ xTicks,
176
+ paths,
177
+ }
178
+ }, [series, plotHeight, plotWidth])
168
179
 
169
180
  // Hover data — depends on hoverX + chartData, must be a separate hook (called unconditionally)
170
181
  const hoverData = useMemo(() => {
@@ -189,11 +200,15 @@ function StackedAreaChart({ series }: { series: OpenCostTrendSeries[] }) {
189
200
  const points = series.map((s, si) => {
190
201
  const val = seriesLookups[si].get(closestTs) ?? 0
191
202
  total += val
192
- return { namespace: s.namespace, value: val, color: SERIES_COLORS[si % SERIES_COLORS.length] }
203
+ return {
204
+ namespace: s.namespace,
205
+ value: val,
206
+ color: SERIES_COLORS[si % SERIES_COLORS.length],
207
+ }
193
208
  })
194
209
 
195
210
  return { ts: closestTs, x: toX(closestTs), total, points }
196
- }, [hoverX, chartData, series])
211
+ }, [hoverX, chartData, series, plotWidth])
197
212
 
198
213
  const handleMouseMove = useCallback((e: React.MouseEvent<SVGRectElement>) => {
199
214
  const svg = svgRef.current
@@ -210,18 +225,15 @@ function StackedAreaChart({ series }: { series: OpenCostTrendSeries[] }) {
210
225
 
211
226
  return (
212
227
  <div className="relative">
213
- <svg
214
- ref={svgRef}
215
- viewBox={`0 0 ${width} ${height}`}
216
- className="w-full"
217
- preserveAspectRatio="xMidYMid meet"
218
- >
228
+ <svg ref={svgRef} viewBox={`0 0 ${width} ${height}`} className="w-full" preserveAspectRatio="xMidYMid meet">
219
229
  {/* Grid lines */}
220
230
  {yTicks.map((tick, i) => (
221
231
  <line
222
232
  key={`grid-${i}`}
223
- x1={marginLeft} y1={tick.y}
224
- x2={width - marginRight} y2={tick.y}
233
+ x1={marginLeft}
234
+ y1={tick.y}
235
+ x2={width - marginRight}
236
+ y2={tick.y}
225
237
  stroke="currentColor"
226
238
  className="text-theme-border/30"
227
239
  strokeWidth="1"
@@ -250,7 +262,7 @@ function StackedAreaChart({ series }: { series: OpenCostTrendSeries[] }) {
250
262
  key={`xlabel-${i}`}
251
263
  x={tick.x}
252
264
  y={height - 4}
253
- textAnchor="middle"
265
+ textAnchor={i === 0 ? 'start' : i === xTicks.length - 1 ? 'end' : 'middle'}
254
266
  className="fill-theme-text-secondary"
255
267
  fontSize="10"
256
268
  fontFamily="ui-monospace, monospace"
@@ -261,11 +273,7 @@ function StackedAreaChart({ series }: { series: OpenCostTrendSeries[] }) {
261
273
 
262
274
  {/* Stacked area fills (render bottom to top) */}
263
275
  {paths.map((p, i) => (
264
- <path
265
- key={`area-${i}`}
266
- d={p.areaPath}
267
- fill={p.color + '33'}
268
- />
276
+ <path key={`area-${i}`} d={p.areaPath} fill={p.color + '33'} />
269
277
  ))}
270
278
 
271
279
  {/* Lines (top edges of each area) */}
@@ -283,8 +291,10 @@ function StackedAreaChart({ series }: { series: OpenCostTrendSeries[] }) {
283
291
  {/* Hover crosshair */}
284
292
  {hoverData && (
285
293
  <line
286
- x1={hoverData.x} y1={marginTop}
287
- x2={hoverData.x} y2={marginTop + plotHeight}
294
+ x1={hoverData.x}
295
+ y1={marginTop}
296
+ x2={hoverData.x}
297
+ y2={marginTop + plotHeight}
288
298
  stroke="currentColor"
289
299
  className="text-theme-text-tertiary"
290
300
  strokeWidth="1"
@@ -294,8 +304,10 @@ function StackedAreaChart({ series }: { series: OpenCostTrendSeries[] }) {
294
304
 
295
305
  {/* Mouse event overlay */}
296
306
  <rect
297
- x={marginLeft} y={marginTop}
298
- width={plotWidth} height={plotHeight}
307
+ x={marginLeft}
308
+ y={marginTop}
309
+ width={plotWidth}
310
+ height={plotHeight}
299
311
  fill="transparent"
300
312
  style={{ cursor: 'crosshair' }}
301
313
  onMouseMove={handleMouseMove}
@@ -315,28 +327,30 @@ function StackedAreaChart({ series }: { series: OpenCostTrendSeries[] }) {
315
327
  <div className="bg-theme-surface border border-theme-border rounded-lg shadow-lg px-3 py-2 text-xs whitespace-nowrap">
316
328
  <div className="text-theme-text-tertiary mb-1.5 font-mono">
317
329
  {new Date(hoverData.ts * 1000).toLocaleString([], {
318
- month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit',
330
+ month: 'short',
331
+ day: 'numeric',
332
+ hour: '2-digit',
333
+ minute: '2-digit',
319
334
  })}
320
335
  </div>
321
336
  {hoverData.points
322
- .filter(p => p.value > 0)
337
+ .filter((p) => p.value > 0)
323
338
  .sort((a, b) => b.value - a.value)
324
339
  .map((p, i) => (
325
340
  <div key={i} className="flex items-center gap-2 py-0.5">
326
- <div
327
- className="w-2 h-2 rounded-full shrink-0"
328
- style={{ backgroundColor: p.color }}
329
- />
341
+ <div className="w-2 h-2 rounded-full shrink-0" style={{ backgroundColor: p.color }} />
330
342
  <span className="text-theme-text-secondary">{p.namespace}</span>
331
343
  <span className="text-theme-text-primary font-semibold ml-auto pl-3 tabular-nums">
332
344
  {formatCostTooltip(p.value)}
333
345
  </span>
334
346
  </div>
335
347
  ))}
336
- <div className="border-t border-theme-border/50 mt-1 pt-1 flex justify-between text-theme-text-primary font-semibold">
337
- <span>Total</span>
338
- <span className="tabular-nums">{formatCostTooltip(hoverData.total)}</span>
339
- </div>
348
+ {series.length > 1 && (
349
+ <div className="border-t border-theme-border/50 mt-1 pt-1 flex justify-between text-theme-text-primary font-semibold">
350
+ <span>Total</span>
351
+ <span className="tabular-nums">{formatCostTooltip(hoverData.total)}</span>
352
+ </div>
353
+ )}
340
354
  </div>
341
355
  </div>
342
356
  )}
@@ -344,7 +358,7 @@ function StackedAreaChart({ series }: { series: OpenCostTrendSeries[] }) {
344
358
  )
345
359
  }
346
360
 
347
- function ChartLegend({ series }: { series: OpenCostTrendSeries[] }) {
361
+ export function ChartLegend({ series }: { series: OpenCostTrendSeries[] }) {
348
362
  return (
349
363
  <div className="flex flex-wrap gap-x-4 gap-y-1 mt-2">
350
364
  {series.map((s, i) => (
@@ -360,20 +374,37 @@ function ChartLegend({ series }: { series: OpenCostTrendSeries[] }) {
360
374
  )
361
375
  }
362
376
 
363
- function formatCostAxis(value: number): string {
364
- if (value >= 1000) return `$${(value / 1000).toFixed(0)}k`
365
- if (value >= 1) return `$${value.toFixed(1)}`
366
- if (value >= 0.01) return `$${value.toFixed(2)}`
367
- if (value > 0) return `$${value.toFixed(3)}`
368
- return '$0'
377
+ export function CostTimeRangeSelector({
378
+ value,
379
+ onChange,
380
+ }: {
381
+ value: CostTimeRange
382
+ onChange: (value: CostTimeRange) => void
383
+ }) {
384
+ return (
385
+ <div className="flex items-center gap-1">
386
+ {TIME_RANGES.map((range) => (
387
+ <button
388
+ key={range.value}
389
+ type="button"
390
+ aria-pressed={value === range.value}
391
+ onClick={() => onChange(range.value)}
392
+ className={clsx(
393
+ 'rounded-md px-2 py-1 text-xs transition-colors',
394
+ value === range.value
395
+ ? 'bg-accent-muted font-medium text-accent-text'
396
+ : 'text-theme-text-tertiary hover:bg-theme-hover hover:text-theme-text-primary',
397
+ )}
398
+ >
399
+ {range.label}
400
+ </button>
401
+ ))}
402
+ </div>
403
+ )
369
404
  }
370
405
 
371
406
  function formatCostTooltip(value: number): string {
372
- if (value >= 1000) return `$${(value / 1000).toFixed(1)}k/hr`
373
- if (value >= 1) return `$${value.toFixed(2)}/hr`
374
- if (value >= 0.01) return `$${value.toFixed(3)}/hr`
375
- if (value > 0) return `$${value.toFixed(4)}/hr`
376
- return '$0.00/hr'
407
+ return formatCostPerHour(value)
377
408
  }
378
409
 
379
410
  function formatTimestamp(unix: number): string {
@@ -0,0 +1,12 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { resourceKindForCostWorkload } from './CostView'
3
+
4
+ describe('resourceKindForCostWorkload', () => {
5
+ it('does not link standalone aggregate rows as pods', () => {
6
+ expect(resourceKindForCostWorkload('standalone')).toBeNull()
7
+ })
8
+
9
+ it('keeps static pod rows linkable to their mirror pod', () => {
10
+ expect(resourceKindForCostWorkload('staticpod')).toBe('Pod')
11
+ })
12
+ })