@skyhook-io/radar-app 1.8.7 → 1.8.9
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 +4 -4
- package/src/App.tsx +69 -61
- package/src/RadarApp.tsx +15 -1
- package/src/api/apiResources.ts +1 -1
- package/src/api/client.argoResourceSync.test.ts +69 -0
- package/src/api/client.rightsizing.test.ts +32 -0
- package/src/api/client.ts +1222 -244
- package/src/api/timelineSource.ts +4 -2
- package/src/components/applications/ApplicationsView.tsx +613 -219
- package/src/components/cost/ApplicationCostTab.test.ts +204 -0
- package/src/components/cost/ApplicationCostTab.tsx +571 -0
- package/src/components/cost/CostTrendChart.tsx +103 -72
- package/src/components/cost/CostView.test.ts +12 -0
- package/src/components/cost/CostView.tsx +494 -229
- package/src/components/cost/CostViewTabs.test.tsx +21 -0
- package/src/components/cost/CostViewTabs.tsx +40 -0
- package/src/components/cost/CurrentAllocationUse.test.ts +21 -0
- package/src/components/cost/CurrentAllocationUse.tsx +126 -0
- package/src/components/cost/WorkloadCostTab.test.ts +153 -0
- package/src/components/cost/WorkloadCostTab.tsx +372 -0
- package/src/components/cost/cloud-console.test.ts +39 -0
- package/src/components/cost/cloud-console.ts +81 -0
- package/src/components/cost/errors.ts +8 -0
- package/src/components/cost/format.test.ts +27 -0
- package/src/components/cost/format.ts +46 -0
- package/src/components/cost/kinds.ts +5 -0
- package/src/components/diagnose/AISettings.tsx +7 -12
- package/src/components/diagnose/DiagnoseContext.tsx +1 -0
- package/src/components/diagnose/DiagnoseSurface.tsx +3 -0
- package/src/components/diagnose/InvestigationView.tsx +16 -3
- package/src/components/diagnose/parts.tsx +140 -73
- package/src/components/gitops/ArgoResourceDiffLoader.tsx +23 -0
- package/src/components/gitops/GitOpsView.tsx +81 -14
- package/src/components/gitops/RevisionMetaChip.tsx +63 -0
- package/src/components/helm/HelmCompareRoute.tsx +1 -2
- package/src/components/helm/ManifestDiffViewer.tsx +1 -31
- package/src/components/helm/ValuesDiffPreview.tsx +2 -3
- package/src/components/home/CostCard.tsx +21 -36
- package/src/components/resource/RightsizingStrip.test.ts +109 -0
- package/src/components/resource/RightsizingStrip.tsx +319 -123
- package/src/components/rightsizing/RightsizingScanView.tsx +938 -0
- package/src/components/rightsizing/copy.test.ts +56 -0
- package/src/components/rightsizing/model.test.ts +227 -0
- package/src/components/rightsizing/model.ts +158 -0
- package/src/components/rightsizing/presentation.test.ts +104 -0
- package/src/components/rightsizing/presentation.ts +94 -0
- package/src/components/settings/MyPermissionsDialog.tsx +66 -116
- package/src/components/settings/SettingsDialog.tsx +1268 -318
- package/src/components/timeline/TimelineList.tsx +35 -8
- package/src/components/timeline/TimelineView.tsx +156 -26
- package/src/components/timeline/TimelineView.urlparams.test.ts +43 -2
- package/src/components/workload/WorkloadView.tsx +711 -328
- package/src/context/DiagnoseCustomization.tsx +36 -2
- package/src/index.css +5 -1
- package/src/index.ts +4 -1
- package/src/main.tsx +1 -1
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { X, GitCompare } from 'lucide-react'
|
|
2
|
-
import { PaneLoader } from '@skyhook-io/k8s-ui'
|
|
3
|
-
import { clsx } from 'clsx'
|
|
2
|
+
import { PaneLoader, DiffLine, hasDiffBodyChange } from '@skyhook-io/k8s-ui'
|
|
4
3
|
|
|
5
4
|
interface ManifestDiffViewerProps {
|
|
6
5
|
diff: string
|
|
@@ -68,32 +67,3 @@ export function ManifestDiffViewer({ diff, isLoading, revision1, revision2, onCl
|
|
|
68
67
|
</div>
|
|
69
68
|
)
|
|
70
69
|
}
|
|
71
|
-
|
|
72
|
-
export function hasDiffBodyChange(diff: string): boolean {
|
|
73
|
-
return diff.split('\n').some((line) => {
|
|
74
|
-
if (!line || line.startsWith('---') || line.startsWith('+++') || line.startsWith('@@')) {
|
|
75
|
-
return false
|
|
76
|
-
}
|
|
77
|
-
return line.startsWith('+') || line.startsWith('-')
|
|
78
|
-
})
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function DiffLine({ line }: { line: string }) {
|
|
82
|
-
const isAddition = line.startsWith('+') && !line.startsWith('+++')
|
|
83
|
-
const isRemoval = line.startsWith('-') && !line.startsWith('---')
|
|
84
|
-
const isHeader = line.startsWith('---') || line.startsWith('+++') || line.startsWith('@@')
|
|
85
|
-
|
|
86
|
-
return (
|
|
87
|
-
<div
|
|
88
|
-
className={clsx(
|
|
89
|
-
'whitespace-pre',
|
|
90
|
-
isAddition && 'bg-green-500/10 text-green-700 dark:text-green-400',
|
|
91
|
-
isRemoval && 'bg-red-500/10 text-red-700 dark:text-red-400',
|
|
92
|
-
isHeader && 'text-theme-text-tertiary font-bold',
|
|
93
|
-
!isAddition && !isRemoval && !isHeader && 'text-theme-text-secondary'
|
|
94
|
-
)}
|
|
95
|
-
>
|
|
96
|
-
{line || ' '}
|
|
97
|
-
</div>
|
|
98
|
-
)
|
|
99
|
-
}
|
|
@@ -2,6 +2,7 @@ import { useEffect, useRef } from 'react'
|
|
|
2
2
|
import { createPortal } from 'react-dom'
|
|
3
3
|
import { X, Play, Loader2, FileText, AlertTriangle } from 'lucide-react'
|
|
4
4
|
import { clsx } from 'clsx'
|
|
5
|
+
import { classifyDiffLine } from '@skyhook-io/k8s-ui'
|
|
5
6
|
import type { ValuesPreviewResponse } from '../../types'
|
|
6
7
|
|
|
7
8
|
interface ValuesDiffPreviewProps {
|
|
@@ -169,9 +170,7 @@ interface DiffLineProps {
|
|
|
169
170
|
}
|
|
170
171
|
|
|
171
172
|
function DiffLine({ line, lineNumber }: DiffLineProps) {
|
|
172
|
-
const isAddition =
|
|
173
|
-
const isDeletion = line.startsWith('-') && !line.startsWith('---')
|
|
174
|
-
const isHeader = line.startsWith('@@') || line.startsWith('---') || line.startsWith('+++')
|
|
173
|
+
const { isAddition, isRemoval: isDeletion, isHeader } = classifyDiffLine(line)
|
|
175
174
|
|
|
176
175
|
return (
|
|
177
176
|
<div
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { OpenCostSummary } from '../../api/client'
|
|
2
2
|
import { useOpenCostSummary } from '../../api/client'
|
|
3
3
|
import { DollarSign } from 'lucide-react'
|
|
4
|
+
import {
|
|
5
|
+
formatCostPerHour,
|
|
6
|
+
formatProjectedDailyRate,
|
|
7
|
+
formatProjectedMonthlyCost,
|
|
8
|
+
formatProjectedMonthlyRate,
|
|
9
|
+
} from '../cost/format'
|
|
4
10
|
|
|
5
11
|
export function CostCard({ onNavigate }: { onNavigate?: () => void }) {
|
|
6
12
|
const { data } = useOpenCostSummary()
|
|
@@ -15,7 +21,6 @@ export function CostCard({ onNavigate }: { onNavigate?: () => void }) {
|
|
|
15
21
|
|
|
16
22
|
function CostCardContent({ data, onNavigate }: { data: OpenCostSummary; onNavigate?: () => void }) {
|
|
17
23
|
const hourlyCost = data.totalHourlyCost ?? 0
|
|
18
|
-
const monthlyCost = hourlyCost * 730
|
|
19
24
|
const namespaces = data.namespaces ?? []
|
|
20
25
|
const topNamespaces = namespaces.slice(0, 5)
|
|
21
26
|
|
|
@@ -30,10 +35,10 @@ function CostCardContent({ data, onNavigate }: { data: OpenCostSummary; onNaviga
|
|
|
30
35
|
<div className="flex flex-col h-full w-full">
|
|
31
36
|
<div className="flex items-center justify-between px-5 py-3 border-b border-theme-border/50">
|
|
32
37
|
<div className="flex items-center gap-2">
|
|
33
|
-
<DollarSign className="w-4 h-4 text-
|
|
34
|
-
<span className="text-xs font-semibold uppercase tracking-wider text-
|
|
38
|
+
<DollarSign className="w-4 h-4 text-accent-text" />
|
|
39
|
+
<span className="text-xs font-semibold uppercase tracking-wider text-accent-text">Cost Insights</span>
|
|
35
40
|
{namespaces.length > 0 && (
|
|
36
|
-
<span className="badge-sm
|
|
41
|
+
<span className="badge-sm border border-theme-border bg-accent-muted text-accent-text">
|
|
37
42
|
{namespaces.length} ns
|
|
38
43
|
</span>
|
|
39
44
|
)}
|
|
@@ -45,13 +50,14 @@ function CostCardContent({ data, onNavigate }: { data: OpenCostSummary; onNaviga
|
|
|
45
50
|
<div className="flex items-baseline gap-3 mb-3">
|
|
46
51
|
<div className="flex items-baseline gap-1">
|
|
47
52
|
<span className="text-2xl font-bold text-theme-text-primary tabular-nums">
|
|
48
|
-
{
|
|
53
|
+
{formatProjectedMonthlyCost(hourlyCost)}
|
|
49
54
|
</span>
|
|
50
|
-
<span className="text-xs text-theme-text-tertiary">/
|
|
55
|
+
<span className="text-xs text-theme-text-tertiary">/mo</span>
|
|
51
56
|
</div>
|
|
52
|
-
<div className="flex items-baseline gap-1 text-theme-text-secondary">
|
|
53
|
-
<span className="text-
|
|
54
|
-
<span className="text-[10px] text-theme-text-
|
|
57
|
+
<div className="flex items-baseline gap-1.5 text-theme-text-secondary">
|
|
58
|
+
<span className="text-xs font-medium tabular-nums">{formatProjectedDailyRate(hourlyCost)}</span>
|
|
59
|
+
<span className="text-[10px] text-theme-text-quaternary">·</span>
|
|
60
|
+
<span className="text-xs font-medium tabular-nums">{formatCostPerHour(hourlyCost)}</span>
|
|
55
61
|
</div>
|
|
56
62
|
</div>
|
|
57
63
|
|
|
@@ -63,30 +69,25 @@ function CostCardContent({ data, onNavigate }: { data: OpenCostSummary; onNaviga
|
|
|
63
69
|
<div key={ns.name} className="flex items-center gap-2">
|
|
64
70
|
<span className="text-[11px] text-theme-text-secondary truncate w-24 shrink-0">{ns.name}</span>
|
|
65
71
|
<div className="flex-1 h-2 rounded-full overflow-hidden bg-theme-hover">
|
|
66
|
-
<div
|
|
67
|
-
className="h-full rounded-full bg-indigo-500/60"
|
|
68
|
-
style={{ width: `${Math.max(pct, 2)}%` }}
|
|
69
|
-
/>
|
|
72
|
+
<div className="h-full rounded-full bg-indigo-500/60" style={{ width: `${Math.max(pct, 2)}%` }} />
|
|
70
73
|
</div>
|
|
71
|
-
<span className="text-[10px] text-theme-text-tertiary tabular-nums w-
|
|
72
|
-
{
|
|
74
|
+
<span className="text-[10px] text-theme-text-tertiary tabular-nums w-20 text-right shrink-0">
|
|
75
|
+
{formatProjectedMonthlyRate(ns.hourlyCost)}
|
|
73
76
|
</span>
|
|
74
77
|
</div>
|
|
75
78
|
)
|
|
76
79
|
})}
|
|
77
80
|
{namespaces.length > 5 && (
|
|
78
|
-
<span className="text-[10px] text-theme-text-tertiary">
|
|
79
|
-
+{namespaces.length - 5} more namespaces
|
|
80
|
-
</span>
|
|
81
|
+
<span className="text-[10px] text-theme-text-tertiary">+{namespaces.length - 5} more namespaces</span>
|
|
81
82
|
)}
|
|
82
83
|
</div>
|
|
83
84
|
</div>
|
|
84
85
|
|
|
85
86
|
<div className="px-4 py-1.5 border-t border-theme-border/50 flex items-center justify-between">
|
|
86
87
|
<span className="text-[10px] text-theme-text-tertiary">
|
|
87
|
-
{data.currency ?? 'USD'} · {data.window ?? '1h'} window
|
|
88
|
+
{data.currency ?? 'USD'} · projected monthly from {data.window ?? '1h'} window
|
|
88
89
|
</span>
|
|
89
|
-
<span className="flex items-center gap-1.5 text-[10px] font-semibold uppercase tracking-wider text-
|
|
90
|
+
<span className="flex items-center gap-1.5 text-[10px] font-semibold uppercase tracking-wider text-accent-text">
|
|
90
91
|
OpenCost
|
|
91
92
|
</span>
|
|
92
93
|
</div>
|
|
@@ -94,19 +95,3 @@ function CostCardContent({ data, onNavigate }: { data: OpenCostSummary; onNaviga
|
|
|
94
95
|
</div>
|
|
95
96
|
)
|
|
96
97
|
}
|
|
97
|
-
|
|
98
|
-
function formatCost(value: number): string {
|
|
99
|
-
if (value >= 1000) {
|
|
100
|
-
return `$${(value / 1000).toFixed(1)}k`
|
|
101
|
-
}
|
|
102
|
-
if (value >= 1) {
|
|
103
|
-
return `$${value.toFixed(2)}`
|
|
104
|
-
}
|
|
105
|
-
if (value >= 0.01) {
|
|
106
|
-
return `$${value.toFixed(3)}`
|
|
107
|
-
}
|
|
108
|
-
if (value > 0) {
|
|
109
|
-
return `$${value.toFixed(4)}`
|
|
110
|
-
}
|
|
111
|
-
return '$0.00'
|
|
112
|
-
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import type { RightsizingRow } from '../../api/client'
|
|
3
|
+
import {
|
|
4
|
+
getRightsizingExplanation,
|
|
5
|
+
getRightsizingPresentation,
|
|
6
|
+
RIGHTSIZING_METHODOLOGY,
|
|
7
|
+
RIGHTSIZING_DOCS_URL,
|
|
8
|
+
RIGHTSIZING_SUMMARY,
|
|
9
|
+
} from './RightsizingStrip'
|
|
10
|
+
|
|
11
|
+
const row = (overrides: Partial<RightsizingRow> = {}): RightsizingRow => ({
|
|
12
|
+
container: 'server',
|
|
13
|
+
resource: 'cpu',
|
|
14
|
+
fit: 'balanced',
|
|
15
|
+
confidence: 'high',
|
|
16
|
+
sampleCount: 2016,
|
|
17
|
+
expectedSamples: 2016,
|
|
18
|
+
coverage: 1,
|
|
19
|
+
hpaManaged: false,
|
|
20
|
+
hpaEvidenceAvailable: true,
|
|
21
|
+
oomEvidenceAvailable: true,
|
|
22
|
+
throttleAvailable: true,
|
|
23
|
+
...overrides,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
describe('rightsizing presentation', () => {
|
|
27
|
+
it('keeps fit, confidence, and runtime risk as separate concepts', () => {
|
|
28
|
+
expect(getRightsizingPresentation('oversized')).toEqual({
|
|
29
|
+
label: 'Oversized',
|
|
30
|
+
severity: 'info',
|
|
31
|
+
})
|
|
32
|
+
expect(getRightsizingPresentation('under_requested')).toEqual({
|
|
33
|
+
label: 'Under-requested',
|
|
34
|
+
severity: 'warning',
|
|
35
|
+
})
|
|
36
|
+
expect(getRightsizingPresentation('insufficient_history')).toEqual({
|
|
37
|
+
label: 'Insufficient history',
|
|
38
|
+
severity: 'neutral',
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('labels query failures independently from insufficient history', () => {
|
|
43
|
+
expect(getRightsizingPresentation('insufficient_history', 'usage query failed')).toEqual({
|
|
44
|
+
label: 'Query failed',
|
|
45
|
+
severity: 'error',
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('explains why recommendations are withheld without inventing a zero-risk result', () => {
|
|
50
|
+
expect(getRightsizingExplanation(row({ recommendationReason: 'hpa_managed' }))).toContain(
|
|
51
|
+
'HPA manages cpu',
|
|
52
|
+
)
|
|
53
|
+
expect(
|
|
54
|
+
getRightsizingExplanation(row({ resource: 'memory', recommendationReason: 'oom_evidence' })),
|
|
55
|
+
).toContain('OOM evidence')
|
|
56
|
+
expect(
|
|
57
|
+
getRightsizingExplanation(row({ recommendationReason: 'hpa_evidence_unavailable' })),
|
|
58
|
+
).toContain('could not verify HPA')
|
|
59
|
+
expect(
|
|
60
|
+
getRightsizingExplanation(
|
|
61
|
+
row({
|
|
62
|
+
resource: 'memory',
|
|
63
|
+
recommendationReason: 'oom_evidence_unavailable',
|
|
64
|
+
}),
|
|
65
|
+
),
|
|
66
|
+
).toContain('could not verify recent OOM')
|
|
67
|
+
expect(getRightsizingExplanation(row({ throttleAvailable: false }))).toContain(
|
|
68
|
+
'throttling metrics are unavailable',
|
|
69
|
+
)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('separates the demand target from a conservative reduction step', () => {
|
|
73
|
+
const explanation = getRightsizingExplanation(
|
|
74
|
+
row({
|
|
75
|
+
fit: 'oversized',
|
|
76
|
+
currentRequest: '1',
|
|
77
|
+
recommendedRequest: '500m',
|
|
78
|
+
calculatedRequest: '10m',
|
|
79
|
+
reductionLimited: true,
|
|
80
|
+
bursty: true,
|
|
81
|
+
peak: { name: 'P99', value: 0.2, formatted: '200m' },
|
|
82
|
+
}),
|
|
83
|
+
)
|
|
84
|
+
expect(explanation).toContain('Demand-based target: 10m')
|
|
85
|
+
expect(explanation).toContain('conservative next step')
|
|
86
|
+
expect(explanation).toContain('CPU P99 reached 200m')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('does not revive the misleading efficiency vocabulary', () => {
|
|
90
|
+
const copy = [
|
|
91
|
+
getRightsizingPresentation('balanced').label,
|
|
92
|
+
getRightsizingPresentation('oversized').label,
|
|
93
|
+
getRightsizingExplanation(row({ recommendationReason: 'request_within_fit_range' })),
|
|
94
|
+
]
|
|
95
|
+
.join(' ')
|
|
96
|
+
.toLowerCase()
|
|
97
|
+
expect(copy).not.toContain('efficien')
|
|
98
|
+
expect(copy).not.toContain('waste')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('sets the workload-level scope and methodology without promising savings or changes', () => {
|
|
102
|
+
expect(RIGHTSIZING_SUMMARY).toContain('this workload')
|
|
103
|
+
expect(RIGHTSIZING_SUMMARY).toContain('not a savings estimate or automatic change')
|
|
104
|
+
expect(RIGHTSIZING_METHODOLOGY).toContain('CPU P95 and memory maximum')
|
|
105
|
+
expect(RIGHTSIZING_METHODOLOGY).toContain('Reductions are staged')
|
|
106
|
+
expect(RIGHTSIZING_METHODOLOGY).toContain('Radar does not change requests')
|
|
107
|
+
expect(RIGHTSIZING_DOCS_URL).toContain('/features/rightsizing')
|
|
108
|
+
})
|
|
109
|
+
})
|