@skyhook-io/radar-app 1.11.0 → 1.12.2

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 (35) hide show
  1. package/package.json +7 -7
  2. package/src/App.tsx +3 -2
  3. package/src/api/client.ts +36 -6
  4. package/src/components/ConnectionErrorView.test.tsx +53 -0
  5. package/src/components/ConnectionErrorView.tsx +15 -12
  6. package/src/components/ContextSwitcher.tsx +10 -13
  7. package/src/components/audit/UpgradeReadinessView.tsx +3 -3
  8. package/src/components/capacity/ClusterSchedulingCard.tsx +8 -9
  9. package/src/components/capacity/schedulingBar.test.ts +10 -0
  10. package/src/components/cost/ApplicationCostTab.test.ts +6 -0
  11. package/src/components/cost/ApplicationCostTab.tsx +28 -16
  12. package/src/components/cost/CostTrendChart.tsx +20 -10
  13. package/src/components/cost/CostView.tsx +79 -28
  14. package/src/components/cost/CurrentAllocationUse.tsx +6 -4
  15. package/src/components/cost/WorkloadCostTab.test.ts +10 -0
  16. package/src/components/cost/WorkloadCostTab.tsx +24 -12
  17. package/src/components/cost/format.test.ts +27 -8
  18. package/src/components/cost/format.ts +78 -27
  19. package/src/components/home/ClusterHealthCard.tsx +3 -0
  20. package/src/components/home/CostCard.tsx +12 -7
  21. package/src/components/home/mcpToolCatalog.ts +12 -0
  22. package/src/components/nav/PrimaryNavRail.tsx +2 -2
  23. package/src/components/rightsizing/RightsizingScanView.tsx +2 -2
  24. package/src/components/settings/SettingsDialog.tsx +160 -36
  25. package/src/components/settings/currency-options.test.ts +49 -0
  26. package/src/components/settings/currency-options.ts +38 -0
  27. package/src/components/ui/DiagnosticsOverlay.test.ts +40 -0
  28. package/src/components/ui/DiagnosticsOverlay.tsx +18 -6
  29. package/src/components/ui/command-items.ts +4 -14
  30. package/src/components/workload/WorkloadView.tsx +2 -1
  31. package/src/main.tsx +4 -114
  32. package/src/utils/context-name.test.ts +63 -0
  33. package/src/utils/context-name.ts +22 -0
  34. package/src/utils/wails-clipboard.test.ts +109 -0
  35. package/src/utils/wails-clipboard.ts +127 -0
@@ -1,5 +1,5 @@
1
1
  import { useEffect, useMemo, useState } from 'react'
2
- import { AlertCircle, DollarSign, HelpCircle, Loader2, TrendingUp } from 'lucide-react'
2
+ import { AlertCircle, Coins, HelpCircle, Loader2, TrendingUp } from 'lucide-react'
3
3
  import type { AppRow, AppWorkload } from '@skyhook-io/k8s-ui'
4
4
  import {
5
5
  COST_DISCOVERY_GRACE_MS,
@@ -15,6 +15,7 @@ import {
15
15
  import { Tooltip } from '../ui/Tooltip'
16
16
  import { ChartLegend, CostTimeRangeSelector, StackedAreaChart } from './CostTrendChart'
17
17
  import {
18
+ DEFAULT_COST_CURRENCY,
18
19
  formatCostPerHour,
19
20
  formatHistoricalSpend,
20
21
  formatProjectedDailyRate,
@@ -148,6 +149,8 @@ export function ApplicationCostTab({
148
149
  const hasTrend = points.length >= 2 && points.some((p) => p.value > 0)
149
150
  const rows = current?.workloads ?? []
150
151
  const maxCost = Math.max(...rows.map((row) => row.current?.hourlyCost ?? 0), 0)
152
+ const currentCurrency = current?.currency ?? trend?.currency ?? DEFAULT_COST_CURRENCY
153
+ const trendCurrency = trend?.currency ?? current?.currency ?? DEFAULT_COST_CURRENCY
151
154
 
152
155
  return (
153
156
  <div className="mx-auto w-full max-w-[1600px] space-y-4">
@@ -182,11 +185,11 @@ export function ApplicationCostTab({
182
185
  <div className="text-sm font-semibold text-theme-text-primary">
183
186
  Application compute cost
184
187
  </div>
185
- <CostInfoTooltip content="Dollars are based on OpenCost CPU and memory allocation over time, grouped by the workloads in this application. OpenCost allocation uses the greater of requested or observed resources." />
188
+ <CostInfoTooltip content="Values are based on OpenCost CPU and memory allocation over time, grouped by the workloads in this application. OpenCost allocation uses the greater of requested or observed resources." />
186
189
  </div>
187
190
  <div className="text-xs text-theme-text-tertiary">
188
- OpenCost CPU and memory allocation rate ($/hr) for Deployment, StatefulSet, and
189
- DaemonSet workloads
191
+ OpenCost CPU and memory allocation rate ({trendCurrency}/hr) for Deployment,
192
+ StatefulSet, and DaemonSet workloads
190
193
  </div>
191
194
  </div>
192
195
  </div>
@@ -201,6 +204,7 @@ export function ApplicationCostTab({
201
204
  points.length,
202
205
  trend?.windowTotalCost ?? 0,
203
206
  trendLoading || state === 'partial_missing_history',
207
+ trendCurrency,
204
208
  )}
205
209
  subvalue={
206
210
  state === 'partial_missing_history'
@@ -210,10 +214,10 @@ export function ApplicationCostTab({
210
214
  />
211
215
  <CostMetricBlock
212
216
  label="Projected monthly"
213
- value={totals ? formatProjectedMonthlyCost(hourly) : '—'}
217
+ value={totals ? formatProjectedMonthlyCost(hourly, currentCurrency) : '—'}
214
218
  subvalue={
215
219
  totals
216
- ? `${formatCostPerHour(hourly)} current rate`
220
+ ? `${formatCostPerHour(hourly, currentCurrency)} current rate`
217
221
  : 'Current allocation unavailable'
218
222
  }
219
223
  />
@@ -226,7 +230,7 @@ export function ApplicationCostTab({
226
230
  </div>
227
231
  ) : hasTrend && chartSeries.length > 0 ? (
228
232
  <div className="min-w-0">
229
- <StackedAreaChart series={chartSeries} />
233
+ <StackedAreaChart series={chartSeries} currency={trendCurrency} />
230
234
  <ChartLegend series={chartSeries} />
231
235
  </div>
232
236
  ) : (
@@ -250,16 +254,17 @@ export function ApplicationCostTab({
250
254
  />
251
255
  <CostMetricTile
252
256
  label="Projected daily"
253
- value={totals ? formatProjectedDailyRate(hourly) : '—'}
257
+ value={totals ? formatProjectedDailyRate(hourly, currentCurrency) : '—'}
254
258
  subvalue={
255
259
  totals
256
- ? `${formatCostPerHour(hourly)} current hourly rate`
260
+ ? `${formatCostPerHour(hourly, currentCurrency)} current hourly rate`
257
261
  : 'Current allocation unavailable'
258
262
  }
259
263
  />
260
264
  </div>
261
265
 
262
266
  <CurrentAllocationUse
267
+ currency={currentCurrency}
263
268
  dataAvailable={Boolean(totals)}
264
269
  cpuCost={totals?.cpuCost ?? 0}
265
270
  memoryCost={totals?.memoryCost ?? 0}
@@ -296,6 +301,7 @@ export function ApplicationCostTab({
296
301
  key={applicationCostKey(row)}
297
302
  row={row}
298
303
  maxCost={maxCost}
304
+ currency={currentCurrency}
299
305
  onOpen={
300
306
  appWorkload && onSelectWorkloadCost
301
307
  ? () => onSelectWorkloadCost(appWorkload)
@@ -309,9 +315,13 @@ export function ApplicationCostTab({
309
315
  </section>
310
316
 
311
317
  <div className="text-xs text-theme-text-tertiary">
312
- Powered by OpenCost via Prometheus. Historical spend uses the selected range; projected
313
- monthly values multiply current hourly allocation. Batch/job cost is separate; storage/PVC
314
- and network costs remain at namespace and cluster level.
318
+ Powered by OpenCost via Prometheus.{' '}
319
+ {currentCurrency !== DEFAULT_COST_CURRENCY && (
320
+ <>Labeled {currentCurrency}; no conversion. </>
321
+ )}
322
+ Historical spend uses the selected range; projected monthly values multiply current hourly
323
+ allocation. Batch/job cost is separate; storage/PVC and network costs remain at namespace
324
+ and cluster level.
315
325
  </div>
316
326
  </div>
317
327
  )
@@ -361,10 +371,12 @@ export function applicationCostWorkloads(workloads: AppWorkload[]): AppWorkload[
361
371
  function ApplicationWorkloadCostRow({
362
372
  row,
363
373
  maxCost,
374
+ currency,
364
375
  onOpen,
365
376
  }: {
366
377
  row: OpenCostApplicationWorkloadCost
367
378
  maxCost: number
379
+ currency: string
368
380
  onOpen?: () => void
369
381
  }) {
370
382
  const current = row.current
@@ -392,10 +404,10 @@ function ApplicationWorkloadCostRow({
392
404
  )}
393
405
  </div>
394
406
  <div className="text-right text-sm font-medium tabular-nums text-theme-text-primary">
395
- {current ? formatProjectedMonthlyRate(hourly) : '—'}
407
+ {current ? formatProjectedMonthlyRate(hourly, currency) : '—'}
396
408
  </div>
397
409
  <div className="hidden text-right text-xs tabular-nums text-theme-text-tertiary sm:block">
398
- {current ? formatCostPerHour(hourly) : '—'}
410
+ {current ? formatCostPerHour(hourly, currency) : '—'}
399
411
  </div>
400
412
  <div className="hidden min-w-0 items-center gap-2 md:flex">
401
413
  <div
@@ -410,7 +422,7 @@ function ApplicationWorkloadCostRow({
410
422
  </div>
411
423
  <div className="hidden text-right text-xs tabular-nums text-theme-text-tertiary lg:block">
412
424
  {current
413
- ? `${formatProjectedMonthlyCost(current.cpuCost)} / ${formatProjectedMonthlyCost(current.memoryCost)}`
425
+ ? `${formatProjectedMonthlyCost(current.cpuCost, currency)} / ${formatProjectedMonthlyCost(current.memoryCost, currency)}`
414
426
  : '—'}
415
427
  </div>
416
428
  </>
@@ -512,7 +524,7 @@ function ApplicationCostUnavailable({
512
524
  return (
513
525
  <div className="flex h-full min-h-[320px] items-center justify-center">
514
526
  <div className="flex max-w-md flex-col items-center gap-3 text-center text-theme-text-secondary">
515
- <DollarSign className="h-8 w-8 text-theme-text-tertiary/50" />
527
+ <Coins className="h-8 w-8 text-theme-text-tertiary/50" />
516
528
  <div className="text-sm">{text}</div>
517
529
  </div>
518
530
  </div>
@@ -2,7 +2,7 @@ import { useState, useMemo, useRef, useCallback } from 'react'
2
2
  import { clsx } from 'clsx'
3
3
  import { Loader2, TrendingUp } from 'lucide-react'
4
4
  import { useOpenCostTrend, type CostTimeRange, type OpenCostTrendSeries } from '../../api/client'
5
- import { formatCostAxis, formatCostPerHour } from './format'
5
+ import { DEFAULT_COST_CURRENCY, formatCostAxis, formatCostPerHour } from './format'
6
6
 
7
7
  const SERIES_COLORS = [
8
8
  '#3b82f6', // blue-500
@@ -41,6 +41,8 @@ export function CostTrendChart() {
41
41
  return null
42
42
  }
43
43
 
44
+ const currency = data.currency ?? DEFAULT_COST_CURRENCY
45
+
44
46
  return (
45
47
  <div className="rounded-lg border border-theme-border bg-theme-surface/50">
46
48
  <div className="flex items-center justify-between px-4 py-2.5 border-b border-theme-border">
@@ -48,20 +50,28 @@ export function CostTrendChart() {
48
50
  <TrendingUp className="w-4 h-4 text-theme-text-tertiary" />
49
51
  <div>
50
52
  <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>
53
+ <div className="text-[10px] text-theme-text-tertiary">
54
+ Historical OpenCost allocation rate ({currency}/hr)
55
+ </div>
52
56
  </div>
53
57
  </div>
54
58
  <CostTimeRangeSelector value={timeRange} onChange={setTimeRange} />
55
59
  </div>
56
60
  <div className="p-4">
57
- <StackedAreaChart series={data.series} />
61
+ <StackedAreaChart series={data.series} currency={currency} />
58
62
  <ChartLegend series={data.series} />
59
63
  </div>
60
64
  </div>
61
65
  )
62
66
  }
63
67
 
64
- export function StackedAreaChart({ series }: { series: OpenCostTrendSeries[] }) {
68
+ export function StackedAreaChart({
69
+ series,
70
+ currency,
71
+ }: {
72
+ series: OpenCostTrendSeries[]
73
+ currency: string
74
+ }) {
65
75
  const svgRef = useRef<SVGSVGElement>(null)
66
76
  const [hoverX, setHoverX] = useState<number | null>(null)
67
77
 
@@ -123,7 +133,7 @@ export function StackedAreaChart({ series }: { series: OpenCostTrendSeries[] })
123
133
  const tickCount = 4
124
134
  const yTicks = Array.from({ length: tickCount + 1 }, (_, i) => {
125
135
  const val = (yMax / tickCount) * i
126
- return { val, y: toY(val), label: formatCostAxis(val) }
136
+ return { val, y: toY(val), label: formatCostAxis(val, currency) }
127
137
  })
128
138
 
129
139
  // X axis ticks
@@ -175,7 +185,7 @@ export function StackedAreaChart({ series }: { series: OpenCostTrendSeries[] })
175
185
  xTicks,
176
186
  paths,
177
187
  }
178
- }, [series, plotHeight, plotWidth])
188
+ }, [series, currency, plotHeight, plotWidth])
179
189
 
180
190
  // Hover data — depends on hoverX + chartData, must be a separate hook (called unconditionally)
181
191
  const hoverData = useMemo(() => {
@@ -341,14 +351,14 @@ export function StackedAreaChart({ series }: { series: OpenCostTrendSeries[] })
341
351
  <div className="w-2 h-2 rounded-full shrink-0" style={{ backgroundColor: p.color }} />
342
352
  <span className="text-theme-text-secondary">{p.namespace}</span>
343
353
  <span className="text-theme-text-primary font-semibold ml-auto pl-3 tabular-nums">
344
- {formatCostTooltip(p.value)}
354
+ {formatCostTooltip(p.value, currency)}
345
355
  </span>
346
356
  </div>
347
357
  ))}
348
358
  {series.length > 1 && (
349
359
  <div className="border-t border-theme-border/50 mt-1 pt-1 flex justify-between text-theme-text-primary font-semibold">
350
360
  <span>Total</span>
351
- <span className="tabular-nums">{formatCostTooltip(hoverData.total)}</span>
361
+ <span className="tabular-nums">{formatCostTooltip(hoverData.total, currency)}</span>
352
362
  </div>
353
363
  )}
354
364
  </div>
@@ -403,8 +413,8 @@ export function CostTimeRangeSelector({
403
413
  )
404
414
  }
405
415
 
406
- function formatCostTooltip(value: number): string {
407
- return formatCostPerHour(value)
416
+ function formatCostTooltip(value: number, currency: string): string {
417
+ return formatCostPerHour(value, currency)
408
418
  }
409
419
 
410
420
  function formatTimestamp(unix: number): string {
@@ -15,7 +15,7 @@ import type {
15
15
  import {
16
16
  ChevronDown,
17
17
  ChevronRight,
18
- DollarSign,
18
+ Coins,
19
19
  ExternalLink,
20
20
  HelpCircle,
21
21
  Loader2,
@@ -26,6 +26,7 @@ import { PaneLoader, FreshnessControl, PageHeader } from '@skyhook-io/k8s-ui'
26
26
  import { CostTrendChart } from './CostTrendChart'
27
27
  import {
28
28
  COST_HOURS_PER_MONTH,
29
+ DEFAULT_COST_CURRENCY,
29
30
  formatCostPerHour,
30
31
  formatProjectedDailyRate,
31
32
  formatProjectedMonthlyCost,
@@ -125,7 +126,7 @@ function CostOverview({ onBack, onOpenResource }: CostViewProps) {
125
126
  <CostOverviewState>
126
127
  <div className="flex min-h-64 items-center justify-center">
127
128
  <div className="flex flex-col items-center gap-3 text-theme-text-secondary">
128
- <DollarSign className="w-8 h-8 text-theme-text-tertiary/40" />
129
+ <Coins className="w-8 h-8 text-theme-text-tertiary/40" />
129
130
  <p className="text-sm">{message}</p>
130
131
  <button
131
132
  onClick={onBack}
@@ -152,6 +153,7 @@ function CostOverview({ onBack, onOpenResource }: CostViewProps) {
152
153
  }
153
154
 
154
155
  const hourlyCost = data.totalHourlyCost ?? 0
156
+ const currency = data.currency ?? DEFAULT_COST_CURRENCY
155
157
  const namespaces = data.namespaces ?? []
156
158
  const totalCpu = namespaces.reduce((sum, ns) => sum + ns.cpuCost, 0)
157
159
  const totalMem = namespaces.reduce((sum, ns) => sum + ns.memoryCost, 0)
@@ -166,6 +168,7 @@ function CostOverview({ onBack, onOpenResource }: CostViewProps) {
166
168
  const storagePct = allocTotal > 0 ? (totalStorage / allocTotal) * 100 : 0
167
169
 
168
170
  const nodes = nodeData?.available ? (nodeData.nodes ?? []) : []
171
+ const nodeCurrency = nodeData?.currency ?? currency
169
172
  const clusterConsoleLink = clusterCloudConsoleLink(clusterInfo?.context)
170
173
 
171
174
  return (
@@ -175,7 +178,7 @@ function CostOverview({ onBack, onOpenResource }: CostViewProps) {
175
178
  <div className="flex items-center justify-between">
176
179
  <div className="flex items-center gap-3">
177
180
  <div className="flex items-center gap-2">
178
- <DollarSign className="w-5 h-5 text-indigo-500" />
181
+ <Coins className="w-5 h-5 text-indigo-500" />
179
182
  <h1 className="text-lg font-semibold text-theme-text-primary">Cost Insights</h1>
180
183
  </div>
181
184
  <span className="text-theme-text-quaternary">·</span>
@@ -212,17 +215,17 @@ function CostOverview({ onBack, onOpenResource }: CostViewProps) {
212
215
  <div className="flex flex-col items-end">
213
216
  <div className="flex items-baseline gap-1">
214
217
  <span className="text-2xl font-bold text-theme-text-primary tabular-nums">
215
- {formatProjectedMonthlyCost(hourlyCost)}
218
+ {formatProjectedMonthlyCost(hourlyCost, currency)}
216
219
  </span>
217
220
  <span className="text-xs text-theme-text-tertiary">/mo</span>
218
221
  </div>
219
222
  <div className="mt-0.5 flex items-baseline gap-2 text-theme-text-secondary">
220
223
  <span className="text-sm font-medium tabular-nums">
221
- {formatProjectedDailyRate(hourlyCost)}
224
+ {formatProjectedDailyRate(hourlyCost, currency)}
222
225
  </span>
223
226
  <span className="text-[10px] text-theme-text-quaternary">·</span>
224
227
  <span className="text-sm font-medium tabular-nums">
225
- {formatCostPerHour(hourlyCost)}
228
+ {formatCostPerHour(hourlyCost, currency)}
226
229
  </span>
227
230
  </div>
228
231
  <span className="text-[10px] text-theme-text-quaternary">
@@ -243,16 +246,16 @@ function CostOverview({ onBack, onOpenResource }: CostViewProps) {
243
246
  <div className="flex items-center gap-4 text-xs text-theme-text-tertiary">
244
247
  <span className="flex items-center gap-1.5">
245
248
  <span className="w-2.5 h-2.5 rounded-sm bg-accent" />
246
- CPU {formatProjectedMonthlyRate(totalCpu)}
249
+ CPU {formatProjectedMonthlyRate(totalCpu, currency)}
247
250
  </span>
248
251
  <span className="flex items-center gap-1.5">
249
252
  <span className="w-2.5 h-2.5 rounded-sm bg-amber-500" />
250
- Memory {formatProjectedMonthlyRate(totalMem)}
253
+ Memory {formatProjectedMonthlyRate(totalMem, currency)}
251
254
  </span>
252
255
  {hasStorage && (
253
256
  <span className="flex items-center gap-1.5">
254
257
  <span className="w-2.5 h-2.5 rounded-sm bg-teal-500" />
255
- Storage {formatProjectedMonthlyRate(totalStorage)}
258
+ Storage {formatProjectedMonthlyRate(totalStorage, currency)}
256
259
  </span>
257
260
  )}
258
261
  </div>
@@ -324,6 +327,7 @@ function CostOverview({ onBack, onOpenResource }: CostViewProps) {
324
327
  ns={ns}
325
328
  maxCost={namespaces[0]?.hourlyCost ?? 0}
326
329
  hasStorage={hasStorage}
330
+ currency={currency}
327
331
  onOpenResource={onOpenResource}
328
332
  />
329
333
  ))}
@@ -331,20 +335,29 @@ function CostOverview({ onBack, onOpenResource }: CostViewProps) {
331
335
  </div>
332
336
 
333
337
  {/* Node cost table */}
334
- {nodes.length > 0 && <NodeCostTable nodes={nodes} onOpenResource={onOpenResource} />}
338
+ {nodes.length > 0 && (
339
+ <NodeCostTable
340
+ nodes={nodes}
341
+ currency={nodeCurrency}
342
+ onOpenResource={onOpenResource}
343
+ />
344
+ )}
335
345
 
336
346
  {/* Footer */}
337
347
  <div className="flex items-center justify-between text-xs text-theme-text-tertiary pb-4">
338
348
  <span>
339
- {data.currency ?? 'USD'} &middot; current rates based on last 1h average &middot;
349
+ {currency} &middot; current rates based on last 1h average &middot;
340
350
  *monthly projections assume {COST_HOURS_PER_MONTH} hrs/mo
351
+ {currency !== DEFAULT_COST_CURRENCY && (
352
+ <> &middot; no conversion</>
353
+ )}
341
354
  </span>
342
355
  <span className="text-indigo-500 font-medium">Powered by OpenCost</span>
343
356
  </div>
344
357
  </div>
345
358
 
346
359
  {/* Help dialog */}
347
- {showHelp && <CostHelpDialog onClose={() => setShowHelp(false)} />}
360
+ {showHelp && <CostHelpDialog currency={currency} onClose={() => setShowHelp(false)} />}
348
361
  </div>
349
362
  )
350
363
  }
@@ -354,7 +367,7 @@ function CostOverviewState({ children }: { children: React.ReactNode }) {
354
367
  <div className="flex-1 overflow-y-auto">
355
368
  <div className="mx-auto flex w-full max-w-[1920px] flex-col gap-4 px-6 py-6">
356
369
  <PageHeader
357
- icon={DollarSign}
370
+ icon={Coins}
358
371
  title="Cost Insights"
359
372
  description="Understand current allocation and find CPU and memory requests worth tuning."
360
373
  />
@@ -369,11 +382,13 @@ function NamespaceCostRow({
369
382
  ns,
370
383
  maxCost,
371
384
  hasStorage,
385
+ currency,
372
386
  onOpenResource,
373
387
  }: {
374
388
  ns: OpenCostNamespaceCost
375
389
  maxCost: number
376
390
  hasStorage: boolean
391
+ currency: string
377
392
  onOpenResource?: (resource: SelectedResource) => void
378
393
  }) {
379
394
  const [expanded, setExpanded] = useState(false)
@@ -410,10 +425,10 @@ function NamespaceCostRow({
410
425
  )}
411
426
  </span>
412
427
  <span className="text-sm font-medium text-theme-text-primary tabular-nums text-right">
413
- {formatProjectedMonthlyCost(ns.hourlyCost)}
428
+ {formatProjectedMonthlyCost(ns.hourlyCost, currency)}
414
429
  </span>
415
430
  <span className="text-sm text-theme-text-secondary tabular-nums text-right">
416
- {formatCostPerHour(ns.hourlyCost)}
431
+ {formatCostPerHour(ns.hourlyCost, currency)}
417
432
  </span>
418
433
  <span className="flex items-center gap-2">
419
434
  <div
@@ -428,10 +443,11 @@ function NamespaceCostRow({
428
443
  </div>
429
444
  </span>
430
445
  <span className="text-[11px] text-theme-text-tertiary tabular-nums text-right">
431
- {formatProjectedMonthlyCost(ns.cpuCost)} / {formatProjectedMonthlyCost(ns.memoryCost)}
446
+ {formatProjectedMonthlyCost(ns.cpuCost, currency)} /{' '}
447
+ {formatProjectedMonthlyCost(ns.memoryCost, currency)}
432
448
  {hasStorage &&
433
449
  (ns.storageCost ?? 0) > 0 &&
434
- ` / ${formatProjectedMonthlyCost(ns.storageCost ?? 0)}`}
450
+ ` / ${formatProjectedMonthlyCost(ns.storageCost ?? 0, currency)}`}
435
451
  </span>
436
452
  </button>
437
453
 
@@ -439,7 +455,13 @@ function NamespaceCostRow({
439
455
  {expanded && isSystemCostNamespace(ns.name) && (
440
456
  <SystemNamespaceCostNote namespace={ns.name} />
441
457
  )}
442
- {expanded && <WorkloadRows namespace={ns.name} onOpenResource={onOpenResource} />}
458
+ {expanded && (
459
+ <WorkloadRows
460
+ namespace={ns.name}
461
+ fallbackCurrency={currency}
462
+ onOpenResource={onOpenResource}
463
+ />
464
+ )}
443
465
  </div>
444
466
  )
445
467
  }
@@ -471,9 +493,11 @@ function SystemNamespacesCostNote() {
471
493
 
472
494
  function WorkloadRows({
473
495
  namespace,
496
+ fallbackCurrency,
474
497
  onOpenResource,
475
498
  }: {
476
499
  namespace: string
500
+ fallbackCurrency: string
477
501
  onOpenResource?: (resource: SelectedResource) => void
478
502
  }) {
479
503
  const { data, isLoading } = useOpenCostWorkloads(namespace)
@@ -488,6 +512,7 @@ function WorkloadRows({
488
512
  }
489
513
 
490
514
  const workloads = data?.workloads ?? []
515
+ const currency = data?.currency ?? fallbackCurrency
491
516
  if (workloads.length === 0) {
492
517
  return (
493
518
  <div className="px-4 py-3 text-xs text-theme-text-tertiary bg-theme-elevated/30 pl-10">
@@ -504,6 +529,7 @@ function WorkloadRows({
504
529
  wl={wl}
505
530
  namespace={namespace}
506
531
  maxCost={workloads[0]?.hourlyCost ?? 0}
532
+ currency={currency}
507
533
  onOpenResource={onOpenResource}
508
534
  />
509
535
  ))}
@@ -515,11 +541,13 @@ function WorkloadCostRow({
515
541
  wl,
516
542
  namespace,
517
543
  maxCost,
544
+ currency,
518
545
  onOpenResource,
519
546
  }: {
520
547
  wl: OpenCostWorkloadCost
521
548
  namespace: string
522
549
  maxCost: number
550
+ currency: string
523
551
  onOpenResource?: (resource: SelectedResource) => void
524
552
  }) {
525
553
  const cpuPct = wl.hourlyCost > 0 ? (wl.cpuCost / wl.hourlyCost) * 100 : 50
@@ -541,10 +569,10 @@ function WorkloadCostRow({
541
569
  )}
542
570
  </span>
543
571
  <span className="text-xs font-medium text-theme-text-secondary tabular-nums text-right">
544
- {formatProjectedMonthlyCost(wl.hourlyCost)}
572
+ {formatProjectedMonthlyCost(wl.hourlyCost, currency)}
545
573
  </span>
546
574
  <span className="text-xs text-theme-text-tertiary tabular-nums text-right">
547
- {formatCostPerHour(wl.hourlyCost)}
575
+ {formatCostPerHour(wl.hourlyCost, currency)}
548
576
  </span>
549
577
  <span className="flex items-center gap-2">
550
578
  <div
@@ -556,7 +584,8 @@ function WorkloadCostRow({
556
584
  </div>
557
585
  </span>
558
586
  <span className="text-[10px] text-theme-text-tertiary tabular-nums text-right">
559
- {formatProjectedMonthlyCost(wl.cpuCost)} / {formatProjectedMonthlyCost(wl.memoryCost)}
587
+ {formatProjectedMonthlyCost(wl.cpuCost, currency)} /{' '}
588
+ {formatProjectedMonthlyCost(wl.memoryCost, currency)}
560
589
  </span>
561
590
  </>
562
591
  )
@@ -582,9 +611,11 @@ function WorkloadCostRow({
582
611
 
583
612
  function NodeCostTable({
584
613
  nodes,
614
+ currency,
585
615
  onOpenResource,
586
616
  }: {
587
617
  nodes: OpenCostNodeCost[]
618
+ currency: string
588
619
  onOpenResource?: (resource: SelectedResource) => void
589
620
  }) {
590
621
  return (
@@ -627,7 +658,12 @@ function NodeCostTable({
627
658
  {/* Node rows */}
628
659
  <div className="divide-y divide-theme-border/50">
629
660
  {nodes.map((node) => (
630
- <NodeCostRow key={node.name} node={node} onOpenResource={onOpenResource} />
661
+ <NodeCostRow
662
+ key={node.name}
663
+ node={node}
664
+ currency={currency}
665
+ onOpenResource={onOpenResource}
666
+ />
631
667
  ))}
632
668
  </div>
633
669
  </div>
@@ -636,9 +672,11 @@ function NodeCostTable({
636
672
 
637
673
  function NodeCostRow({
638
674
  node,
675
+ currency,
639
676
  onOpenResource,
640
677
  }: {
641
678
  node: OpenCostNodeCost
679
+ currency: string
642
680
  onOpenResource?: (resource: SelectedResource) => void
643
681
  }) {
644
682
  const cloudLink = nodeCloudConsoleLink(node.providerID)
@@ -680,13 +718,14 @@ function NodeCostRow({
680
718
  {node.region && <span className="text-theme-text-quaternary ml-1.5">({node.region})</span>}
681
719
  </span>
682
720
  <span className="text-sm font-medium text-theme-text-primary tabular-nums text-right">
683
- {formatProjectedMonthlyCost(node.hourlyCost)}
721
+ {formatProjectedMonthlyCost(node.hourlyCost, currency)}
684
722
  </span>
685
723
  <span className="text-sm text-theme-text-secondary tabular-nums text-right">
686
- {formatCostPerHour(node.hourlyCost)}
724
+ {formatCostPerHour(node.hourlyCost, currency)}
687
725
  </span>
688
726
  <span className="text-[11px] text-theme-text-tertiary tabular-nums text-right">
689
- {formatProjectedMonthlyCost(node.cpuCost)} / {formatProjectedMonthlyCost(node.memoryCost)}
727
+ {formatProjectedMonthlyCost(node.cpuCost, currency)} /{' '}
728
+ {formatProjectedMonthlyCost(node.memoryCost, currency)}
690
729
  </span>
691
730
  </div>
692
731
  )
@@ -729,7 +768,7 @@ function apiGroupForCostWorkload(kind: string): string | undefined {
729
768
 
730
769
  // --- Help dialog ---
731
770
 
732
- function CostHelpDialog({ onClose }: { onClose: () => void }) {
771
+ function CostHelpDialog({ currency, onClose }: { currency: string; onClose: () => void }) {
733
772
  useEffect(() => {
734
773
  const handleKeyDown = (e: KeyboardEvent) => {
735
774
  if (e.key === 'Escape') onClose()
@@ -767,8 +806,20 @@ function CostHelpDialog({ onClose }: { onClose: () => void }) {
767
806
  <p>
768
807
  Cost data comes from <strong>OpenCost</strong>, an open-source tool that combines your
769
808
  cloud provider's pricing (how much each node costs per hour) with Kubernetes resource
770
- allocation data. This gives you a dollar value for each workload running on your
771
- cluster.
809
+ allocation data. This gives you a cost value for each workload running on your cluster.
810
+ </p>
811
+ </section>
812
+
813
+ <section>
814
+ <h3 className="text-sm font-semibold text-theme-text-primary mb-1.5">
815
+ Which currency is shown?
816
+ </h3>
817
+ <p>
818
+ Radar labels these values <strong>{currency}</strong> and does not convert them. Auto
819
+ reads <code>currencyCode</code> or <code>DISPLAY_CURRENCY</code> from an active
820
+ OpenCost/Kubecost installation when Prometheus is cluster-discovered, then falls back
821
+ to USD. Override it in <strong>Settings → Cost</strong> or, for automation, with{' '}
822
+ <code>--opencost-currency</code> (Helm: <code>cost.currency</code>).
772
823
  </p>
773
824
  </section>
774
825
 
@@ -4,6 +4,7 @@ import { Tooltip } from '../ui/Tooltip'
4
4
  import { formatCostPerHour, formatProjectedMonthlyRate } from './format'
5
5
 
6
6
  interface CurrentAllocationUseProps {
7
+ currency: string
7
8
  dataAvailable: boolean
8
9
  cpuCost: number
9
10
  memoryCost: number
@@ -32,6 +33,7 @@ export function formatAllocatedUse(
32
33
  }
33
34
 
34
35
  export function CurrentAllocationUse({
36
+ currency,
35
37
  dataAvailable,
36
38
  cpuCost,
37
39
  memoryCost,
@@ -63,11 +65,11 @@ export function CurrentAllocationUse({
63
65
  </div>
64
66
  <div className="text-right">
65
67
  <div className="text-sm font-medium text-theme-text-primary tabular-nums">
66
- {dataAvailable ? formatProjectedMonthlyRate(hourlyCost) : '—'}
68
+ {dataAvailable ? formatProjectedMonthlyRate(hourlyCost, currency) : '—'}
67
69
  </div>
68
70
  {dataAvailable && (
69
71
  <div className="text-[10px] text-theme-text-tertiary tabular-nums">
70
- {formatCostPerHour(hourlyCost)} current rate
72
+ {formatCostPerHour(hourlyCost, currency)} current rate
71
73
  </div>
72
74
  )}
73
75
  </div>
@@ -86,13 +88,13 @@ export function CurrentAllocationUse({
86
88
  <AllocationUseItem
87
89
  colorClass="bg-accent"
88
90
  label="CPU"
89
- allocation={dataAvailable ? formatProjectedMonthlyRate(cpuCost) : '—'}
91
+ allocation={dataAvailable ? formatProjectedMonthlyRate(cpuCost, currency) : '—'}
90
92
  use={formatAllocatedUse(cpuCost, cpuAllocationUse, cpuUsageAvailable, dataAvailable)}
91
93
  />
92
94
  <AllocationUseItem
93
95
  colorClass="bg-amber-500"
94
96
  label="Memory"
95
- allocation={dataAvailable ? formatProjectedMonthlyRate(memoryCost) : '—'}
97
+ allocation={dataAvailable ? formatProjectedMonthlyRate(memoryCost, currency) : '—'}
96
98
  use={formatAllocatedUse(memoryCost, memoryAllocationUse, memoryUsageAvailable, dataAvailable)}
97
99
  />
98
100
  </div>