@skyhook-io/radar-app 1.14.4 → 1.14.6
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/README.md +2 -9
- package/package.json +6 -6
- package/src/App.tsx +91 -328
- package/src/RadarApp.tsx +3 -8
- package/src/api/client.jobset.test.ts +18 -0
- package/src/api/client.resource-identity.test.ts +44 -0
- package/src/api/client.rightsizing.test.ts +7 -3
- package/src/api/client.ts +233 -78
- package/src/api/preferences.test.ts +54 -0
- package/src/api/preferences.ts +38 -0
- package/src/components/CloudFunnelButton.test.tsx +155 -0
- package/src/components/CloudFunnelButton.tsx +14 -8
- package/src/components/ContextSwitcher.tsx +13 -132
- package/src/components/applications/ApplicationsView.identity.test.ts +39 -0
- package/src/components/applications/ApplicationsView.tsx +5 -3
- package/src/components/audit/AuditSettingsDialog.tsx +15 -11
- package/src/components/diagnose/AIMarkdown.tsx +8 -3
- package/src/components/diagnose/AgentControls.tsx +26 -3
- package/src/components/diagnose/AssessmentCard.tsx +249 -237
- package/src/components/diagnose/DiagnoseContext.tsx +1 -0
- package/src/components/diagnose/InvestigationEvidencePane.test.tsx +47 -0
- package/src/components/diagnose/InvestigationEvidencePane.tsx +15 -2
- package/src/components/diagnose/InvestigationEvidenceSections.tsx +1 -1
- package/src/components/diagnose/InvestigationView.tsx +4 -1
- package/src/components/diagnose/agentCatalog.ts +6 -0
- package/src/components/diagnose/investigationCase.test.tsx +56 -0
- package/src/components/diagnose/investigationEvidence/adapters/logs.test.ts +60 -0
- package/src/components/diagnose/investigationEvidence/adapters/logs.ts +7 -3
- package/src/components/diagnose/investigationEvidence/bodies/streams.tsx +1 -1
- package/src/components/diagnose/investigationEvidence/observations.ts +8 -5
- package/src/components/diagnose/investigationEvidence/types.ts +1 -1
- package/src/components/diagnose/launch.test.ts +23 -0
- package/src/components/diagnose/launch.ts +4 -0
- package/src/components/diagnose/parts.test.tsx +159 -2
- package/src/components/dock/NodeTerminalTab.test.tsx +34 -0
- package/src/components/dock/NodeTerminalTab.tsx +5 -3
- package/src/components/dock/WorkloadLogsTab.test.tsx +66 -0
- package/src/components/dock/WorkloadLogsTab.tsx +2 -1
- package/src/components/execution/BatchExecutionView.render.test.tsx +2 -1
- package/src/components/execution/BatchExecutionView.test.ts +52 -1
- package/src/components/execution/BatchExecutionView.tsx +324 -65
- package/src/components/execution/JobSetAdmission.test.tsx +42 -0
- package/src/components/execution/JobSetAdmission.tsx +17 -0
- package/src/components/execution/JobSetMemberComparison.test.tsx +30 -0
- package/src/components/execution/JobSetMemberComparison.tsx +88 -0
- package/src/components/execution/batch-run-actions.test.ts +1 -0
- package/src/components/execution/batch-timeline.test.ts +3 -0
- package/src/components/execution/execution-definition.test.ts +15 -1
- package/src/components/execution/execution-definition.ts +4 -0
- package/src/components/execution/member-collection.render.test.tsx +142 -0
- package/src/components/gitops/GitOpsView.tsx +33 -2
- package/src/components/gitops/destination-toast.test.ts +33 -0
- package/src/components/gitops/destination-toast.ts +46 -0
- package/src/components/gitops/useDestinationCluster.ts +30 -0
- package/src/components/helm/TrackChartSourceDialog.tsx +20 -11
- package/src/components/home/MCPSetupDialog.tsx +10 -0
- package/src/components/logs/ScheduledWorkloadLogsViewer.tsx +75 -22
- package/src/components/logs/WorkloadLogsViewer.tsx +7 -4
- package/src/components/nav/PrimaryNavRail.tsx +2 -5
- package/src/components/resource/PVCUsageBar.render.test.tsx +168 -0
- package/src/components/resource/PVCUsageBar.tsx +62 -16
- package/src/components/resource/PrometheusChartsGrid.render.test.tsx +1 -1
- package/src/components/resources/ResourcesView.tsx +3 -3
- package/src/components/resources/renderers/CAPIClusterRenderer.tsx +8 -0
- package/src/components/resources/renderers/KueueWorkloadRenderer.test.tsx +39 -0
- package/src/components/resources/renderers/KueueWorkloadRenderer.tsx +37 -0
- package/src/components/resources/renderers/RayClusterRenderer.test.tsx +18 -0
- package/src/components/resources/renderers/RayClusterRenderer.tsx +17 -0
- package/src/components/resources/renderers/RayServiceRenderer.test.tsx +69 -0
- package/src/components/resources/renderers/RayServiceRenderer.tsx +38 -0
- package/src/components/rightsizing/RightsizingScanView.tsx +207 -83
- package/src/components/rightsizing/copy.test.ts +7 -0
- package/src/components/rightsizing/empty-state.test.tsx +67 -0
- package/src/components/rightsizing/model.test.ts +120 -2
- package/src/components/rightsizing/model.ts +36 -13
- package/src/components/rightsizing/notices.test.tsx +176 -0
- package/src/components/settings/OperatorManagedNotice.tsx +17 -0
- package/src/components/settings/SettingsDialog.tsx +157 -42
- package/src/components/settings/settings-state.test.ts +18 -0
- package/src/components/settings/settings-state.ts +18 -0
- package/src/components/timeline/TimelineView.tsx +9 -3
- package/src/components/timeline/TimelineView.urlparams.test.ts +18 -1
- package/src/components/ui/Markdown.tsx +33 -6
- package/src/components/ui/command-items.ts +0 -3
- package/src/components/useContextSwitchFlow.tsx +147 -0
- package/src/components/workload/WorkloadView.test.ts +17 -1
- package/src/components/workload/WorkloadView.tsx +36 -18
- package/src/context/ContextSwitchContext.tsx +18 -2
- package/src/context/NavCustomization.tsx +4 -58
- package/src/context/ThemeContext.tsx +3 -11
- package/src/hooks/useClusterLoadState.ts +2 -2
- package/src/hooks/useFavorites.ts +3 -5
- package/src/utils/auditBadges.ts +1 -1
- package/src/utils/navigation.test.ts +18 -1
- package/src/utils/navigation.ts +12 -5
- package/src/utils/topology-selection.test.ts +56 -0
- package/src/utils/topology-selection.ts +9 -9
- package/src/components/ui/CommandPalette.tsx +0 -261
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
import type { RightsizingScanResponse, RightsizingRow } from '../../api/client'
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
calculateImpact,
|
|
5
|
+
classifyRows,
|
|
6
|
+
flattenScanResults,
|
|
7
|
+
isActionableClass,
|
|
8
|
+
scanClassCounts,
|
|
9
|
+
} from './model'
|
|
4
10
|
|
|
5
11
|
function metric(overrides: Partial<RightsizingRow> = {}): RightsizingRow {
|
|
6
12
|
return {
|
|
@@ -25,6 +31,7 @@ function response(
|
|
|
25
31
|
scaledToZero = false,
|
|
26
32
|
): RightsizingScanResponse {
|
|
27
33
|
return {
|
|
34
|
+
scanId: 'rs_test', scanStatus: 'finished', deadlineAt: '2026-09-19T10:03:00Z',
|
|
28
35
|
state: 'complete',
|
|
29
36
|
scannedAt: '2026-07-12T10:00:00Z',
|
|
30
37
|
window: '7d',
|
|
@@ -34,7 +41,7 @@ function response(
|
|
|
34
41
|
workloadsEvaluated: 1,
|
|
35
42
|
workloadsWithData: 1,
|
|
36
43
|
batches: 1,
|
|
37
|
-
completedBatches: 1,
|
|
44
|
+
attemptedBatches: 1, completedBatches: 1,
|
|
38
45
|
},
|
|
39
46
|
workloads: [
|
|
40
47
|
{
|
|
@@ -152,6 +159,77 @@ describe('rightsizing scan model', () => {
|
|
|
152
159
|
expect(classifyRows([metric({ queryError: 'query failed' })])).toBe('need_data')
|
|
153
160
|
})
|
|
154
161
|
|
|
162
|
+
describe.each([
|
|
163
|
+
['failed query', { queryError: 'usage query failed' }],
|
|
164
|
+
['short history', { fit: 'insufficient_history' as const }],
|
|
165
|
+
])('with %s on one resource', (_label, missing) => {
|
|
166
|
+
it.each([
|
|
167
|
+
[
|
|
168
|
+
'increase',
|
|
169
|
+
{
|
|
170
|
+
fit: 'under_requested' as const,
|
|
171
|
+
recommendedRequest: '1Gi',
|
|
172
|
+
recommendedRequestValue: 1024 ** 3,
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
['review', { currentPodOOM: true }],
|
|
176
|
+
['review', { hpaManaged: true, recommendationReason: 'hpa_managed' }],
|
|
177
|
+
[
|
|
178
|
+
'reduction',
|
|
179
|
+
{
|
|
180
|
+
fit: 'oversized' as const,
|
|
181
|
+
currentRequestValue: 1024 ** 3,
|
|
182
|
+
recommendedRequest: '512Mi',
|
|
183
|
+
recommendedRequestValue: 512 * 1024 ** 2,
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
['need_data', {}],
|
|
187
|
+
])('retains %s from the other resource', (classification, known) => {
|
|
188
|
+
const cpu = metric(missing)
|
|
189
|
+
const memory = metric({ resource: 'memory', ...known })
|
|
190
|
+
const rows = flattenScanResults(response([cpu, memory]))
|
|
191
|
+
expect(rows).toHaveLength(1)
|
|
192
|
+
expect(rows[0].classification).toBe(classification)
|
|
193
|
+
expect(rows[0].cpu).toBe(cpu)
|
|
194
|
+
expect(rows[0].memory).toBe(memory)
|
|
195
|
+
expect(scanClassCounts(rows)[rows[0].classification]).toBe(1)
|
|
196
|
+
const actions = rows.filter((row) => isActionableClass(row.classification))
|
|
197
|
+
expect(actions).toHaveLength(classification === 'need_data' ? 0 : 1)
|
|
198
|
+
})
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('retains a known CPU increase when memory has no evidence', () => {
|
|
202
|
+
const cpu = metric({
|
|
203
|
+
fit: 'under_requested',
|
|
204
|
+
recommendedRequest: '200m',
|
|
205
|
+
recommendedRequestValue: 0.2,
|
|
206
|
+
currentRequestValue: 0.1,
|
|
207
|
+
})
|
|
208
|
+
const memory = metric({ resource: 'memory', fit: 'insufficient_history' })
|
|
209
|
+
const [row] = flattenScanResults(response([cpu, memory], 3))
|
|
210
|
+
expect(row.classification).toBe('increase')
|
|
211
|
+
expect(row.impact.cpuChange).toBeCloseTo(0.3)
|
|
212
|
+
expect(row.impact.memoryChange).toBe(0)
|
|
213
|
+
expect(row.memory).toBe(memory)
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('keeps empty and entirely unevidenced containers in need_data', () => {
|
|
217
|
+
expect(classifyRows([])).toBe('need_data')
|
|
218
|
+
expect(
|
|
219
|
+
classifyRows([
|
|
220
|
+
metric({ queryError: 'usage query failed' }),
|
|
221
|
+
metric({ resource: 'memory', fit: 'insufficient_history' }),
|
|
222
|
+
]),
|
|
223
|
+
).toBe('need_data')
|
|
224
|
+
expect(
|
|
225
|
+
classifyRows(
|
|
226
|
+
[metric({ fit: 'insufficient_history' }), metric({ resource: 'memory' })],
|
|
227
|
+
0,
|
|
228
|
+
true,
|
|
229
|
+
),
|
|
230
|
+
).toBe('review')
|
|
231
|
+
})
|
|
232
|
+
|
|
155
233
|
it('keeps workloads with no current replicas in review', () => {
|
|
156
234
|
const oversized = flattenScanResults(
|
|
157
235
|
response(
|
|
@@ -225,3 +303,43 @@ describe('rightsizing scan model', () => {
|
|
|
225
303
|
expect(scanClassCounts(rows).increase).toBe(1)
|
|
226
304
|
})
|
|
227
305
|
})
|
|
306
|
+
|
|
307
|
+
it('ranks known safety risks and increases above savings, while routine reviews stay lower', () => {
|
|
308
|
+
const make = (name: string, rows: RightsizingRow[], scaledToZero = false) => ({
|
|
309
|
+
...response(rows, scaledToZero ? 0 : 1, 'shop', scaledToZero).workloads[0],
|
|
310
|
+
name,
|
|
311
|
+
})
|
|
312
|
+
const cut = metric({
|
|
313
|
+
fit: 'oversized',
|
|
314
|
+
currentRequestValue: 10,
|
|
315
|
+
recommendedRequestValue: 1,
|
|
316
|
+
recommendedRequest: '1',
|
|
317
|
+
})
|
|
318
|
+
const grow = metric({
|
|
319
|
+
fit: 'under_requested',
|
|
320
|
+
currentRequestValue: 0.1,
|
|
321
|
+
recommendedRequestValue: 0.2,
|
|
322
|
+
recommendedRequest: '200m',
|
|
323
|
+
})
|
|
324
|
+
const risk = metric({ resource: 'memory', currentPodOOM: true })
|
|
325
|
+
const workloads = [
|
|
326
|
+
make('cut', [cut]),
|
|
327
|
+
make('grow', [grow]),
|
|
328
|
+
make('hpa', [metric({ hpaManaged: true })]),
|
|
329
|
+
make('oom', [risk]),
|
|
330
|
+
make('limit', [metric({ limitConflict: true })]),
|
|
331
|
+
make('bursty', [{ ...cut, bursty: true }]),
|
|
332
|
+
make('throttled', [{ ...cut, throttleRatio: 0.2 }]),
|
|
333
|
+
make('idle', [risk], true),
|
|
334
|
+
make('failed-risk', [metric({ queryError: 'failed', currentPodOOM: true })]),
|
|
335
|
+
make('partial-grow', [grow, metric({ resource: 'memory', queryError: 'failed' })]),
|
|
336
|
+
make('steady', [metric()]),
|
|
337
|
+
]
|
|
338
|
+
const rows = flattenScanResults({ ...response([]), workloads })
|
|
339
|
+
const names = rows.map((row) => row.name)
|
|
340
|
+
expect(new Set(names.slice(0, 3))).toEqual(new Set(['oom', 'limit', 'throttled']))
|
|
341
|
+
expect(names.slice(3, 5)).toEqual(['grow', 'partial-grow'])
|
|
342
|
+
expect(names[5]).toBe('cut')
|
|
343
|
+
expect(names.slice(6, 9)).toEqual(['bursty', 'hpa', 'idle'])
|
|
344
|
+
expect(names.slice(9)).toEqual(['failed-risk', 'steady'])
|
|
345
|
+
})
|
|
@@ -24,11 +24,11 @@ export interface RightsizingScanRow {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
const CLASS_RANK: Record<ScanClass, number> = {
|
|
27
|
-
reduction: 0,
|
|
28
27
|
increase: 1,
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
reduction: 2,
|
|
29
|
+
review: 3,
|
|
30
|
+
need_data: 4,
|
|
31
|
+
in_range: 5,
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
const MIN_CPU_REDUCTION = 0.05
|
|
@@ -39,32 +39,55 @@ export function classifyRows(
|
|
|
39
39
|
replicas = 1,
|
|
40
40
|
scaledToZero = false,
|
|
41
41
|
): ScanClass {
|
|
42
|
-
|
|
42
|
+
const evidenced = rows.filter((row) => !row.queryError && row.fit !== 'insufficient_history')
|
|
43
|
+
if (evidenced.length === 0) return 'need_data'
|
|
43
44
|
if (scaledToZero) return 'review'
|
|
44
45
|
if (
|
|
45
|
-
|
|
46
|
+
evidenced.some(
|
|
46
47
|
(row) =>
|
|
47
48
|
(row.fit === 'under_requested' || row.fit === 'missing_request') && row.recommendedRequest,
|
|
48
49
|
)
|
|
49
50
|
)
|
|
50
51
|
return 'increase'
|
|
51
|
-
if (
|
|
52
|
-
const impact = calculateImpact(
|
|
52
|
+
if (evidenced.some(needsManualReview)) return 'review'
|
|
53
|
+
const impact = calculateImpact(evidenced, replicas)
|
|
53
54
|
if (-impact.cpuChange >= MIN_CPU_REDUCTION || -impact.memoryChange >= MIN_MEMORY_REDUCTION)
|
|
54
55
|
return 'reduction'
|
|
55
|
-
return 'in_range'
|
|
56
|
+
return evidenced.length === rows.length ? 'in_range' : 'need_data'
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
function needsManualReview(row: RightsizingRow): boolean {
|
|
59
60
|
return (
|
|
60
61
|
row.hpaManaged ||
|
|
62
|
+
needsSafetyReview(row) ||
|
|
63
|
+
(isReduction(row) && row.bursty === true) ||
|
|
64
|
+
row.recommendationReason === 'hpa_evidence_unavailable' ||
|
|
65
|
+
row.recommendationReason === 'oom_evidence_unavailable'
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function needsSafetyReview(row: RightsizingRow): boolean {
|
|
70
|
+
return (
|
|
61
71
|
row.currentPodOOM === true ||
|
|
62
72
|
row.windowOomEvidence === true ||
|
|
63
73
|
row.limitConflict === true ||
|
|
64
|
-
row.
|
|
65
|
-
|
|
66
|
-
|
|
74
|
+
(isReduction(row) && (row.throttleRatio ?? 0) >= 0.1)
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function scanPriority(row: RightsizingScanRow): number {
|
|
79
|
+
if (
|
|
80
|
+
!row.scaledToZero &&
|
|
81
|
+
[row.cpu, row.memory].some(
|
|
82
|
+
(resource) =>
|
|
83
|
+
resource &&
|
|
84
|
+
!resource.queryError &&
|
|
85
|
+
resource.fit !== 'insufficient_history' &&
|
|
86
|
+
needsSafetyReview(resource),
|
|
87
|
+
)
|
|
67
88
|
)
|
|
89
|
+
return 0
|
|
90
|
+
return CLASS_RANK[row.classification]
|
|
68
91
|
}
|
|
69
92
|
|
|
70
93
|
function isReduction(row: RightsizingRow): boolean {
|
|
@@ -117,7 +140,7 @@ export function flattenScanResults(scan: RightsizingScanResponse): RightsizingSc
|
|
|
117
140
|
}
|
|
118
141
|
return result.sort(
|
|
119
142
|
(a, b) =>
|
|
120
|
-
|
|
143
|
+
scanPriority(a) - scanPriority(b) ||
|
|
121
144
|
impactScore(b) - impactScore(a) ||
|
|
122
145
|
a.namespace.localeCompare(b.namespace) ||
|
|
123
146
|
a.name.localeCompare(b.name) ||
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import type { RightsizingRow, RightsizingScanResponse } from '../../api/client'
|
|
4
|
+
import { FitWhy, ScanNotices, ScanSummary } from './RightsizingScanView'
|
|
5
|
+
|
|
6
|
+
const scan: RightsizingScanResponse = {
|
|
7
|
+
scanId: 'rs_test', scanStatus: 'finished', deadlineAt: '2026-09-19T10:03:00Z',
|
|
8
|
+
state: 'complete',
|
|
9
|
+
scannedAt: '2026-09-19T06:52:54Z',
|
|
10
|
+
window: '7d',
|
|
11
|
+
source: 'radar',
|
|
12
|
+
coverage: {
|
|
13
|
+
workloadsDiscovered: 120,
|
|
14
|
+
workloadsEvaluated: 120,
|
|
15
|
+
workloadsWithData: 47,
|
|
16
|
+
batches: 3,
|
|
17
|
+
attemptedBatches: 3, completedBatches: 3,
|
|
18
|
+
},
|
|
19
|
+
workloads: [],
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function render(result: RightsizingScanResponse) {
|
|
23
|
+
return renderToStaticMarkup(<ScanNotices result={result} />)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('rightsizing scan notices', () => {
|
|
27
|
+
it('keeps timeout coverage visible with query failures behind one collapsed disclosure', () => {
|
|
28
|
+
const html = render({
|
|
29
|
+
...scan,
|
|
30
|
+
state: 'partial',
|
|
31
|
+
coverage: {
|
|
32
|
+
...scan.coverage,
|
|
33
|
+
workloadsEvaluated: 100,
|
|
34
|
+
attemptedBatches: 1, completedBatches: 0,
|
|
35
|
+
daemonSetsWithoutNodes: 27,
|
|
36
|
+
},
|
|
37
|
+
warnings: ['cpu_query_failed', 'memory_query_failed', 'scan_deadline_exceeded'].map(
|
|
38
|
+
(code) => ({ code, message: 'backend-specific details' }),
|
|
39
|
+
),
|
|
40
|
+
})
|
|
41
|
+
expect(html.match(/<section/g)).toHaveLength(1)
|
|
42
|
+
expect(html.match(/<button/g)).toHaveLength(1)
|
|
43
|
+
expect(html).toContain('Partial results')
|
|
44
|
+
expect(html).not.toContain('workloads evaluated')
|
|
45
|
+
expect(html).toContain('select namespaces in the top bar')
|
|
46
|
+
expect(html).toContain('Available recommendations are shown')
|
|
47
|
+
expect(html).toContain('aria-expanded="false"')
|
|
48
|
+
expect(html).toContain('inert=""')
|
|
49
|
+
expect(html).toContain('Some CPU data could not be queried')
|
|
50
|
+
expect(html).toContain('Some memory data could not be queried')
|
|
51
|
+
expect(html).toContain('27 DaemonSets')
|
|
52
|
+
expect(html).not.toContain('completed batches')
|
|
53
|
+
expect(html).not.toContain('backend-specific details')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('does not label a complete scan with excluded DaemonSets as a query failure', () => {
|
|
57
|
+
const html = render({
|
|
58
|
+
...scan,
|
|
59
|
+
coverage: { ...scan.coverage, daemonSetsWithoutNodes: 1 },
|
|
60
|
+
})
|
|
61
|
+
expect(html).toContain('Scan notes')
|
|
62
|
+
expect(html).toContain('1 DaemonSet runs on no node right now and is not listed')
|
|
63
|
+
expect(html).not.toContain('<button')
|
|
64
|
+
expect(html).not.toContain('Partial results')
|
|
65
|
+
expect(html).not.toContain('time limit')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('preserves access and cache limitations without claiming a timeout', () => {
|
|
69
|
+
const html = render({
|
|
70
|
+
...scan,
|
|
71
|
+
state: 'partial',
|
|
72
|
+
coverage: {
|
|
73
|
+
...scan.coverage,
|
|
74
|
+
restrictedKinds: ['Deployment'],
|
|
75
|
+
partiallyCachedKinds: ['StatefulSet'],
|
|
76
|
+
unavailableKinds: ['DaemonSet'],
|
|
77
|
+
},
|
|
78
|
+
})
|
|
79
|
+
expect(html).toContain('Kubernetes access')
|
|
80
|
+
expect(html).toContain('available ownership data')
|
|
81
|
+
expect(html).toContain('caching only some namespaces')
|
|
82
|
+
expect(html).not.toContain('time limit')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('does not infer a timeout or short history from a query failure', () => {
|
|
86
|
+
const html = render({
|
|
87
|
+
...scan,
|
|
88
|
+
state: 'partial',
|
|
89
|
+
warnings: [{ code: 'cpu_query_failed', message: 'unknown error' }],
|
|
90
|
+
})
|
|
91
|
+
expect(html).toContain('Some CPU data could not be queried')
|
|
92
|
+
expect(html).not.toContain('time limit')
|
|
93
|
+
expect(html).not.toContain('not enough recent history')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('shows no notice for a clean scan and no empty Details button for deadline-only partial results', () => {
|
|
97
|
+
expect(render(scan)).toBe('')
|
|
98
|
+
const html = render({
|
|
99
|
+
...scan,
|
|
100
|
+
state: 'partial',
|
|
101
|
+
warnings: [{ code: 'scan_deadline_exceeded', message: 'deadline' }],
|
|
102
|
+
})
|
|
103
|
+
expect(html).toContain('time limit')
|
|
104
|
+
expect(html).not.toContain('<button')
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
describe('rightsizing evidence explanation', () => {
|
|
109
|
+
const row: RightsizingRow = {
|
|
110
|
+
container: 'app',
|
|
111
|
+
resource: 'cpu',
|
|
112
|
+
fit: 'insufficient_history',
|
|
113
|
+
confidence: 'low',
|
|
114
|
+
coverage: 0,
|
|
115
|
+
sampleCount: 0,
|
|
116
|
+
expectedSamples: 2017,
|
|
117
|
+
hpaManaged: false,
|
|
118
|
+
hpaEvidenceAvailable: true,
|
|
119
|
+
oomEvidenceAvailable: false,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
it('distinguishes failed queries from successfully measured short history', () => {
|
|
123
|
+
const failed = renderToStaticMarkup(
|
|
124
|
+
<FitWhy label="CPU" row={{ ...row, queryError: 'usage query failed' }} />,
|
|
125
|
+
)
|
|
126
|
+
expect(failed).toContain('Metrics could not be queried')
|
|
127
|
+
expect(failed).not.toContain('0.0 days')
|
|
128
|
+
expect(failed).not.toContain('Wait for more runtime history')
|
|
129
|
+
const short = renderToStaticMarkup(<FitWhy label="CPU" row={row} />)
|
|
130
|
+
expect(short).toContain('0.0 days')
|
|
131
|
+
expect(short).toContain('Wait for more runtime history')
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
describe('rightsizing scope and coverage summary', () => {
|
|
136
|
+
const counts = {
|
|
137
|
+
increase: 1,
|
|
138
|
+
reduction: 4,
|
|
139
|
+
review: 0,
|
|
140
|
+
in_range: 2,
|
|
141
|
+
need_data: 3,
|
|
142
|
+
}
|
|
143
|
+
it('separates attempted workloads from usage evidence and container counts', () => {
|
|
144
|
+
const html = renderToStaticMarkup(
|
|
145
|
+
<ScanSummary
|
|
146
|
+
namespaces={['dev', 'staging']}
|
|
147
|
+
result={{
|
|
148
|
+
...scan,
|
|
149
|
+
coverage: { ...scan.coverage, workloadsEvaluated: 100 },
|
|
150
|
+
}}
|
|
151
|
+
counts={counts}
|
|
152
|
+
selected="actions"
|
|
153
|
+
onSelect={() => {}}
|
|
154
|
+
/>,
|
|
155
|
+
)
|
|
156
|
+
expect(html).toContain('dev, staging')
|
|
157
|
+
expect(html).toContain('Change scan scope in the top bar')
|
|
158
|
+
expect(html).toContain('100 of 120 workloads attempted')
|
|
159
|
+
expect(html).toContain('47 with usage data')
|
|
160
|
+
expect(html).toContain('7d window')
|
|
161
|
+
expect(html).toContain('1 container<')
|
|
162
|
+
expect(html).not.toContain('workloads evaluated')
|
|
163
|
+
})
|
|
164
|
+
it('qualifies the all-namespace scope by visibility', () => {
|
|
165
|
+
const html = renderToStaticMarkup(
|
|
166
|
+
<ScanSummary
|
|
167
|
+
namespaces={[]}
|
|
168
|
+
result={scan}
|
|
169
|
+
counts={counts}
|
|
170
|
+
selected="actions"
|
|
171
|
+
onSelect={() => {}}
|
|
172
|
+
/>,
|
|
173
|
+
)
|
|
174
|
+
expect(html).toContain('All visible namespaces')
|
|
175
|
+
})
|
|
176
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Lock } from 'lucide-react'
|
|
2
|
+
|
|
3
|
+
export function OperatorManagedNotice({ helmValue }: { helmValue?: string }) {
|
|
4
|
+
return (
|
|
5
|
+
<div className="flex items-start gap-2 rounded-lg border border-theme-border bg-theme-elevated/50 p-3 text-sm">
|
|
6
|
+
<Lock className="mt-0.5 h-4 w-4 shrink-0 text-theme-text-tertiary" />
|
|
7
|
+
<div className="min-w-0">
|
|
8
|
+
<p className="font-medium text-theme-text-primary">Managed by your operator</p>
|
|
9
|
+
<p className="mt-1 text-xs text-theme-text-secondary">
|
|
10
|
+
To change these settings, ask your operator to update {helmValue ? <><code className="font-mono">{helmValue}</code> in </> : null}
|
|
11
|
+
Radar’s Helm values or startup configuration.
|
|
12
|
+
{' '}<a className="text-accent hover:underline" href="https://github.com/skyhook-io/radar/blob/main/docs/in-cluster.md#installation-settings" target="_blank" rel="noreferrer">Configuration guide</a>
|
|
13
|
+
</p>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
)
|
|
17
|
+
}
|