@skyhook-io/radar-app 1.8.6 → 1.8.8
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 +7 -1
- package/package.json +12 -10
- package/src/App.tsx +278 -230
- package/src/RadarApp.tsx +91 -20
- package/src/api/client.argoResourceSync.test.ts +69 -0
- package/src/api/client.delta.test.ts +89 -0
- package/src/api/client.deltaSync.test.ts +216 -0
- package/src/api/client.rightsizing.test.ts +32 -0
- package/src/api/client.ts +1462 -246
- package/src/api/diagnose.ts +288 -0
- package/src/api/timelineSource.test.ts +217 -0
- package/src/api/timelineSource.ts +582 -0
- package/src/components/ConnectionErrorView.tsx +174 -70
- package/src/components/ContextSwitcher.tsx +13 -5
- package/src/components/applications/ApplicationsView.tsx +775 -80
- package/src/components/audit/AuditSettingsDialog.tsx +2 -2
- 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/curl/ServiceCurlButton.tsx +2 -2
- package/src/components/diagnose/AISettings.tsx +116 -0
- package/src/components/diagnose/DiagnoseContext.tsx +491 -0
- package/src/components/diagnose/DiagnoseSurface.tsx +385 -0
- package/src/components/diagnose/Home.tsx +163 -0
- package/src/components/diagnose/InvestigationView.tsx +604 -0
- package/src/components/diagnose/LocalDiagnoseAction.tsx +150 -0
- package/src/components/diagnose/launch.ts +65 -0
- package/src/components/diagnose/parts.tsx +1689 -0
- package/src/components/dock/BottomDock.tsx +2 -3
- package/src/components/dock/WorkloadLogsTab.tsx +21 -5
- package/src/components/execution/BatchExecutionView.test.ts +170 -0
- package/src/components/execution/BatchExecutionView.tsx +1329 -0
- package/src/components/execution/batch-run-actions.test.ts +48 -0
- package/src/components/execution/batch-run-actions.ts +24 -0
- package/src/components/execution/batch-timeline.test.ts +57 -0
- package/src/components/execution/batch-timeline.ts +46 -0
- package/src/components/execution/execution-definition.test.ts +208 -0
- package/src/components/execution/execution-definition.ts +245 -0
- 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/ChartBrowser.tsx +2 -3
- package/src/components/helm/HelmCompareRoute.tsx +1 -2
- package/src/components/helm/HelmReleaseDrawer.test.ts +17 -0
- package/src/components/helm/HelmReleaseDrawer.tsx +376 -43
- package/src/components/helm/HelmView.tsx +2 -3
- package/src/components/helm/InstallWizard.tsx +3 -5
- package/src/components/helm/ManifestDiffViewer.tsx +1 -31
- package/src/components/helm/TrackChartSourceDialog.tsx +48 -4
- package/src/components/helm/ValuesDiffPreview.tsx +17 -7
- package/src/components/home/CostCard.tsx +21 -36
- package/src/components/home/HomeView.tsx +17 -15
- package/src/components/home/MCPSetupDialog.tsx +1 -1
- package/src/components/home/mcpToolCatalog.ts +2 -2
- package/src/components/issues/IssuesPane.tsx +9 -1
- package/src/components/logs/ScheduledWorkloadLogsViewer.tsx +135 -0
- package/src/components/portforward/PortForwardButton.tsx +2 -2
- package/src/components/portforward/PortForwardManager.tsx +19 -14
- package/src/components/resource/PrometheusChartsGrid.tsx +6 -80
- package/src/components/resource/RightsizingStrip.test.ts +109 -0
- package/src/components/resource/RightsizingStrip.tsx +320 -127
- package/src/components/resources/ImageFilesystemModal.tsx +2 -2
- package/src/components/resources/PodFilesystemModal.tsx +2 -3
- package/src/components/resources/ResourceDetailDrawer.tsx +2 -0
- package/src/components/resources/ResourcesView.tsx +13 -4
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +1 -0
- package/src/components/resources/renderers/index.ts +1 -0
- 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 +1290 -283
- package/src/components/shared/LargeClusterNamespacePicker.tsx +2 -2
- package/src/components/timeline/LocalTimelineScrubber.tsx +212 -0
- package/src/components/timeline/RetainedTimelineScrubber.tsx +311 -0
- package/src/components/timeline/TimelineList.tsx +81 -13
- package/src/components/timeline/TimelineView.tsx +842 -26
- package/src/components/timeline/TimelineView.urlparams.test.ts +335 -0
- package/src/components/traffic/TrafficFilterSidebar.tsx +2 -2
- package/src/components/traffic/TrafficView.tsx +8 -4
- package/src/components/ui/CommandPalette.tsx +2 -2
- package/src/components/workload/WorkloadView.tsx +912 -245
- package/src/context/ConnectionContext.tsx +109 -11
- package/src/context/DiagnoseCustomization.tsx +42 -0
- package/src/context/TimelineSource.tsx +50 -0
- package/src/hooks/useClusterLoadState.ts +73 -0
- package/src/hooks/useEventSource.ts +6 -0
- package/src/index.css +162 -1
- package/src/index.ts +12 -0
- package/src/main.tsx +1 -1
- package/src/types/clusterLoadState.ts +33 -0
- package/src/utils/navigation.ts +10 -0
|
@@ -1,170 +1,363 @@
|
|
|
1
|
-
import { ArrowRight,
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { ArrowRight, ExternalLink, Gauge, HelpCircle, Loader2 } from 'lucide-react'
|
|
2
|
+
import { Link, useLocation } from 'react-router-dom'
|
|
3
|
+
import { Badge } from '@skyhook-io/k8s-ui/components/ui/Badge'
|
|
4
|
+
import {
|
|
5
|
+
usePrometheusRightsizing,
|
|
6
|
+
usePrometheusStatus,
|
|
7
|
+
useAutoPromConnect,
|
|
8
|
+
type PrometheusRightsizing,
|
|
9
|
+
type RightsizingFit,
|
|
10
|
+
type RightsizingRow,
|
|
11
|
+
} from '../../api/client'
|
|
4
12
|
import { Tooltip } from '../ui/Tooltip'
|
|
5
13
|
|
|
6
14
|
const RIGHTSIZING_KINDS = new Set(['Deployment', 'StatefulSet', 'DaemonSet'])
|
|
15
|
+
export const RIGHTSIZING_DOCS_URL = 'https://radarhq.io/docs/features/rightsizing'
|
|
16
|
+
export const RIGHTSIZING_SUMMARY =
|
|
17
|
+
'Evidence-based guidance for this workload, not a savings estimate or automatic change.'
|
|
18
|
+
export const RIGHTSIZING_METHODOLOGY =
|
|
19
|
+
'Uses seven days of 5-minute samples: CPU P95 and memory maximum, plus 15% headroom. Reductions are staged and rounded to practical values; memory reductions also require verifiable restart history. Radar does not change requests.'
|
|
7
20
|
|
|
8
|
-
|
|
9
|
-
* RightsizingStrip — compact "current → recommended" table per container.
|
|
10
|
-
*
|
|
11
|
-
* Tone policy is deliberately mild:
|
|
12
|
-
* - "Well-sized" or "Nx headroom" → neutral, no badge
|
|
13
|
-
* - 5×+ over-provisioning → info ("could reduce"), not a problem
|
|
14
|
-
* - P95 exceeds request but within limit → warning
|
|
15
|
-
* - P95 exceeds CPU limit → alert (throttling)
|
|
16
|
-
* - Memory P95 near limit → critical (active OOM risk)
|
|
17
|
-
*
|
|
18
|
-
* Anything below severe over-provisioning displays without flagging it as
|
|
19
|
-
* an issue. 2-3× headroom is the common, sensible default and should not nag.
|
|
20
|
-
*/
|
|
21
|
-
export function RightsizingStrip({ kind, namespace, name }: {
|
|
21
|
+
interface RightsizingProps {
|
|
22
22
|
kind: string
|
|
23
23
|
namespace: string
|
|
24
24
|
name: string
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function useRightsizingEvidence({ kind, namespace, name }: RightsizingProps) {
|
|
28
|
+
useAutoPromConnect()
|
|
29
|
+
const { data: status, isLoading: statusLoading } = usePrometheusStatus()
|
|
30
|
+
const connected = status?.connected === true
|
|
28
31
|
const supported = RIGHTSIZING_KINDS.has(kind)
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
32
|
+
const query = usePrometheusRightsizing(kind, namespace, name, connected && supported)
|
|
33
|
+
return { connected, supported, statusLoading, ...query }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function RightsizingPanel(props: RightsizingProps) {
|
|
37
|
+
const state = useRightsizingEvidence(props)
|
|
38
|
+
const { search } = useLocation()
|
|
39
|
+
if (!state.supported) return null
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<section className="mx-auto w-full max-w-[1600px] rounded-lg border border-theme-border bg-theme-surface/50">
|
|
43
|
+
<header className="flex flex-wrap items-start justify-between gap-3 border-b border-theme-border px-4 py-3">
|
|
44
|
+
<div className="flex items-start gap-2">
|
|
45
|
+
<Gauge className="mt-0.5 h-4 w-4 text-theme-text-tertiary" />
|
|
46
|
+
<div>
|
|
47
|
+
<div className="flex items-center gap-1.5">
|
|
48
|
+
<h3 className="text-sm font-semibold text-theme-text-primary">Rightsizing</h3>
|
|
49
|
+
<Tooltip content={RIGHTSIZING_METHODOLOGY}>
|
|
50
|
+
<HelpCircle className="h-3.5 w-3.5 text-theme-text-tertiary" />
|
|
51
|
+
</Tooltip>
|
|
52
|
+
</div>
|
|
53
|
+
<p className="text-xs text-theme-text-tertiary">
|
|
54
|
+
{RIGHTSIZING_SUMMARY}{' '}
|
|
55
|
+
<a
|
|
56
|
+
href={RIGHTSIZING_DOCS_URL}
|
|
57
|
+
target="_blank"
|
|
58
|
+
rel="noreferrer"
|
|
59
|
+
className="inline-flex items-center gap-1 text-accent-text hover:underline"
|
|
60
|
+
>
|
|
61
|
+
How it works
|
|
62
|
+
<ExternalLink className="h-3 w-3" />
|
|
63
|
+
</a>
|
|
64
|
+
</p>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
<div className="flex items-center gap-3">
|
|
68
|
+
{state.data && <RightsizingContext data={state.data} />}
|
|
69
|
+
<Link
|
|
70
|
+
to={{ pathname: '/cost/rightsizing', search }}
|
|
71
|
+
className="text-xs font-medium text-accent-text hover:underline"
|
|
72
|
+
>
|
|
73
|
+
Scan visible workloads
|
|
74
|
+
</Link>
|
|
75
|
+
</div>
|
|
76
|
+
</header>
|
|
77
|
+
<div className="p-4">
|
|
78
|
+
<RightsizingBody state={state} compact={false} />
|
|
79
|
+
</div>
|
|
80
|
+
</section>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function RightsizingStrip(props: RightsizingProps) {
|
|
85
|
+
const state = useRightsizingEvidence(props)
|
|
86
|
+
if (!state.supported || !state.connected || (state.isLoading && !state.data)) return null
|
|
87
|
+
return (
|
|
88
|
+
<section className="mb-3 rounded-lg border border-theme-border bg-theme-surface/40 p-3">
|
|
89
|
+
<header className="mb-2 flex items-center justify-between gap-3">
|
|
90
|
+
<div className="flex items-center gap-1.5">
|
|
91
|
+
<h3 className="text-sm font-medium text-theme-text-primary">Rightsizing</h3>
|
|
92
|
+
<Tooltip content={RIGHTSIZING_METHODOLOGY}>
|
|
93
|
+
<HelpCircle className="h-3.5 w-3.5 text-theme-text-tertiary" />
|
|
94
|
+
</Tooltip>
|
|
95
|
+
</div>
|
|
96
|
+
{state.data && <RightsizingContext data={state.data} compact />}
|
|
97
|
+
</header>
|
|
98
|
+
<RightsizingBody state={state} compact />
|
|
99
|
+
</section>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function RightsizingBody({
|
|
104
|
+
state,
|
|
105
|
+
compact,
|
|
106
|
+
}: {
|
|
107
|
+
state: ReturnType<typeof useRightsizingEvidence>
|
|
108
|
+
compact: boolean
|
|
109
|
+
}) {
|
|
110
|
+
if (state.statusLoading) {
|
|
111
|
+
return <EmptyState text="Checking Prometheus connection…" loading />
|
|
47
112
|
}
|
|
48
|
-
if (!
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
)
|
|
113
|
+
if (!state.connected) {
|
|
114
|
+
return <EmptyState text="Connect Prometheus to compare requests with observed demand." />
|
|
115
|
+
}
|
|
116
|
+
if (state.isLoading && !state.data) {
|
|
117
|
+
return <EmptyState text="Analyzing seven days of workload demand…" loading />
|
|
118
|
+
}
|
|
119
|
+
if (state.error && !state.data) {
|
|
120
|
+
const message = state.error instanceof Error ? state.error.message : String(state.error)
|
|
121
|
+
return <EmptyState text={`Rightsizing unavailable — ${message}`} />
|
|
122
|
+
}
|
|
123
|
+
if (!state.data) return null
|
|
124
|
+
if (!state.data.sampleAvailable || state.data.rows.length === 0) {
|
|
125
|
+
return <EmptyState text={state.data.reason ?? 'No rightsizing evidence is available.'} />
|
|
62
126
|
}
|
|
63
127
|
|
|
64
|
-
// Group rows by container so each container is a compact two-row block (cpu+mem).
|
|
65
128
|
const byContainer = new Map<string, RightsizingRow[]>()
|
|
66
|
-
for (const row of data.rows) {
|
|
67
|
-
|
|
68
|
-
|
|
129
|
+
for (const row of state.data.rows) {
|
|
130
|
+
const rows = byContainer.get(row.container) ?? []
|
|
131
|
+
rows.push(row)
|
|
132
|
+
byContainer.set(row.container, rows)
|
|
69
133
|
}
|
|
70
134
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
<
|
|
74
|
-
<h3 className="text-sm font-medium text-theme-text-primary">Right-sizing</h3>
|
|
75
|
-
<span className="text-[11px] text-theme-text-tertiary">
|
|
76
|
-
based on last {data.window} · P95
|
|
77
|
-
</span>
|
|
78
|
-
</header>
|
|
79
|
-
<div className="space-y-1.5">
|
|
135
|
+
if (compact) {
|
|
136
|
+
return (
|
|
137
|
+
<div className="space-y-2">
|
|
80
138
|
{Array.from(byContainer.entries()).map(([container, rows]) => (
|
|
81
|
-
<div key={container}
|
|
82
|
-
<div className="
|
|
83
|
-
<div className="space-y-
|
|
84
|
-
{rows.map(row => (
|
|
85
|
-
<
|
|
139
|
+
<div key={container}>
|
|
140
|
+
<div className="mb-1 text-xs font-medium text-theme-text-secondary">{container}</div>
|
|
141
|
+
<div className="space-y-1 pl-2">
|
|
142
|
+
{rows.map((row) => (
|
|
143
|
+
<CompactFitRow key={row.resource} row={row} />
|
|
86
144
|
))}
|
|
87
145
|
</div>
|
|
88
146
|
</div>
|
|
89
147
|
))}
|
|
90
148
|
</div>
|
|
91
|
-
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<div className="space-y-4">
|
|
154
|
+
{(state.data.scaledToZero || state.data.ownerCoverage === 'current_pods') && (
|
|
155
|
+
<div className="flex flex-wrap gap-2">
|
|
156
|
+
{state.data.scaledToZero && (
|
|
157
|
+
<Badge severity="neutral" size="sm">
|
|
158
|
+
No current replicas · showing retained evidence
|
|
159
|
+
</Badge>
|
|
160
|
+
)}
|
|
161
|
+
{state.data.ownerCoverage === 'current_pods' && (
|
|
162
|
+
<Badge severity="info" size="sm">
|
|
163
|
+
Current pods only · confidence capped
|
|
164
|
+
</Badge>
|
|
165
|
+
)}
|
|
166
|
+
</div>
|
|
167
|
+
)}
|
|
168
|
+
{Array.from(byContainer.entries()).map(([container, rows]) => (
|
|
169
|
+
<div key={container}>
|
|
170
|
+
<h4 className="mb-2 text-xs font-semibold text-theme-text-secondary">{container}</h4>
|
|
171
|
+
<div className="grid gap-3 md:grid-cols-2">
|
|
172
|
+
{rows.map((row) => (
|
|
173
|
+
<FitCard key={row.resource} row={row} window={state.data!.window} />
|
|
174
|
+
))}
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
))}
|
|
178
|
+
</div>
|
|
92
179
|
)
|
|
93
180
|
}
|
|
94
181
|
|
|
95
|
-
function
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
182
|
+
function FitCard({ row, window }: { row: RightsizingRow; window: string }) {
|
|
183
|
+
return (
|
|
184
|
+
<div className="rounded-md border border-theme-border bg-theme-base/60 p-3">
|
|
185
|
+
<div className="mb-3 flex flex-wrap items-center justify-between gap-2">
|
|
186
|
+
<span className="text-xs font-semibold uppercase tracking-wide text-theme-text-tertiary">
|
|
187
|
+
{row.resource}
|
|
188
|
+
</span>
|
|
189
|
+
<div className="flex flex-wrap items-center justify-end gap-1.5">
|
|
190
|
+
<FitBadge fit={row.fit} queryError={row.queryError} />
|
|
191
|
+
{row.hpaManaged && (
|
|
192
|
+
<Badge severity="info" size="sm">
|
|
193
|
+
HPA-managed
|
|
194
|
+
</Badge>
|
|
195
|
+
)}
|
|
196
|
+
{row.bursty && (
|
|
197
|
+
<Badge severity="warning" size="sm">
|
|
198
|
+
Bursty CPU
|
|
199
|
+
</Badge>
|
|
200
|
+
)}
|
|
201
|
+
{row.throttleRatio != null && row.throttleRatio >= 0.1 && (
|
|
202
|
+
<Badge severity="alert" size="sm">
|
|
203
|
+
Throttling observed
|
|
204
|
+
</Badge>
|
|
205
|
+
)}
|
|
206
|
+
{(row.currentPodOOM || row.windowOomEvidence) && (
|
|
207
|
+
<Badge severity="error" size="sm">
|
|
208
|
+
OOM evidence
|
|
209
|
+
</Badge>
|
|
210
|
+
)}
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
<div className="flex items-baseline gap-2 tabular-nums">
|
|
214
|
+
<div>
|
|
215
|
+
<div className="text-[10px] uppercase tracking-wide text-theme-text-quaternary">
|
|
216
|
+
Current request
|
|
217
|
+
</div>
|
|
218
|
+
<div className="text-lg font-semibold text-theme-text-primary">
|
|
219
|
+
{row.currentRequest ?? 'Unset'}
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
<ArrowRight className="h-4 w-4 text-theme-text-quaternary" />
|
|
223
|
+
<div>
|
|
224
|
+
<div className="text-[10px] uppercase tracking-wide text-theme-text-quaternary">
|
|
225
|
+
Suggested next step
|
|
226
|
+
</div>
|
|
227
|
+
<div className="text-lg font-semibold text-theme-text-primary">
|
|
228
|
+
{row.recommendedRequest ?? '—'}
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
<div className="mt-3 grid grid-cols-2 gap-3 border-t border-theme-border pt-3 text-xs">
|
|
233
|
+
<Metric
|
|
234
|
+
label={`${row.observed?.name ?? (row.resource === 'cpu' ? 'P95' : 'Max')} observed`}
|
|
235
|
+
value={row.observed?.formatted ?? 'No samples'}
|
|
236
|
+
/>
|
|
237
|
+
<Metric label={`Evidence · ${window}`} value={`${confidenceLabel(row)} · 5m samples`} />
|
|
238
|
+
</div>
|
|
239
|
+
<FitExplanation row={row} />
|
|
240
|
+
</div>
|
|
241
|
+
)
|
|
242
|
+
}
|
|
99
243
|
|
|
244
|
+
function CompactFitRow({ row }: { row: RightsizingRow }) {
|
|
100
245
|
return (
|
|
101
|
-
<div className="flex items-center gap-2 text-theme-text-tertiary
|
|
102
|
-
<span className="w-12 text-
|
|
246
|
+
<div className="flex flex-wrap items-center gap-2 text-xs tabular-nums text-theme-text-tertiary">
|
|
247
|
+
<span className="w-12 text-[10px] uppercase tracking-wide text-theme-text-quaternary">
|
|
103
248
|
{row.resource}
|
|
104
249
|
</span>
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
{row.currentRequest ?? <span className="text-theme-text-quaternary italic">unset</span>}
|
|
108
|
-
</span>
|
|
109
|
-
|
|
110
|
-
{showRec && (
|
|
250
|
+
<span className="min-w-14 text-theme-text-secondary">{row.currentRequest ?? 'unset'}</span>
|
|
251
|
+
{row.recommendedRequest && (
|
|
111
252
|
<>
|
|
112
|
-
<ArrowRight className="
|
|
113
|
-
<span className=
|
|
114
|
-
{row.recommendedRequest}
|
|
115
|
-
</span>
|
|
253
|
+
<ArrowRight className="h-3 w-3 text-theme-text-quaternary" />
|
|
254
|
+
<span className="min-w-14 text-theme-text-primary">{row.recommendedRequest}</span>
|
|
116
255
|
</>
|
|
117
256
|
)}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
(P95 {row.p95})
|
|
122
|
-
</span>
|
|
123
|
-
)}
|
|
124
|
-
|
|
125
|
-
{row.tone !== 'ok' && Icon && (
|
|
126
|
-
<span className={`ml-auto inline-flex items-center gap-1 ${toneClass.badge} px-1.5 py-0.5 rounded text-[10px]`}>
|
|
127
|
-
<Icon className="w-3 h-3" />
|
|
128
|
-
<span>{row.message}</span>
|
|
257
|
+
{row.observed && (
|
|
258
|
+
<span className="text-[10px] text-theme-text-quaternary">
|
|
259
|
+
({row.observed.name} {row.observed.formatted})
|
|
129
260
|
</span>
|
|
130
261
|
)}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
)}
|
|
262
|
+
<span className="ml-auto">
|
|
263
|
+
<FitBadge fit={row.fit} queryError={row.queryError} />
|
|
264
|
+
</span>
|
|
135
265
|
</div>
|
|
136
266
|
)
|
|
137
267
|
}
|
|
138
268
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
269
|
+
function RightsizingContext({
|
|
270
|
+
data,
|
|
271
|
+
compact = false,
|
|
272
|
+
}: {
|
|
273
|
+
data: PrometheusRightsizing
|
|
274
|
+
compact?: boolean
|
|
275
|
+
}) {
|
|
276
|
+
const evidence = data.ownerCoverage === 'ksm_history' ? 'retained ownership' : 'current pods only'
|
|
277
|
+
return (
|
|
278
|
+
<span className="text-[11px] text-theme-text-tertiary">
|
|
279
|
+
{compact ? data.window : `Last ${data.window} · ${evidence}`}
|
|
280
|
+
</span>
|
|
281
|
+
)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function FitBadge({ fit, queryError }: { fit: RightsizingFit; queryError?: string }) {
|
|
285
|
+
const presentation = getRightsizingPresentation(fit, queryError)
|
|
286
|
+
return (
|
|
287
|
+
<Badge severity={presentation.severity} size="sm">
|
|
288
|
+
{presentation.label}
|
|
289
|
+
</Badge>
|
|
290
|
+
)
|
|
145
291
|
}
|
|
146
292
|
|
|
147
|
-
function
|
|
148
|
-
if (
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
293
|
+
export function getRightsizingPresentation(fit: RightsizingFit, queryError?: string) {
|
|
294
|
+
if (queryError) return { label: 'Query failed', severity: 'error' as const }
|
|
295
|
+
const labels: Record<RightsizingFit, string> = {
|
|
296
|
+
balanced: 'Balanced',
|
|
297
|
+
oversized: 'Oversized',
|
|
298
|
+
under_requested: 'Under-requested',
|
|
299
|
+
missing_request: 'Missing request',
|
|
300
|
+
insufficient_history: 'Insufficient history',
|
|
301
|
+
}
|
|
302
|
+
const severities: Record<RightsizingFit, 'success' | 'info' | 'warning' | 'neutral'> = {
|
|
303
|
+
balanced: 'success',
|
|
304
|
+
oversized: 'info',
|
|
305
|
+
under_requested: 'warning',
|
|
306
|
+
missing_request: 'warning',
|
|
307
|
+
insufficient_history: 'neutral',
|
|
152
308
|
}
|
|
153
|
-
|
|
154
|
-
return { value: SEVERITY_TEXT[sev], badge: SEVERITY_BADGE[sev] }
|
|
309
|
+
return { label: labels[fit], severity: severities[fit] }
|
|
155
310
|
}
|
|
156
311
|
|
|
157
|
-
function
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
312
|
+
function FitExplanation({ row }: { row: RightsizingRow }) {
|
|
313
|
+
const message = getRightsizingExplanation(row)
|
|
314
|
+
if (!message) return null
|
|
315
|
+
return <p className="mt-3 text-xs text-theme-text-tertiary">{message}</p>
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function getRightsizingExplanation(row: RightsizingRow): string | undefined {
|
|
319
|
+
const messages: Record<string, string> = {
|
|
320
|
+
request_within_fit_range: 'The configured request is within 30% of the evidence-based target.',
|
|
321
|
+
insufficient_history: 'At least six hours of samples are required before suggesting a request.',
|
|
322
|
+
hpa_managed: `The HPA manages ${row.resource}; review its target before changing this request.`,
|
|
323
|
+
hpa_evidence_unavailable:
|
|
324
|
+
'Radar could not verify HPA ownership, so it withheld a suggested request.',
|
|
325
|
+
oom_evidence: 'A lower memory request is withheld because OOM evidence exists.',
|
|
326
|
+
oom_evidence_unavailable:
|
|
327
|
+
'Radar could not verify recent OOM history, so it withheld a lower memory request.',
|
|
328
|
+
recommended_request_exceeds_limit:
|
|
329
|
+
'The evidence-based request would exceed the current limit; review the limit first.',
|
|
330
|
+
}
|
|
331
|
+
if (row.reductionLimited && row.calculatedRequest && row.recommendedRequest) {
|
|
332
|
+
const burstNote = row.bursty && row.peak ? ` CPU P99 reached ${row.peak.formatted}.` : ''
|
|
333
|
+
return `Demand-based target: ${row.calculatedRequest}. Radar suggests ${row.recommendedRequest} as a conservative next step; observe another full window before reducing further.${burstNote}`
|
|
169
334
|
}
|
|
335
|
+
const message = row.queryError
|
|
336
|
+
? `Partial result: ${row.queryError}.`
|
|
337
|
+
: messages[row.recommendationReason ?? '']
|
|
338
|
+
const throttleUnavailable = row.resource === 'cpu' && !row.throttleAvailable
|
|
339
|
+
if (!message && !throttleUnavailable) return undefined
|
|
340
|
+
return message ?? 'CPU throttling metrics are unavailable; no throttling conclusion was drawn.'
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function confidenceLabel(row: RightsizingRow) {
|
|
344
|
+
return `${row.confidence[0].toUpperCase()}${row.confidence.slice(1)} confidence`
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function Metric({ label, value }: { label: string; value: string }) {
|
|
348
|
+
return (
|
|
349
|
+
<div>
|
|
350
|
+
<div className="text-theme-text-quaternary">{label}</div>
|
|
351
|
+
<div className="mt-0.5 text-theme-text-secondary">{value}</div>
|
|
352
|
+
</div>
|
|
353
|
+
)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function EmptyState({ text, loading = false }: { text: string; loading?: boolean }) {
|
|
357
|
+
return (
|
|
358
|
+
<div className="flex min-h-16 items-center justify-center text-sm text-theme-text-tertiary">
|
|
359
|
+
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
360
|
+
{text}
|
|
361
|
+
</div>
|
|
362
|
+
)
|
|
170
363
|
}
|
|
@@ -9,6 +9,7 @@ import { formatBytes } from '../../utils/format'
|
|
|
9
9
|
import { downloadBlob, filterTree } from './file-browser-utils'
|
|
10
10
|
import { Tooltip } from '../ui/Tooltip'
|
|
11
11
|
import { apiUrl, getAuthHeaders, getCredentialsMode } from '../../api/config'
|
|
12
|
+
import { Input } from '@skyhook-io/k8s-ui'
|
|
12
13
|
|
|
13
14
|
// Manual fetch function for filesystem (not a hook - gives us full control)
|
|
14
15
|
async function fetchImageFilesystem(
|
|
@@ -166,8 +167,7 @@ export function ImageFilesystemModal({
|
|
|
166
167
|
<div className="p-3 border-b border-theme-border shrink-0">
|
|
167
168
|
<div className="relative">
|
|
168
169
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-theme-text-tertiary" />
|
|
169
|
-
<
|
|
170
|
-
type="text"
|
|
170
|
+
<Input
|
|
171
171
|
placeholder="Search files..."
|
|
172
172
|
value={searchQuery}
|
|
173
173
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useState, useRef, useEffect, useMemo, useCallback } from 'react'
|
|
2
2
|
import { createPortal } from 'react-dom'
|
|
3
3
|
import { X, File, Link2, ChevronRight, AlertTriangle, Loader2, Search, Download, FolderOpen } from 'lucide-react'
|
|
4
|
-
import { PaneLoader } from '@skyhook-io/k8s-ui'
|
|
4
|
+
import { PaneLoader, Input } from '@skyhook-io/k8s-ui'
|
|
5
5
|
import { clsx } from 'clsx'
|
|
6
6
|
import type { FileNode } from '../../types'
|
|
7
7
|
import { formatBytes } from '../../utils/format'
|
|
@@ -192,8 +192,7 @@ export function PodFilesystemModal({
|
|
|
192
192
|
{showFilesystem && (
|
|
193
193
|
<div className="relative flex-1 min-w-[200px]">
|
|
194
194
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-theme-text-tertiary" />
|
|
195
|
-
<
|
|
196
|
-
type="text"
|
|
195
|
+
<Input
|
|
197
196
|
placeholder="Filter files..."
|
|
198
197
|
value={searchQuery}
|
|
199
198
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
@@ -23,6 +23,8 @@ interface ResourceDetailDrawerProps {
|
|
|
23
23
|
/** Top offset for the drawer (px). Defaults to Radar's 49px header height;
|
|
24
24
|
* pass 0 in chromeless embeds where there's no Radar header above it. */
|
|
25
25
|
headerHeight?: number
|
|
26
|
+
/** Right inset in px so the drawer sits beside a docked side panel (AI), not under it. */
|
|
27
|
+
rightInset?: number
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
export function ResourceDetailDrawer(props: ResourceDetailDrawerProps) {
|
|
@@ -41,6 +41,7 @@ interface ResourcesViewProps {
|
|
|
41
41
|
|
|
42
42
|
type SelectedKindInfo = { name: string; kind: string; group: string } | null
|
|
43
43
|
|
|
44
|
+
const EMPTY_RESOURCE_COUNTS: Record<string, number> = {}
|
|
44
45
|
const LARGE_RESOURCE_LIST_LIMIT = 25000
|
|
45
46
|
const LARGE_RESOURCE_LIST_GUARD_KEYS = new Set([
|
|
46
47
|
'Pod',
|
|
@@ -60,6 +61,10 @@ function resourceCountKey(kind: NonNullable<SelectedKindInfo>): string {
|
|
|
60
61
|
return kind.group ? `${kind.group}/${kind.kind}` : kind.kind
|
|
61
62
|
}
|
|
62
63
|
|
|
64
|
+
function hasResourceCount(counts: Record<string, number> | undefined, key: string): boolean {
|
|
65
|
+
return Object.prototype.hasOwnProperty.call(counts ?? {}, key)
|
|
66
|
+
}
|
|
67
|
+
|
|
63
68
|
export function ResourcesView({ namespaces, selectedResource, onResourceClick, onResourceClickYaml, onKindChange, onClearNamespaces }: ResourcesViewProps) {
|
|
64
69
|
const location = useLocation()
|
|
65
70
|
const navigate = useNavigate()
|
|
@@ -181,14 +186,16 @@ export function ResourcesView({ namespaces, selectedResource, onResourceClick, o
|
|
|
181
186
|
|
|
182
187
|
const selectedCountKey = selectedKind ? resourceCountKey(selectedKind) : ''
|
|
183
188
|
const selectedCount = selectedCountKey ? countsData?.counts[selectedCountKey] : undefined
|
|
189
|
+
const selectedCountKnown = selectedCountKey ? hasResourceCount(countsData?.counts, selectedCountKey) : false
|
|
184
190
|
const selectedCountUnavailable = selectedCountKey ? countsData?.unavailable?.includes(selectedCountKey) ?? false : false
|
|
185
191
|
const isSelectedKindGuarded = selectedCountKey !== '' && LARGE_RESOURCE_LIST_GUARD_KEYS.has(selectedCountKey)
|
|
186
192
|
const waitingForGuardCount = isSelectedKindGuarded && !countsData && !countsIsError
|
|
187
|
-
const largeListBlocked = isSelectedKindGuarded && countsData != null && (selectedCountUnavailable || (selectedCount ?? 0) > LARGE_RESOURCE_LIST_LIMIT)
|
|
193
|
+
const largeListBlocked = isSelectedKindGuarded && countsData != null && (selectedCountUnavailable || (selectedCountKnown && (selectedCount ?? 0) > LARGE_RESOURCE_LIST_LIMIT))
|
|
188
194
|
const selectedKindQueryBlocked = waitingForGuardCount || largeListBlocked
|
|
189
195
|
const podCount = countsData?.counts.Pod
|
|
196
|
+
const podCountKnown = hasResourceCount(countsData?.counts, 'Pod')
|
|
190
197
|
const podCountUnavailable = countsData?.unavailable?.includes('Pod') ?? false
|
|
191
|
-
const podCountAllowsBulkMetrics = countsData != null && !podCountUnavailable && (podCount ?? 0) <= LARGE_RESOURCE_LIST_LIMIT
|
|
198
|
+
const podCountAllowsBulkMetrics = countsData != null && podCountKnown && !podCountUnavailable && (podCount ?? 0) <= LARGE_RESOURCE_LIST_LIMIT
|
|
192
199
|
const selectedKindName = selectedKind?.name.toLowerCase() ?? ''
|
|
193
200
|
const topPodMetricsEnabled = selectedKindName === 'pods' && podCountAllowsBulkMetrics
|
|
194
201
|
const topNodeMetricsEnabled = selectedKindName === 'nodes' && namespaces.length === 0 && podCountAllowsBulkMetrics
|
|
@@ -248,6 +255,8 @@ export function ResourcesView({ namespaces, selectedResource, onResourceClick, o
|
|
|
248
255
|
const selectedKindQueryResult: ResourceQueryResult | undefined = useMemo(() => {
|
|
249
256
|
if (!selectedKind) return undefined
|
|
250
257
|
return {
|
|
258
|
+
resourceName: selectedKind.name,
|
|
259
|
+
group: selectedKind.group,
|
|
251
260
|
data: selectedKindQueryBlocked ? [] : selectedKindQuery.data as any[] | undefined,
|
|
252
261
|
isLoading: waitingForGuardCount || selectedKindQuery.isLoading,
|
|
253
262
|
error: selectedKindQueryBlocked ? undefined : selectedKindQuery.error,
|
|
@@ -315,7 +324,7 @@ export function ResourcesView({ namespaces, selectedResource, onResourceClick, o
|
|
|
315
324
|
return (
|
|
316
325
|
<>
|
|
317
326
|
<BaseResourcesView
|
|
318
|
-
key={
|
|
327
|
+
key={connection.context || 'default'}
|
|
319
328
|
namespaces={namespaces}
|
|
320
329
|
selectedResource={selectedResource}
|
|
321
330
|
onResourceClick={onResourceClick}
|
|
@@ -325,7 +334,7 @@ export function ResourcesView({ namespaces, selectedResource, onResourceClick, o
|
|
|
325
334
|
// Injected data
|
|
326
335
|
apiResources={apiResources}
|
|
327
336
|
// Lightweight counts for sidebar (replaces 233 parallel queries)
|
|
328
|
-
resourceCounts={countsData?.counts}
|
|
337
|
+
resourceCounts={countsData?.counts ?? EMPTY_RESOURCE_COUNTS}
|
|
329
338
|
resourceForbidden={countsData?.forbidden}
|
|
330
339
|
resourceReasons={countsData?.reasons}
|
|
331
340
|
resourceUnavailable={countsData?.unavailable}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '@skyhook-io/k8s-ui/components/resources/renderers/CronWorkflowRenderer'
|
|
@@ -7,6 +7,7 @@ export { ConfigMapRenderer } from './ConfigMapRenderer'
|
|
|
7
7
|
export { SecretRenderer } from './SecretRenderer'
|
|
8
8
|
export { JobRenderer } from './JobRenderer'
|
|
9
9
|
export { CronJobRenderer } from './CronJobRenderer'
|
|
10
|
+
export { CronWorkflowRenderer } from './CronWorkflowRenderer'
|
|
10
11
|
export { HPARenderer } from './HPARenderer'
|
|
11
12
|
export { NodeRenderer } from './NodeRenderer'
|
|
12
13
|
export { PVCRenderer } from './PVCRenderer'
|