@skyhook-io/radar-app 0.2.2 → 0.3.1
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 +33 -25
- package/src/App.tsx +1449 -382
- package/src/RadarApp.tsx +132 -19
- package/src/api/apiResources.ts +1 -1
- 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.metrics.test.ts +106 -0
- package/src/api/client.rightsizing.test.ts +32 -0
- package/src/api/client.ts +2730 -271
- package/src/api/client.yaml.test.ts +45 -0
- package/src/api/diagnose.ts +289 -0
- package/src/api/quotas.ts +16 -0
- package/src/api/rbac.ts +57 -0
- package/src/api/timelineSource.test.ts +217 -0
- package/src/api/timelineSource.ts +582 -0
- package/src/components/ConnectionErrorView.tsx +186 -70
- package/src/components/ContextSwitcher.tsx +63 -18
- package/src/components/DebugOverlay.tsx +5 -3
- package/src/components/NamespaceSwitcher.tsx +41 -0
- package/src/components/UserMenu.tsx +69 -21
- package/src/components/applications/ApplicationsView.tsx +936 -0
- package/src/components/audit/AuditSettingsDialog.tsx +79 -17
- package/src/components/audit/AuditView.tsx +65 -62
- package/src/components/compare/CompareViewRoute.tsx +124 -0
- package/src/components/compare/useCompareCandidates.ts +27 -0
- package/src/components/compare/useCompareLauncher.tsx +79 -0
- package/src/components/cost/ApplicationCostTab.test.ts +204 -0
- package/src/components/cost/ApplicationCostTab.tsx +571 -0
- package/src/components/cost/CostTrendChart.tsx +106 -75
- package/src/components/cost/CostView.test.ts +12 -0
- package/src/components/cost/CostView.tsx +507 -223
- 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 +445 -0
- package/src/components/diagnose/AISettings.tsx +147 -0
- package/src/components/diagnose/DiagnoseContext.tsx +495 -0
- package/src/components/diagnose/DiagnoseSurface.tsx +394 -0
- package/src/components/diagnose/Home.tsx +163 -0
- package/src/components/diagnose/InvestigationView.tsx +622 -0
- package/src/components/diagnose/LocalDiagnoseAction.tsx +162 -0
- package/src/components/diagnose/launch.ts +65 -0
- package/src/components/diagnose/parts.tsx +1756 -0
- package/src/components/dock/BottomDock.tsx +2 -3
- package/src/components/dock/DockContext.tsx +1 -0
- package/src/components/dock/TerminalTab.tsx +1 -1
- package/src/components/dock/WorkloadLogsTab.tsx +21 -5
- package/src/components/dock/index.ts +1 -1
- 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 +1042 -0
- package/src/components/gitops/RevisionMetaChip.tsx +63 -0
- package/src/components/helm/ChartBrowser.tsx +87 -31
- package/src/components/helm/HelmCompareRoute.tsx +1341 -0
- package/src/components/helm/HelmReleaseDrawer.test.ts +17 -0
- package/src/components/helm/HelmReleaseDrawer.tsx +1073 -102
- package/src/components/helm/HelmView.tsx +237 -96
- package/src/components/helm/InstallWizard.tsx +94 -38
- package/src/components/helm/ManifestDiffViewer.tsx +8 -27
- package/src/components/helm/OwnedResources.tsx +34 -59
- package/src/components/helm/RevisionHistory.tsx +52 -3
- package/src/components/helm/RoleGatedPanel.tsx +3 -3
- package/src/components/helm/TrackChartSourceDialog.tsx +185 -0
- package/src/components/helm/ValuesDiffPreview.tsx +17 -7
- package/src/components/helm/ValuesViewer.tsx +49 -53
- package/src/components/helm/helm-utils.ts +4 -0
- package/src/components/home/ActivitySummary.tsx +4 -1
- package/src/components/home/ClusterHealthCard.tsx +56 -42
- package/src/components/home/CostCard.tsx +21 -36
- package/src/components/home/GitOpsControllersCard.tsx +110 -0
- package/src/components/home/HelmSummary.tsx +3 -1
- package/src/components/home/HomeView.tsx +339 -105
- package/src/components/home/MCPSetupDialog.tsx +29 -87
- package/src/components/home/TrafficSummary.tsx +2 -2
- package/src/components/home/mcpToolCatalog.ts +333 -0
- package/src/components/issues/IssuesPane.tsx +151 -0
- package/src/components/logs/LogsViewer.tsx +4 -1
- package/src/components/logs/ScheduledWorkloadLogsViewer.tsx +135 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +4 -1
- package/src/components/nav/PrimaryNavRail.tsx +285 -0
- package/src/components/portforward/PortForwardButton.tsx +118 -47
- package/src/components/portforward/PortForwardManager.tsx +253 -131
- package/src/components/resource/HPACharts.tsx +237 -0
- package/src/components/resource/PVCUsageBar.tsx +59 -0
- package/src/components/resource/PrometheusCharts.tsx +160 -584
- package/src/components/resource/PrometheusChartsGrid.tsx +270 -0
- package/src/components/resource/RestartChart.tsx +133 -0
- package/src/components/resource/RightsizingStrip.test.ts +109 -0
- package/src/components/resource/RightsizingStrip.tsx +363 -0
- package/src/components/resource-drawer/ResourceDrawer.tsx +3 -1
- package/src/components/resources/CompositeRenderer.tsx +101 -0
- package/src/components/resources/ImageFilesystemModal.tsx +19 -12
- package/src/components/resources/PodFilesystemModal.tsx +6 -5
- package/src/components/resources/ResourceDetailDrawer.tsx +13 -3
- package/src/components/resources/ResourcesView.tsx +194 -17
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +1 -0
- package/src/components/resources/renderers/HPARenderer.tsx +20 -1
- package/src/components/resources/renderers/NamespaceRenderer.tsx +31 -0
- package/src/components/resources/renderers/NodeRenderer.tsx +10 -4
- package/src/components/resources/renderers/PVCRenderer.tsx +19 -1
- package/src/components/resources/renderers/PodRenderer.tsx +30 -6
- package/src/components/resources/renderers/RoleBindingRenderer.tsx +45 -1
- package/src/components/resources/renderers/RoleRenderer.tsx +27 -1
- package/src/components/resources/renderers/ServiceAccountRenderer.tsx +28 -1
- package/src/components/resources/renderers/ServiceRenderer.tsx +81 -8
- package/src/components/resources/renderers/WorkloadRenderer.tsx +51 -4
- package/src/components/resources/renderers/index.ts +2 -0
- package/src/components/resources/resource-utils.ts +2 -1
- 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 +241 -0
- package/src/components/settings/SettingsDialog.tsx +1505 -165
- package/src/components/shared/CreateResourceDialog.tsx +9 -2
- package/src/components/shared/LargeClusterNamespacePicker.tsx +3 -3
- package/src/components/timeline/LocalTimelineScrubber.tsx +212 -0
- package/src/components/timeline/RetainedTimelineScrubber.tsx +311 -0
- package/src/components/timeline/TimelineList.tsx +86 -13
- package/src/components/timeline/TimelineSwimlanes.tsx +9 -1299
- package/src/components/timeline/TimelineView.tsx +873 -24
- package/src/components/timeline/TimelineView.urlparams.test.ts +335 -0
- package/src/components/traffic/TrafficFilterSidebar.tsx +10 -45
- package/src/components/traffic/TrafficFlowList.tsx +29 -15
- package/src/components/traffic/TrafficGraph.tsx +42 -24
- package/src/components/traffic/TrafficView.tsx +32 -19
- package/src/components/ui/CommandPalette.tsx +8 -215
- package/src/components/ui/DiagnosticsOverlay.tsx +219 -9
- package/src/components/ui/Markdown.tsx +3 -3
- package/src/components/ui/Omnibar.tsx +602 -0
- package/src/components/ui/RadarOmnibar.tsx +52 -0
- package/src/components/ui/SearchSyntaxHelp.tsx +89 -0
- package/src/components/ui/ShortcutHelpOverlay.tsx +3 -2
- package/src/components/ui/UpdateNotification.tsx +48 -36
- package/src/components/ui/command-items.ts +178 -0
- package/src/components/workload/WorkloadView.tsx +1342 -158
- package/src/context/ConnectionContext.tsx +146 -21
- package/src/context/DiagnoseCustomization.tsx +93 -0
- package/src/context/NavCustomization.tsx +75 -0
- package/src/context/TimelineSource.tsx +50 -0
- package/src/contexts/CapabilitiesContext.tsx +32 -8
- package/src/filter/FilterLocationBridge.tsx +30 -0
- package/src/hooks/useClusterLoadState.ts +73 -0
- package/src/hooks/useDocumentTitle.ts +25 -0
- package/src/hooks/useEventSource.ts +6 -0
- package/src/hooks/useKeyboardShortcuts.tsx +1 -0
- package/src/hooks/useMediaQuery.ts +21 -0
- package/src/hooks/useNavRailPinned.ts +46 -0
- package/src/hooks/useRecentResources.ts +49 -0
- package/src/index.css +162 -1
- package/src/index.ts +73 -1
- package/src/main.tsx +7 -5
- package/src/types/clusterLoadState.ts +33 -0
- package/src/types.ts +2 -0
- package/src/utils/auditBadges.ts +53 -0
- package/src/utils/navigation.ts +64 -1
- package/src/components/ui/NamespaceSelector.tsx +0 -436
|
@@ -0,0 +1,363 @@
|
|
|
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'
|
|
12
|
+
import { Tooltip } from '../ui/Tooltip'
|
|
13
|
+
|
|
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.'
|
|
20
|
+
|
|
21
|
+
interface RightsizingProps {
|
|
22
|
+
kind: string
|
|
23
|
+
namespace: string
|
|
24
|
+
name: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function useRightsizingEvidence({ kind, namespace, name }: RightsizingProps) {
|
|
28
|
+
useAutoPromConnect()
|
|
29
|
+
const { data: status, isLoading: statusLoading } = usePrometheusStatus()
|
|
30
|
+
const connected = status?.connected === true
|
|
31
|
+
const supported = RIGHTSIZING_KINDS.has(kind)
|
|
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 />
|
|
112
|
+
}
|
|
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.'} />
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const byContainer = new Map<string, RightsizingRow[]>()
|
|
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)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (compact) {
|
|
136
|
+
return (
|
|
137
|
+
<div className="space-y-2">
|
|
138
|
+
{Array.from(byContainer.entries()).map(([container, rows]) => (
|
|
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} />
|
|
144
|
+
))}
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
))}
|
|
148
|
+
</div>
|
|
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>
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
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
|
+
}
|
|
243
|
+
|
|
244
|
+
function CompactFitRow({ row }: { row: RightsizingRow }) {
|
|
245
|
+
return (
|
|
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">
|
|
248
|
+
{row.resource}
|
|
249
|
+
</span>
|
|
250
|
+
<span className="min-w-14 text-theme-text-secondary">{row.currentRequest ?? 'unset'}</span>
|
|
251
|
+
{row.recommendedRequest && (
|
|
252
|
+
<>
|
|
253
|
+
<ArrowRight className="h-3 w-3 text-theme-text-quaternary" />
|
|
254
|
+
<span className="min-w-14 text-theme-text-primary">{row.recommendedRequest}</span>
|
|
255
|
+
</>
|
|
256
|
+
)}
|
|
257
|
+
{row.observed && (
|
|
258
|
+
<span className="text-[10px] text-theme-text-quaternary">
|
|
259
|
+
({row.observed.name} {row.observed.formatted})
|
|
260
|
+
</span>
|
|
261
|
+
)}
|
|
262
|
+
<span className="ml-auto">
|
|
263
|
+
<FitBadge fit={row.fit} queryError={row.queryError} />
|
|
264
|
+
</span>
|
|
265
|
+
</div>
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
|
|
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
|
+
)
|
|
291
|
+
}
|
|
292
|
+
|
|
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',
|
|
308
|
+
}
|
|
309
|
+
return { label: labels[fit], severity: severities[fit] }
|
|
310
|
+
}
|
|
311
|
+
|
|
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}`
|
|
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
|
+
)
|
|
363
|
+
}
|
|
@@ -4,6 +4,7 @@ import { clsx } from 'clsx'
|
|
|
4
4
|
import { useState, useCallback } from 'react'
|
|
5
5
|
import type { TopologyNode, NodeKind, HealthStatus } from '../../types'
|
|
6
6
|
import { getKindBadgeBordered, healthToSeverity, SEVERITY_BADGE_BORDERED } from '../../utils/badge-colors'
|
|
7
|
+
import { Tooltip } from '../ui/Tooltip'
|
|
7
8
|
|
|
8
9
|
interface ResourceDrawerProps {
|
|
9
10
|
node: TopologyNode
|
|
@@ -146,10 +147,10 @@ export const ResourceDrawer = memo(function ResourceDrawer({
|
|
|
146
147
|
<h2 className="text-lg font-semibold text-theme-text-primary truncate">
|
|
147
148
|
{node.name}
|
|
148
149
|
</h2>
|
|
150
|
+
<Tooltip content="Copy name">
|
|
149
151
|
<button
|
|
150
152
|
onClick={copyName}
|
|
151
153
|
className="p-1 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded"
|
|
152
|
-
title="Copy name"
|
|
153
154
|
>
|
|
154
155
|
{copied ? (
|
|
155
156
|
<Check className="w-4 h-4 text-green-400" />
|
|
@@ -157,6 +158,7 @@ export const ResourceDrawer = memo(function ResourceDrawer({
|
|
|
157
158
|
<Copy className="w-4 h-4" />
|
|
158
159
|
)}
|
|
159
160
|
</button>
|
|
161
|
+
</Tooltip>
|
|
160
162
|
</div>
|
|
161
163
|
</div>
|
|
162
164
|
<button
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import { useQueries } from '@tanstack/react-query'
|
|
3
|
+
import {
|
|
4
|
+
CompositeRenderer as BaseCompositeRenderer,
|
|
5
|
+
type ComposedRefStatus,
|
|
6
|
+
} from '@skyhook-io/k8s-ui/components/resources/renderers/CompositeRenderer'
|
|
7
|
+
import {
|
|
8
|
+
getCrossplaneResourceRefs,
|
|
9
|
+
type CrossplaneResourceRef,
|
|
10
|
+
} from '@skyhook-io/k8s-ui/components/resources/resource-utils-crossplane'
|
|
11
|
+
import { getResourceStatus } from '@skyhook-io/k8s-ui'
|
|
12
|
+
import { kindToPlural } from '@skyhook-io/k8s-ui/utils/navigation'
|
|
13
|
+
import type { ResourceRef as NavRef } from '../../types'
|
|
14
|
+
import { fetchJSON, ApiError } from '../../api/client'
|
|
15
|
+
|
|
16
|
+
interface CompositeRendererProps {
|
|
17
|
+
data: any
|
|
18
|
+
onNavigate?: (ref: NavRef) => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function makeRefKey(ref: CrossplaneResourceRef): string {
|
|
22
|
+
return `${ref.apiVersion}/${ref.kind}/${ref.namespace ?? ''}/${ref.name}`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Crossplane refs carry full apiVersion (e.g. "s3.aws.upbound.io/v1beta1")
|
|
26
|
+
// because plural collisions across provider groups are real — two providers
|
|
27
|
+
// can ship CRDs with the same kind. Drop the version, keep the group.
|
|
28
|
+
function groupFromApiVersion(apiVersion: string | undefined): string {
|
|
29
|
+
if (!apiVersion) return ''
|
|
30
|
+
const slash = apiVersion.indexOf('/')
|
|
31
|
+
return slash < 0 ? '' : apiVersion.slice(0, slash)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Host wrapper for the package's CompositeRenderer: fans out a React Query
|
|
36
|
+
* lookup per composed resource ref so each row in the composed-resources
|
|
37
|
+
* list can show a live status badge.
|
|
38
|
+
*
|
|
39
|
+
* Errors are folded into the per-ref status entry rather than thrown — a
|
|
40
|
+
* missing composed resource (e.g. 404 because reconciliation hasn't created
|
|
41
|
+
* it yet) is a normal state for a freshly-applied Composite, not a failure.
|
|
42
|
+
*/
|
|
43
|
+
export function CompositeRenderer({ data, onNavigate }: CompositeRendererProps) {
|
|
44
|
+
const refs = useMemo<CrossplaneResourceRef[]>(() => getCrossplaneResourceRefs(data), [data])
|
|
45
|
+
|
|
46
|
+
const queries = useQueries({
|
|
47
|
+
queries: refs.map(ref => {
|
|
48
|
+
const group = groupFromApiVersion(ref.apiVersion)
|
|
49
|
+
return {
|
|
50
|
+
queryKey: ['composed-ref', group, ref.kind, ref.namespace ?? '', ref.name],
|
|
51
|
+
queryFn: async () => {
|
|
52
|
+
const ns = ref.namespace || '_'
|
|
53
|
+
const plural = kindToPlural(ref.kind)
|
|
54
|
+
const query = group ? `?group=${encodeURIComponent(group)}` : ''
|
|
55
|
+
return fetchJSON<{ resource: any }>(`/resources/${plural}/${ns}/${ref.name}${query}`)
|
|
56
|
+
},
|
|
57
|
+
staleTime: 30000,
|
|
58
|
+
retry: false,
|
|
59
|
+
enabled: Boolean(ref.kind && ref.name),
|
|
60
|
+
}
|
|
61
|
+
}),
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const composedRefStatuses = useMemo<Map<string, ComposedRefStatus>>(() => {
|
|
65
|
+
const map = new Map<string, ComposedRefStatus>()
|
|
66
|
+
refs.forEach((ref, i) => {
|
|
67
|
+
const q = queries[i]
|
|
68
|
+
const key = makeRefKey(ref)
|
|
69
|
+
if (q.isLoading) {
|
|
70
|
+
map.set(key, { ref, loading: true })
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
if (q.isError) {
|
|
74
|
+
// 404 = the composed resource genuinely doesn't exist yet (normal
|
|
75
|
+
// during reconciliation). Any other failure (401/403/500/network)
|
|
76
|
+
// means we can't tell the operator the resource's status — render
|
|
77
|
+
// it distinctly so they don't read "missing" and think the resource
|
|
78
|
+
// is absent when they've actually just lost permission to read it.
|
|
79
|
+
if (q.error instanceof ApiError && q.error.status === 404) {
|
|
80
|
+
map.set(key, { ref, missing: true })
|
|
81
|
+
} else {
|
|
82
|
+
const message = q.error instanceof Error ? q.error.message : 'Failed to fetch composed resource'
|
|
83
|
+
map.set(key, { ref, error: true, errorMessage: message })
|
|
84
|
+
}
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
if (!q.data) {
|
|
88
|
+
map.set(key, { ref, missing: true })
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
const status = getResourceStatus(kindToPlural(ref.kind), q.data.resource) ?? undefined
|
|
92
|
+
map.set(key, {
|
|
93
|
+
ref,
|
|
94
|
+
status: status ? { ...status, level: (status as any).level ?? 'unknown' } : undefined,
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
return map
|
|
98
|
+
}, [refs, queries])
|
|
99
|
+
|
|
100
|
+
return <BaseCompositeRenderer data={data} onNavigate={onNavigate} composedRefStatuses={composedRefStatuses} />
|
|
101
|
+
}
|
|
@@ -7,7 +7,9 @@ import { useImageMetadata, ApiError } from '../../api/client'
|
|
|
7
7
|
import type { FileNode, ImageFilesystem } from '../../types'
|
|
8
8
|
import { formatBytes } from '../../utils/format'
|
|
9
9
|
import { downloadBlob, filterTree } from './file-browser-utils'
|
|
10
|
+
import { Tooltip } from '../ui/Tooltip'
|
|
10
11
|
import { apiUrl, getAuthHeaders, getCredentialsMode } from '../../api/config'
|
|
12
|
+
import { Input } from '@skyhook-io/k8s-ui'
|
|
11
13
|
|
|
12
14
|
// Manual fetch function for filesystem (not a hook - gives us full control)
|
|
13
15
|
async function fetchImageFilesystem(
|
|
@@ -141,9 +143,11 @@ export function ImageFilesystemModal({
|
|
|
141
143
|
<div className="flex items-center justify-between p-4 border-b border-theme-border shrink-0">
|
|
142
144
|
<div className="flex-1 min-w-0">
|
|
143
145
|
<h3 className="text-lg font-semibold text-theme-text-primary">Image Filesystem</h3>
|
|
144
|
-
<
|
|
146
|
+
<Tooltip content={image} wrapperClassName="!block w-full">
|
|
147
|
+
<p className="text-sm text-theme-text-secondary truncate mt-0.5">
|
|
145
148
|
{image}
|
|
146
149
|
</p>
|
|
150
|
+
</Tooltip>
|
|
147
151
|
{(displayFilesystem?.platform || metadata?.platform) && (
|
|
148
152
|
<p className="text-xs text-theme-text-tertiary mt-1">
|
|
149
153
|
Platform: {displayFilesystem?.platform || metadata?.platform}
|
|
@@ -163,8 +167,7 @@ export function ImageFilesystemModal({
|
|
|
163
167
|
<div className="p-3 border-b border-theme-border shrink-0">
|
|
164
168
|
<div className="relative">
|
|
165
169
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-theme-text-tertiary" />
|
|
166
|
-
<
|
|
167
|
-
type="text"
|
|
170
|
+
<Input
|
|
168
171
|
placeholder="Search files..."
|
|
169
172
|
value={searchQuery}
|
|
170
173
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
@@ -245,9 +248,11 @@ export function ImageFilesystemModal({
|
|
|
245
248
|
<span>{formatBytes(displayFilesystem.totalSize)}</span>
|
|
246
249
|
{displayFilesystem.layers && <span>{displayFilesystem.layers.length} layers</span>}
|
|
247
250
|
{displayFilesystem.digest && (
|
|
248
|
-
<
|
|
251
|
+
<Tooltip content={displayFilesystem.digest} wrapperClassName="min-w-0">
|
|
252
|
+
<span className="truncate">
|
|
249
253
|
Digest: {displayFilesystem.digest.substring(0, 20)}...
|
|
250
254
|
</span>
|
|
255
|
+
</Tooltip>
|
|
251
256
|
)}
|
|
252
257
|
</>
|
|
253
258
|
)}
|
|
@@ -362,10 +367,10 @@ function DownloadConfirmation({ metadata, onConfirm, onCancel }: DownloadConfirm
|
|
|
362
367
|
<pre className="bg-theme-elevated rounded p-3 text-xs text-theme-text-primary overflow-x-auto font-mono">
|
|
363
368
|
{authCommand}
|
|
364
369
|
</pre>
|
|
370
|
+
<Tooltip content="Copy to clipboard" position="left" wrapperClassName="absolute top-2 right-2">
|
|
365
371
|
<button
|
|
366
372
|
onClick={handleCopy}
|
|
367
|
-
className="
|
|
368
|
-
title="Copy to clipboard"
|
|
373
|
+
className="p-1.5 text-theme-text-tertiary hover:text-theme-text-primary hover:bg-theme-base rounded transition-colors"
|
|
369
374
|
>
|
|
370
375
|
{copied ? (
|
|
371
376
|
<Check className="w-4 h-4 text-green-400" />
|
|
@@ -373,6 +378,7 @@ function DownloadConfirmation({ metadata, onConfirm, onCancel }: DownloadConfirm
|
|
|
373
378
|
<Copy className="w-4 h-4" />
|
|
374
379
|
)}
|
|
375
380
|
</button>
|
|
381
|
+
</Tooltip>
|
|
376
382
|
</div>
|
|
377
383
|
</div>
|
|
378
384
|
)}
|
|
@@ -444,7 +450,7 @@ function AuthenticationHelp({ image, registryType, onRetry }: AuthenticationHelp
|
|
|
444
450
|
</p>
|
|
445
451
|
|
|
446
452
|
<p className="text-xs text-theme-text-tertiary text-center max-w-md mb-6">
|
|
447
|
-
Registry: <span className="
|
|
453
|
+
Registry: <span className="inline-code">{registry}</span>
|
|
448
454
|
{registryType && registryType !== 'generic' && (
|
|
449
455
|
<> ({formatAuthMethod(registryType)})</>
|
|
450
456
|
)}
|
|
@@ -461,10 +467,10 @@ function AuthenticationHelp({ image, registryType, onRetry }: AuthenticationHelp
|
|
|
461
467
|
<pre className="bg-theme-elevated rounded p-3 text-xs text-theme-text-primary overflow-x-auto font-mono">
|
|
462
468
|
{authCommand}
|
|
463
469
|
</pre>
|
|
470
|
+
<Tooltip content="Copy to clipboard" position="left" wrapperClassName="absolute top-2 right-2">
|
|
464
471
|
<button
|
|
465
472
|
onClick={handleCopy}
|
|
466
|
-
className="
|
|
467
|
-
title="Copy to clipboard"
|
|
473
|
+
className="p-1.5 text-theme-text-tertiary hover:text-theme-text-primary hover:bg-theme-base rounded transition-colors"
|
|
468
474
|
>
|
|
469
475
|
{copied ? (
|
|
470
476
|
<Check className="w-4 h-4 text-green-400" />
|
|
@@ -472,6 +478,7 @@ function AuthenticationHelp({ image, registryType, onRetry }: AuthenticationHelp
|
|
|
472
478
|
<Copy className="w-4 h-4" />
|
|
473
479
|
)}
|
|
474
480
|
</button>
|
|
481
|
+
</Tooltip>
|
|
475
482
|
</div>
|
|
476
483
|
</div>
|
|
477
484
|
)}
|
|
@@ -709,11 +716,11 @@ function FileTreeNode({ node, depth, defaultExpanded = true, image, namespace, p
|
|
|
709
716
|
)}
|
|
710
717
|
|
|
711
718
|
{isFile && (
|
|
719
|
+
<Tooltip content="Download file" wrapperClassName="ml-1">
|
|
712
720
|
<button
|
|
713
721
|
onClick={handleDownload}
|
|
714
722
|
disabled={downloading}
|
|
715
|
-
className="p-1 text-theme-text-tertiary hover:text-blue-400 hover:bg-theme-elevated rounded
|
|
716
|
-
title="Download file"
|
|
723
|
+
className="p-1 text-theme-text-tertiary hover:text-blue-400 hover:bg-theme-elevated rounded disabled:opacity-50 disabled:pointer-events-none"
|
|
717
724
|
>
|
|
718
725
|
{downloading ? (
|
|
719
726
|
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
|
@@ -721,6 +728,7 @@ function FileTreeNode({ node, depth, defaultExpanded = true, image, namespace, p
|
|
|
721
728
|
<Download className="w-3.5 h-3.5" />
|
|
722
729
|
)}
|
|
723
730
|
</button>
|
|
731
|
+
</Tooltip>
|
|
724
732
|
)}
|
|
725
733
|
</div>
|
|
726
734
|
|
|
@@ -743,4 +751,3 @@ function FileTreeNode({ node, depth, defaultExpanded = true, image, namespace, p
|
|
|
743
751
|
</div>
|
|
744
752
|
)
|
|
745
753
|
}
|
|
746
|
-
|
|
@@ -1,12 +1,13 @@
|
|
|
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'
|
|
8
8
|
import { downloadBlob, filterTree } from './file-browser-utils'
|
|
9
9
|
import { apiUrl, getAuthHeaders, getCredentialsMode } from '../../api/config'
|
|
10
|
+
import { Tooltip } from '../ui/Tooltip'
|
|
10
11
|
|
|
11
12
|
interface PodFilesystem {
|
|
12
13
|
root: FileNode
|
|
@@ -191,8 +192,7 @@ export function PodFilesystemModal({
|
|
|
191
192
|
{showFilesystem && (
|
|
192
193
|
<div className="relative flex-1 min-w-[200px]">
|
|
193
194
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-theme-text-tertiary" />
|
|
194
|
-
<
|
|
195
|
-
type="text"
|
|
195
|
+
<Input
|
|
196
196
|
placeholder="Filter files..."
|
|
197
197
|
value={searchQuery}
|
|
198
198
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
@@ -384,11 +384,11 @@ function PodFileTreeNode({ node, namespace, podName, container, onNavigate }: Po
|
|
|
384
384
|
)}
|
|
385
385
|
|
|
386
386
|
{isDownloadable && (
|
|
387
|
+
<Tooltip content="Download file" wrapperClassName="ml-1">
|
|
387
388
|
<button
|
|
388
389
|
onClick={handleDownload}
|
|
389
390
|
disabled={downloading}
|
|
390
|
-
className="p-1 text-theme-text-tertiary hover:text-blue-400 hover:bg-theme-elevated rounded
|
|
391
|
-
title="Download file"
|
|
391
|
+
className="p-1 text-theme-text-tertiary hover:text-blue-400 hover:bg-theme-elevated rounded disabled:opacity-50 disabled:pointer-events-none"
|
|
392
392
|
>
|
|
393
393
|
{downloading ? (
|
|
394
394
|
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
|
@@ -396,6 +396,7 @@ function PodFileTreeNode({ node, namespace, podName, container, onNavigate }: Po
|
|
|
396
396
|
<Download className="w-3.5 h-3.5" />
|
|
397
397
|
)}
|
|
398
398
|
</button>
|
|
399
|
+
</Tooltip>
|
|
399
400
|
)}
|
|
400
401
|
</div>
|
|
401
402
|
)
|