@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.
- package/package.json +7 -7
- package/src/App.tsx +3 -2
- package/src/api/client.ts +36 -6
- package/src/components/ConnectionErrorView.test.tsx +53 -0
- package/src/components/ConnectionErrorView.tsx +15 -12
- package/src/components/ContextSwitcher.tsx +10 -13
- package/src/components/audit/UpgradeReadinessView.tsx +3 -3
- package/src/components/capacity/ClusterSchedulingCard.tsx +8 -9
- package/src/components/capacity/schedulingBar.test.ts +10 -0
- package/src/components/cost/ApplicationCostTab.test.ts +6 -0
- package/src/components/cost/ApplicationCostTab.tsx +28 -16
- package/src/components/cost/CostTrendChart.tsx +20 -10
- package/src/components/cost/CostView.tsx +79 -28
- package/src/components/cost/CurrentAllocationUse.tsx +6 -4
- package/src/components/cost/WorkloadCostTab.test.ts +10 -0
- package/src/components/cost/WorkloadCostTab.tsx +24 -12
- package/src/components/cost/format.test.ts +27 -8
- package/src/components/cost/format.ts +78 -27
- package/src/components/home/ClusterHealthCard.tsx +3 -0
- package/src/components/home/CostCard.tsx +12 -7
- package/src/components/home/mcpToolCatalog.ts +12 -0
- package/src/components/nav/PrimaryNavRail.tsx +2 -2
- package/src/components/rightsizing/RightsizingScanView.tsx +2 -2
- package/src/components/settings/SettingsDialog.tsx +160 -36
- package/src/components/settings/currency-options.test.ts +49 -0
- package/src/components/settings/currency-options.ts +38 -0
- package/src/components/ui/DiagnosticsOverlay.test.ts +40 -0
- package/src/components/ui/DiagnosticsOverlay.tsx +18 -6
- package/src/components/ui/command-items.ts +4 -14
- package/src/components/workload/WorkloadView.tsx +2 -1
- package/src/main.tsx +4 -114
- package/src/utils/context-name.test.ts +63 -0
- package/src/utils/context-name.ts +22 -0
- package/src/utils/wails-clipboard.test.ts +109 -0
- package/src/utils/wails-clipboard.ts +127 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useEffect, useMemo, useState } from 'react'
|
|
2
|
-
import { AlertCircle,
|
|
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="
|
|
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 (
|
|
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.
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
<
|
|
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">
|
|
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({
|
|
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
|
-
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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 &&
|
|
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
|
-
{
|
|
349
|
+
{currency} · current rates based on last 1h average ·
|
|
340
350
|
*monthly projections assume {COST_HOURS_PER_MONTH} hrs/mo
|
|
351
|
+
{currency !== DEFAULT_COST_CURRENCY && (
|
|
352
|
+
<> · 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={
|
|
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)} /
|
|
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 &&
|
|
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)} /
|
|
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
|
|
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)} /
|
|
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
|
|
771
|
-
|
|
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>
|