Skip to content

Commit cfd0666

Browse files
committed
Polish cost rate presentation
1 parent 7d628c6 commit cfd0666

3 files changed

Lines changed: 121 additions & 57 deletions

File tree

web/src/components/cost/ApplicationCostTab.tsx

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -258,31 +258,38 @@ export function ApplicationCostTab({ app, workloads, onSelectWorkloadCost }: App
258258
tooltip="Actual CPU and memory usage divided by allocated CPU and memory cost across included workloads. Low efficiency usually means requested capacity is sitting idle."
259259
/>
260260
<CostMetricTile
261-
label="Current rate"
262-
value={totals ? formatCostPerHour(hourly) : '—'}
263-
subvalue={totals ? `${formatProjectedDailyRate(hourly)} at this rate` : 'Current allocation unavailable'}
261+
label="Projected daily"
262+
value={totals ? formatProjectedDailyRate(hourly) : '—'}
263+
subvalue={totals ? `${formatCostPerHour(hourly)} current hourly rate` : 'Current allocation unavailable'}
264264
/>
265265
</div>
266266

267267
<section className="rounded-lg border border-theme-border bg-theme-surface/50 p-4">
268268
<div className="mb-3 flex items-center justify-between gap-3">
269269
<div>
270270
<div className="flex items-center gap-1.5">
271-
<div className="text-sm font-semibold text-theme-text-primary">Current split</div>
271+
<div className="text-sm font-semibold text-theme-text-primary">Current allocation split</div>
272272
<CostInfoTooltip content="Last-hour allocated CPU and memory cost summed across included app workloads. Storage/PVC and network attribution remain at namespace and cluster level." />
273273
</div>
274274
<div className="text-xs text-theme-text-tertiary">Last 1h OpenCost allocation window</div>
275275
</div>
276-
<div className="text-sm font-medium text-theme-text-primary tabular-nums">
277-
{totals ? formatCostPerHour(currentSplit) : '—'}
276+
<div className="text-right">
277+
<div className="text-sm font-medium text-theme-text-primary tabular-nums">
278+
{totals ? formatProjectedMonthlyRate(currentSplit) : '—'}
279+
</div>
280+
{totals && (
281+
<div className="text-[10px] text-theme-text-tertiary tabular-nums">
282+
{formatCostPerHour(currentSplit)} current rate
283+
</div>
284+
)}
278285
</div>
279286
</div>
280287
<div className="h-3 overflow-hidden rounded-full bg-theme-hover">
281288
<div className="flex h-full">
282289
{totals && (
283290
<>
284291
<div className="h-full bg-accent" style={{ width: `${cpuPct}%` }} />
285-
<div className="h-full bg-[var(--color-info)]" style={{ width: `${memoryPct}%` }} />
292+
<div className="h-full bg-amber-500" style={{ width: `${memoryPct}%` }} />
286293
</>
287294
)}
288295
</div>
@@ -291,12 +298,12 @@ export function ApplicationCostTab({ app, workloads, onSelectWorkloadCost }: App
291298
<CostLegendItem
292299
colorClass="bg-accent"
293300
label="CPU"
294-
value={totals ? formatCostPerHour(totals.cpuCost ?? 0) : '—'}
301+
value={totals ? formatProjectedMonthlyRate(totals.cpuCost ?? 0) : '—'}
295302
/>
296303
<CostLegendItem
297-
colorClass="bg-[var(--color-info)]"
304+
colorClass="bg-amber-500"
298305
label="Memory"
299-
value={totals ? formatCostPerHour(totals.memoryCost ?? 0) : '—'}
306+
value={totals ? formatProjectedMonthlyRate(totals.memoryCost ?? 0) : '—'}
300307
/>
301308
</div>
302309
</section>
@@ -363,7 +370,8 @@ export function getApplicationCostState(
363370
if (loading) return 'loading'
364371

365372
const reason = current?.reason ?? trend?.reason
366-
if (reason === 'no_prometheus' || reason === 'query_error' || reason === 'access_denied' || reason === 'not_found') return reason
373+
if (reason === 'no_prometheus' || reason === 'query_error' || reason === 'access_denied' || reason === 'not_found')
374+
return reason
367375
return 'no_metrics'
368376
}
369377

@@ -406,12 +414,14 @@ function ApplicationWorkloadCostRow({
406414
<div className="h-1.5 flex-1 overflow-hidden rounded-full bg-theme-hover" style={{ maxWidth: `${barWidth}%` }}>
407415
<div className="flex h-full">
408416
<div className="h-full bg-accent" style={{ width: `${cpuPct}%` }} />
409-
<div className="h-full bg-[var(--color-info)]" style={{ width: `${100 - cpuPct}%` }} />
417+
<div className="h-full bg-amber-500" style={{ width: `${100 - cpuPct}%` }} />
410418
</div>
411419
</div>
412420
</div>
413421
<div className="hidden text-right text-xs tabular-nums text-theme-text-tertiary lg:block">
414-
{current ? `${formatCost(current.cpuCost)} / ${formatCost(current.memoryCost)}` : '—'}
422+
{current
423+
? `${formatProjectedMonthlyCost(current.cpuCost)} / ${formatProjectedMonthlyCost(current.memoryCost)}`
424+
: '—'}
415425
</div>
416426
</>
417427
)
@@ -495,9 +505,9 @@ function ApplicationCostUnavailable({
495505
? 'Cost data is unavailable because these workloads are not accessible with your current permissions.'
496506
: state === 'not_found'
497507
? 'Cost data is unavailable because the referenced workloads no longer exist.'
498-
: state === 'load_error'
499-
? 'Could not load application cost data. Check access to these workloads and try again.'
500-
: 'OpenCost workload metrics were not found for this application.')
508+
: state === 'load_error'
509+
? 'Could not load application cost data. Check access to these workloads and try again.'
510+
: 'OpenCost workload metrics were not found for this application.')
501511
return (
502512
<div className="flex h-full min-h-[320px] items-center justify-center">
503513
<div className="flex max-w-md flex-col items-center gap-3 text-center text-theme-text-secondary">

web/src/components/cost/CostView.tsx

Lines changed: 73 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import { PaneLoader, FreshnessControl } from '@skyhook-io/k8s-ui'
1212
import { CostTrendChart } from './CostTrendChart'
1313
import {
1414
COST_HOURS_PER_MONTH,
15-
formatCost,
1615
formatCostPerHour,
1716
formatProjectedDailyRate,
1817
formatProjectedMonthlyCost,
@@ -29,6 +28,8 @@ interface CostViewProps {
2928
onOpenResource?: (resource: SelectedResource) => void
3029
}
3130

31+
const SYSTEM_COST_NAMESPACES = new Set(['kube-system', 'kube-public', 'kube-node-lease'])
32+
3233
export function CostView({ onBack, onOpenResource }: CostViewProps) {
3334
const { data, isLoading, isFetching, dataUpdatedAt, refetch } = useOpenCostSummary()
3435
const { data: nodeData } = useOpenCostNodes()
@@ -119,6 +120,7 @@ export function CostView({ onBack, onOpenResource }: CostViewProps) {
119120
const totalStorage = data.totalStorageCost ?? 0
120121
const hasStorage = totalStorage > 0
121122
const hasEfficiency = (data.clusterEfficiency ?? 0) > 0
123+
const hasSystemNamespaces = namespaces.some((ns) => isSystemCostNamespace(ns.name))
122124

123125
// Compute split percentages (CPU + Memory + optional Storage)
124126
const allocTotal = totalCpu + totalMem + totalStorage
@@ -131,7 +133,7 @@ export function CostView({ onBack, onOpenResource }: CostViewProps) {
131133

132134
return (
133135
<div className="flex-1 overflow-y-auto">
134-
<div className="mx-auto w-full max-w-[1600px] px-6 py-6 space-y-6">
136+
<div className="mx-auto w-full max-w-[1920px] px-6 py-6 space-y-6">
135137
{/* Header */}
136138
<div className="flex items-center justify-between">
137139
<div className="flex items-center gap-3">
@@ -205,24 +207,24 @@ export function CostView({ onBack, onOpenResource }: CostViewProps) {
205207
<span className="text-xs font-medium text-theme-text-secondary">Cluster Resource Cost</span>
206208
<div className="flex items-center gap-4 text-xs text-theme-text-tertiary">
207209
<span className="flex items-center gap-1.5">
208-
<span className="w-2.5 h-2.5 rounded-sm bg-blue-500" />
209-
CPU {formatCostPerHour(totalCpu)}
210+
<span className="w-2.5 h-2.5 rounded-sm bg-accent" />
211+
CPU {formatProjectedMonthlyRate(totalCpu)}
210212
</span>
211213
<span className="flex items-center gap-1.5">
212-
<span className="w-2.5 h-2.5 rounded-sm bg-purple-500" />
213-
Memory {formatCostPerHour(totalMem)}
214+
<span className="w-2.5 h-2.5 rounded-sm bg-amber-500" />
215+
Memory {formatProjectedMonthlyRate(totalMem)}
214216
</span>
215217
{hasStorage && (
216218
<span className="flex items-center gap-1.5">
217219
<span className="w-2.5 h-2.5 rounded-sm bg-teal-500" />
218-
Storage {formatCostPerHour(totalStorage)}
220+
Storage {formatProjectedMonthlyRate(totalStorage)}
219221
</span>
220222
)}
221223
</div>
222224
</div>
223225
<div className="h-3 rounded-full overflow-hidden bg-theme-hover flex">
224-
<div className="h-full bg-blue-500 transition-all duration-300" style={{ width: `${cpuPct}%` }} />
225-
<div className="h-full bg-purple-500 transition-all duration-300" style={{ width: `${memPct}%` }} />
226+
<div className="h-full bg-accent transition-all duration-300" style={{ width: `${cpuPct}%` }} />
227+
<div className="h-full bg-amber-500 transition-all duration-300" style={{ width: `${memPct}%` }} />
226228
{hasStorage && (
227229
<div className="h-full bg-teal-500 transition-all duration-300" style={{ width: `${storagePct}%` }} />
228230
)}
@@ -243,9 +245,10 @@ export function CostView({ onBack, onOpenResource }: CostViewProps) {
243245
<span className="text-xs text-theme-text-tertiary">{namespaces.length} namespaces</span>
244246
</div>
245247
</div>
248+
{hasSystemNamespaces && <SystemNamespacesCostNote />}
246249

247250
{/* Table header */}
248-
<div className="grid grid-cols-[minmax(180px,1fr)_100px_90px_80px_minmax(160px,1fr)_120px] gap-2 px-4 py-2 border-b border-theme-border text-[11px] font-medium text-theme-text-tertiary uppercase tracking-wider">
251+
<div className="grid grid-cols-[minmax(180px,1fr)_110px_90px_80px_minmax(160px,1fr)_150px] gap-2 px-4 py-2 border-b border-theme-border text-[11px] font-medium text-theme-text-tertiary uppercase tracking-wider">
249252
<span>Namespace</span>
250253
<Tooltip
251254
content="Projected from current hourly rate — not historical spend"
@@ -261,7 +264,12 @@ export function CostView({ onBack, onOpenResource }: CostViewProps) {
261264
<span className="cursor-help">Efficiency</span>
262265
</Tooltip>
263266
<span>CPU / Memory</span>
264-
<span className="text-right">Cost Split</span>
267+
<Tooltip
268+
content="Projected monthly CPU and memory allocation from the current hourly rate"
269+
wrapperClassName="!block text-right"
270+
>
271+
<span className="cursor-help">CPU / Memory/mo*</span>
272+
</Tooltip>
265273
</div>
266274

267275
{/* Namespace rows */}
@@ -320,7 +328,7 @@ function NamespaceCostRow({
320328
<div>
321329
<button
322330
onClick={() => setExpanded(!expanded)}
323-
className="w-full grid grid-cols-[minmax(180px,1fr)_100px_90px_80px_minmax(160px,1fr)_120px] gap-2 px-4 py-2.5 text-left hover:bg-theme-hover/50 transition-colors group"
331+
className="w-full grid grid-cols-[minmax(180px,1fr)_110px_90px_80px_minmax(160px,1fr)_150px] gap-2 px-4 py-2.5 text-left hover:bg-theme-hover/50 transition-colors group"
324332
>
325333
<span className="flex items-center gap-1.5 min-w-0">
326334
{expanded ? (
@@ -331,6 +339,16 @@ function NamespaceCostRow({
331339
<Tooltip content={`Namespace ${ns.name}`} wrapperClassName="min-w-0">
332340
<span className="block truncate text-sm text-theme-text-primary font-medium">{ns.name}</span>
333341
</Tooltip>
342+
{isSystemCostNamespace(ns.name) && (
343+
<Tooltip
344+
content="Usually platform overhead from Kubernetes and cluster add-ons. Review enabled add-ons before treating this as app waste."
345+
wrapperClassName="shrink-0"
346+
>
347+
<span className="rounded bg-theme-elevated px-1.5 py-0.5 text-[10px] font-medium text-theme-text-tertiary">
348+
system
349+
</span>
350+
</Tooltip>
351+
)}
334352
</span>
335353
<span className="text-sm font-medium text-theme-text-primary tabular-nums text-right">
336354
{formatProjectedMonthlyCost(ns.hourlyCost)}
@@ -352,25 +370,50 @@ function NamespaceCostRow({
352370
className="flex-1 h-2 rounded-full overflow-hidden bg-theme-hover flex"
353371
style={{ maxWidth: `${Math.max(barWidth, 3)}%` }}
354372
>
355-
<div className="h-full bg-blue-500/70" style={{ width: `${cpuPct}%` }} />
356-
<div className="h-full bg-purple-500/70" style={{ width: `${memPct}%` }} />
373+
<div className="h-full bg-accent" style={{ width: `${cpuPct}%` }} />
374+
<div className="h-full bg-amber-500" style={{ width: `${memPct}%` }} />
357375
{hasStorage && (ns.storageCost ?? 0) > 0 && (
358-
<div className="h-full bg-teal-500/70" style={{ width: `${100 - cpuPct - memPct}%` }} />
376+
<div className="h-full bg-teal-500" style={{ width: `${100 - cpuPct - memPct}%` }} />
359377
)}
360378
</div>
361379
</span>
362380
<span className="text-[11px] text-theme-text-tertiary tabular-nums text-right">
363-
{formatCost(ns.cpuCost)} / {formatCost(ns.memoryCost)}
364-
{hasStorage && (ns.storageCost ?? 0) > 0 && ` / ${formatCost(ns.storageCost ?? 0)}`}
381+
{formatProjectedMonthlyCost(ns.cpuCost)} / {formatProjectedMonthlyCost(ns.memoryCost)}
382+
{hasStorage && (ns.storageCost ?? 0) > 0 && ` / ${formatProjectedMonthlyCost(ns.storageCost ?? 0)}`}
365383
</span>
366384
</button>
367385

368386
{/* Expanded workload rows */}
387+
{expanded && isSystemCostNamespace(ns.name) && <SystemNamespaceCostNote namespace={ns.name} />}
369388
{expanded && <WorkloadRows namespace={ns.name} onOpenResource={onOpenResource} />}
370389
</div>
371390
)
372391
}
373392

393+
function isSystemCostNamespace(namespace: string): boolean {
394+
return SYSTEM_COST_NAMESPACES.has(namespace)
395+
}
396+
397+
function SystemNamespaceCostNote({ namespace }: { namespace: string }) {
398+
return (
399+
<div className="border-t border-theme-border/30 bg-theme-elevated/30 px-10 py-2 text-xs text-theme-text-tertiary">
400+
<span className="font-medium text-theme-text-secondary">{namespace}</span> is usually baseline cluster overhead:
401+
Kubernetes components, cloud-provider agents, and installed add-ons. Optimize by reviewing add-ons, logging,
402+
monitoring, or node count rather than deleting system workloads directly.
403+
</div>
404+
)
405+
}
406+
407+
function SystemNamespacesCostNote() {
408+
return (
409+
<div className="border-b border-theme-border/50 bg-theme-elevated/30 px-4 py-2 text-xs text-theme-text-tertiary">
410+
Rows marked <span className="font-medium text-theme-text-secondary">system</span> are usually baseline Kubernetes,
411+
cloud-provider, or add-on overhead. Optimize by reviewing enabled add-ons, telemetry, or node count before
412+
treating them as application waste.
413+
</div>
414+
)
415+
}
416+
374417
function WorkloadRows({
375418
namespace,
376419
onOpenResource,
@@ -463,18 +506,18 @@ function WorkloadCostRow({
463506
className="flex-1 h-1.5 rounded-full overflow-hidden bg-theme-hover flex"
464507
style={{ maxWidth: `${Math.max(barWidth, 3)}%` }}
465508
>
466-
<div className="h-full bg-blue-500/50" style={{ width: `${cpuPct}%` }} />
467-
<div className="h-full bg-purple-500/50" style={{ width: `${100 - cpuPct}%` }} />
509+
<div className="h-full bg-accent" style={{ width: `${cpuPct}%` }} />
510+
<div className="h-full bg-amber-500" style={{ width: `${100 - cpuPct}%` }} />
468511
</div>
469512
</span>
470513
<span className="text-[10px] text-theme-text-tertiary tabular-nums text-right">
471-
{formatCost(wl.cpuCost)} / {formatCost(wl.memoryCost)}
514+
{formatProjectedMonthlyCost(wl.cpuCost)} / {formatProjectedMonthlyCost(wl.memoryCost)}
472515
</span>
473516
</>
474517
)
475518

476519
const rowClass =
477-
'grid grid-cols-[minmax(180px,1fr)_100px_90px_80px_minmax(160px,1fr)_120px] gap-2 px-4 py-2 text-left'
520+
'grid grid-cols-[minmax(180px,1fr)_110px_90px_80px_minmax(160px,1fr)_150px] gap-2 px-4 py-2 text-left'
478521

479522
if (canOpen && resource) {
480523
return (
@@ -518,7 +561,7 @@ function NodeCostTable({
518561
</div>
519562

520563
{/* Table header */}
521-
<div className="grid grid-cols-[minmax(200px,1fr)_minmax(120px,1fr)_100px_90px_140px] gap-2 px-4 py-2 border-b border-theme-border text-[11px] font-medium text-theme-text-tertiary uppercase tracking-wider">
564+
<div className="grid grid-cols-[minmax(200px,1fr)_minmax(120px,1fr)_110px_90px_150px] gap-2 px-4 py-2 border-b border-theme-border text-[11px] font-medium text-theme-text-tertiary uppercase tracking-wider">
522565
<span>Node</span>
523566
<span>Instance Type</span>
524567
<Tooltip
@@ -528,7 +571,12 @@ function NodeCostTable({
528571
<span className="cursor-help">Projected/mo*</span>
529572
</Tooltip>
530573
<span className="text-right">Hourly</span>
531-
<span className="text-right">CPU / Memory</span>
574+
<Tooltip
575+
content="Projected monthly CPU and memory portions of this node's current price"
576+
wrapperClassName="!block text-right"
577+
>
578+
<span className="cursor-help">CPU / Memory/mo*</span>
579+
</Tooltip>
532580
</div>
533581

534582
{/* Node rows */}
@@ -552,7 +600,7 @@ function NodeCostRow({
552600
const openNode = () => onOpenResource?.({ kind: 'nodes', namespace: '', name: node.name })
553601

554602
return (
555-
<div className="grid grid-cols-[minmax(200px,1fr)_minmax(120px,1fr)_100px_90px_140px] gap-2 px-4 py-2.5">
603+
<div className="grid grid-cols-[minmax(200px,1fr)_minmax(120px,1fr)_110px_90px_150px] gap-2 px-4 py-2.5">
556604
<span className="flex min-w-0 items-center gap-1.5">
557605
<Tooltip content={`Open Node ${node.name}`} wrapperClassName="!block min-w-0">
558606
{onOpenResource ? (
@@ -591,7 +639,7 @@ function NodeCostRow({
591639
{formatCostPerHour(node.hourlyCost)}
592640
</span>
593641
<span className="text-[11px] text-theme-text-tertiary tabular-nums text-right">
594-
{formatCost(node.cpuCost)} / {formatCost(node.memoryCost)}
642+
{formatProjectedMonthlyCost(node.cpuCost)} / {formatProjectedMonthlyCost(node.memoryCost)}
595643
</span>
596644
</div>
597645
)

0 commit comments

Comments
 (0)