@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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useState, useCallback, useEffect, useMemo, useRef } from 'react'
|
|
2
2
|
import { X, Package, ChevronRight, ChevronLeft, Play, Loader2, AlertTriangle, CheckCircle, User, BookOpen, Link as LinkIcon, Star, BadgeCheck, Shield, Globe, Building2, Plus, Minus, Terminal } from 'lucide-react'
|
|
3
|
-
import { PaneLoader } from '@skyhook-io/k8s-ui'
|
|
3
|
+
import { PaneLoader, Input } from '@skyhook-io/k8s-ui'
|
|
4
4
|
import { clsx } from 'clsx'
|
|
5
5
|
import yaml from 'yaml'
|
|
6
6
|
import { createPatch } from 'diff'
|
|
@@ -12,6 +12,7 @@ import { YamlEditor } from '../ui/YamlEditor'
|
|
|
12
12
|
import { Tooltip } from '../ui/Tooltip'
|
|
13
13
|
import { Markdown } from '../ui/Markdown'
|
|
14
14
|
import { SEVERITY_BADGE, SEVERITY_TEXT } from '../../utils/badge-colors'
|
|
15
|
+
import { validateHelmReleaseName, validateRFC1123Label } from '@skyhook-io/k8s-ui/utils/validators'
|
|
15
16
|
|
|
16
17
|
// Deep merge two objects — values from `overrides` take priority
|
|
17
18
|
function deepMerge(base: Record<string, unknown>, overrides: Record<string, unknown>): Record<string, unknown> {
|
|
@@ -27,7 +28,6 @@ function deepMerge(base: Record<string, unknown>, overrides: Record<string, unkn
|
|
|
27
28
|
}
|
|
28
29
|
return result
|
|
29
30
|
}
|
|
30
|
-
|
|
31
31
|
interface InstallWizardProps {
|
|
32
32
|
repo: string
|
|
33
33
|
chartName: string
|
|
@@ -105,11 +105,11 @@ export function InstallWizard({ repo, chartName, version, source, repoUrl, defau
|
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
if (baseValues && defaultValues) {
|
|
108
|
-
setValuesYaml(yaml.stringify(deepMerge(baseValues, defaultValues)))
|
|
108
|
+
setValuesYaml(yaml.stringify(deepMerge(baseValues, defaultValues), { lineWidth: 0 }))
|
|
109
109
|
} else if (defaultValues && !baseValues) {
|
|
110
|
-
setValuesYaml(yaml.stringify(defaultValues))
|
|
110
|
+
setValuesYaml(yaml.stringify(defaultValues, { lineWidth: 0 }))
|
|
111
111
|
} else if (baseValues) {
|
|
112
|
-
setValuesYaml(yaml.stringify(baseValues))
|
|
112
|
+
setValuesYaml(yaml.stringify(baseValues, { lineWidth: 0 }))
|
|
113
113
|
}
|
|
114
114
|
}, [localChartDetail?.values, artifactHubDetail?.values, isLocal, defaultValues])
|
|
115
115
|
|
|
@@ -134,11 +134,20 @@ export function InstallWizard({ repo, chartName, version, source, repoUrl, defau
|
|
|
134
134
|
setInstallError(null)
|
|
135
135
|
setProgressLogs([])
|
|
136
136
|
|
|
137
|
+
// Send the same trimmed values the validators ran on, not the
|
|
138
|
+
// raw input. Without this, a release name with trailing
|
|
139
|
+
// whitespace passes the client-side validator (which calls
|
|
140
|
+
// `.trim()`) but the server receives the untrimmed string and
|
|
141
|
+
// rejects it — exactly the surprise this validation layer
|
|
142
|
+
// exists to prevent.
|
|
143
|
+
const trimmedReleaseName = releaseName.trim()
|
|
144
|
+
const trimmedNamespace = namespace.trim()
|
|
145
|
+
|
|
137
146
|
try {
|
|
138
147
|
const release = await installChartWithProgress(
|
|
139
148
|
{
|
|
140
|
-
releaseName,
|
|
141
|
-
namespace,
|
|
149
|
+
releaseName: trimmedReleaseName,
|
|
150
|
+
namespace: trimmedNamespace,
|
|
142
151
|
chartName,
|
|
143
152
|
version,
|
|
144
153
|
repository,
|
|
@@ -169,7 +178,7 @@ export function InstallWizard({ repo, chartName, version, source, repoUrl, defau
|
|
|
169
178
|
|
|
170
179
|
// Wait a moment to show success, then close
|
|
171
180
|
setTimeout(() => {
|
|
172
|
-
onSuccess(
|
|
181
|
+
onSuccess(trimmedNamespace, trimmedReleaseName)
|
|
173
182
|
}, 1500)
|
|
174
183
|
} catch (err) {
|
|
175
184
|
setInstallError(err instanceof Error ? err.message : 'Install failed')
|
|
@@ -181,9 +190,25 @@ export function InstallWizard({ repo, chartName, version, source, repoUrl, defau
|
|
|
181
190
|
} finally {
|
|
182
191
|
setIsInstalling(false)
|
|
183
192
|
}
|
|
184
|
-
}, [releaseName, namespace, chartName, version, repo, valuesYaml, createNamespace, onSuccess, isLocal, artifactHubDetail, queryClient])
|
|
185
|
-
|
|
186
|
-
|
|
193
|
+
}, [releaseName, namespace, chartName, version, repo, repoUrl, valuesYaml, createNamespace, onSuccess, isLocal, artifactHubDetail, queryClient])
|
|
194
|
+
|
|
195
|
+
// Validate release name + namespace before letting the user
|
|
196
|
+
// advance. Without this, a name like "Invalid Name With Spaces!"
|
|
197
|
+
// was accepted through to step 2 and only failed server-side at
|
|
198
|
+
// install time — the server returned a 422 the user couldn't
|
|
199
|
+
// connect back to anything they typed. K8s / Helm rules are
|
|
200
|
+
// pinned in packages/k8s-ui/src/utils/validators.ts.
|
|
201
|
+
const releaseNameValidation = useMemo(
|
|
202
|
+
() => validateHelmReleaseName(releaseName.trim()),
|
|
203
|
+
[releaseName],
|
|
204
|
+
)
|
|
205
|
+
const namespaceValidation = useMemo(
|
|
206
|
+
() => validateRFC1123Label(namespace.trim()),
|
|
207
|
+
[namespace],
|
|
208
|
+
)
|
|
209
|
+
const releaseNameError = releaseNameValidation.valid ? null : releaseNameValidation.error
|
|
210
|
+
const namespaceError = namespaceValidation.valid ? null : namespaceValidation.error
|
|
211
|
+
const canProceedFromInfo = releaseNameValidation.valid && namespaceValidation.valid
|
|
187
212
|
const canInstall = canProceedFromInfo && !yamlError
|
|
188
213
|
|
|
189
214
|
const steps: { id: WizardStep; label: string }[] = [
|
|
@@ -206,7 +231,7 @@ export function InstallWizard({ repo, chartName, version, source, repoUrl, defau
|
|
|
206
231
|
<img
|
|
207
232
|
src={isLocal ? (chartDetail as ChartDetail)?.icon : (chartDetail as ArtifactHubChartDetail)?.logoUrl}
|
|
208
233
|
alt=""
|
|
209
|
-
className="w-8 h-8 rounded object-contain bg-
|
|
234
|
+
className="w-8 h-8 rounded object-contain bg-theme-elevated p-1"
|
|
210
235
|
/>
|
|
211
236
|
) : (
|
|
212
237
|
<Package className="w-8 h-8 text-purple-400" />
|
|
@@ -216,7 +241,7 @@ export function InstallWizard({ repo, chartName, version, source, repoUrl, defau
|
|
|
216
241
|
<h2 className="text-lg font-semibold text-theme-text-primary">Install {chartName}</h2>
|
|
217
242
|
{!isLocal && (
|
|
218
243
|
<Tooltip content="From ArtifactHub">
|
|
219
|
-
<Globe className="w-4 h-4 text-
|
|
244
|
+
<Globe className="w-4 h-4 text-accent" />
|
|
220
245
|
</Tooltip>
|
|
221
246
|
)}
|
|
222
247
|
</div>
|
|
@@ -224,7 +249,7 @@ export function InstallWizard({ repo, chartName, version, source, repoUrl, defau
|
|
|
224
249
|
<span>{repo} / {version}</span>
|
|
225
250
|
{!isLocal && (chartDetail as ArtifactHubChartDetail)?.repository?.official && (
|
|
226
251
|
<Tooltip content="Official">
|
|
227
|
-
<BadgeCheck className="w-3.5 h-3.5 text-
|
|
252
|
+
<BadgeCheck className="w-3.5 h-3.5 text-accent" />
|
|
228
253
|
</Tooltip>
|
|
229
254
|
)}
|
|
230
255
|
{!isLocal && (chartDetail as ArtifactHubChartDetail)?.repository?.verifiedPublisher && (
|
|
@@ -264,7 +289,7 @@ export function InstallWizard({ repo, chartName, version, source, repoUrl, defau
|
|
|
264
289
|
>
|
|
265
290
|
<span className={clsx(
|
|
266
291
|
'w-5 h-5 rounded-full flex items-center justify-center text-xs font-medium',
|
|
267
|
-
step === s.id ? 'bg-
|
|
292
|
+
step === s.id ? 'bg-accent text-white' : 'bg-theme-elevated text-theme-text-secondary'
|
|
268
293
|
)}>
|
|
269
294
|
{i + 1}
|
|
270
295
|
</span>
|
|
@@ -287,8 +312,10 @@ export function InstallWizard({ repo, chartName, version, source, repoUrl, defau
|
|
|
287
312
|
source={source}
|
|
288
313
|
releaseName={releaseName}
|
|
289
314
|
setReleaseName={setReleaseName}
|
|
315
|
+
releaseNameError={releaseNameError}
|
|
290
316
|
namespace={namespace}
|
|
291
317
|
setNamespace={setNamespace}
|
|
318
|
+
namespaceError={namespaceError}
|
|
292
319
|
namespaces={namespaces || []}
|
|
293
320
|
createNamespace={createNamespace}
|
|
294
321
|
setCreateNamespace={setCreateNamespace}
|
|
@@ -321,7 +348,7 @@ export function InstallWizard({ repo, chartName, version, source, repoUrl, defau
|
|
|
321
348
|
valuesYaml={valuesYaml}
|
|
322
349
|
defaultValuesYaml={
|
|
323
350
|
isLocal
|
|
324
|
-
? (localChartDetail?.values ? yaml.stringify(localChartDetail.values) : '')
|
|
351
|
+
? (localChartDetail?.values ? yaml.stringify(localChartDetail.values, { lineWidth: 0 }) : '')
|
|
325
352
|
: (artifactHubDetail?.values || '')
|
|
326
353
|
}
|
|
327
354
|
/>
|
|
@@ -383,11 +410,11 @@ export function InstallWizard({ repo, chartName, version, source, repoUrl, defau
|
|
|
383
410
|
Cancel
|
|
384
411
|
</button>
|
|
385
412
|
{step === 'review' ? (
|
|
413
|
+
<Tooltip content={!canHelmWrite ? helmActReason : ''}>
|
|
386
414
|
<button
|
|
387
415
|
onClick={handleInstall}
|
|
388
416
|
disabled={!canInstall || isInstalling || !canHelmWrite}
|
|
389
|
-
className="flex items-center gap-2 px-4 py-2 text-sm font-medium btn-brand rounded-lg disabled:cursor-not-allowed"
|
|
390
|
-
title={!canHelmWrite ? helmActReason : undefined}
|
|
417
|
+
className="flex items-center gap-2 px-4 py-2 text-sm font-medium btn-brand rounded-lg disabled:cursor-not-allowed disabled:pointer-events-none"
|
|
391
418
|
>
|
|
392
419
|
{isInstalling ? (
|
|
393
420
|
<Loader2 className="w-4 h-4 animate-spin" />
|
|
@@ -396,6 +423,7 @@ export function InstallWizard({ repo, chartName, version, source, repoUrl, defau
|
|
|
396
423
|
)}
|
|
397
424
|
Install
|
|
398
425
|
</button>
|
|
426
|
+
</Tooltip>
|
|
399
427
|
) : (
|
|
400
428
|
<button
|
|
401
429
|
onClick={() => setStep(step === 'info' ? 'values' : 'review')}
|
|
@@ -420,8 +448,10 @@ interface InfoStepProps {
|
|
|
420
448
|
source: ChartSource
|
|
421
449
|
releaseName: string
|
|
422
450
|
setReleaseName: (name: string) => void
|
|
451
|
+
releaseNameError: string | null
|
|
423
452
|
namespace: string
|
|
424
453
|
setNamespace: (ns: string) => void
|
|
454
|
+
namespaceError: string | null
|
|
425
455
|
namespaces: { name: string }[]
|
|
426
456
|
createNamespace: boolean
|
|
427
457
|
setCreateNamespace: (create: boolean) => void
|
|
@@ -434,8 +464,10 @@ function InfoStep({
|
|
|
434
464
|
source,
|
|
435
465
|
releaseName,
|
|
436
466
|
setReleaseName,
|
|
467
|
+
releaseNameError,
|
|
437
468
|
namespace,
|
|
438
469
|
setNamespace,
|
|
470
|
+
namespaceError,
|
|
439
471
|
namespaces,
|
|
440
472
|
createNamespace,
|
|
441
473
|
setCreateNamespace,
|
|
@@ -506,7 +538,7 @@ function InfoStep({
|
|
|
506
538
|
href={home}
|
|
507
539
|
target="_blank"
|
|
508
540
|
rel="noopener noreferrer"
|
|
509
|
-
className="flex items-center gap-1 text-xs text-
|
|
541
|
+
className="flex items-center gap-1 text-xs text-accent-text hover:underline"
|
|
510
542
|
>
|
|
511
543
|
<LinkIcon className="w-3.5 h-3.5" />
|
|
512
544
|
Homepage
|
|
@@ -528,16 +560,28 @@ function InfoStep({
|
|
|
528
560
|
<label className="block text-sm font-medium text-theme-text-secondary mb-2">
|
|
529
561
|
Release Name
|
|
530
562
|
</label>
|
|
531
|
-
<
|
|
532
|
-
type="text"
|
|
563
|
+
<Input
|
|
533
564
|
value={releaseName}
|
|
534
565
|
onChange={(e) => setReleaseName(e.target.value)}
|
|
535
566
|
placeholder="my-release"
|
|
536
|
-
|
|
567
|
+
aria-invalid={releaseNameError ? true : undefined}
|
|
568
|
+
aria-describedby="release-name-help"
|
|
569
|
+
className={clsx(
|
|
570
|
+
'w-full px-3 py-2 bg-theme-elevated border rounded-lg text-sm text-theme-text-primary placeholder-theme-text-disabled focus:outline-none focus:ring-2',
|
|
571
|
+
releaseNameError
|
|
572
|
+
? 'border-red-500/60 focus:ring-red-500'
|
|
573
|
+
: 'border-theme-border-light focus:ring-accent',
|
|
574
|
+
)}
|
|
537
575
|
/>
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
576
|
+
{releaseNameError ? (
|
|
577
|
+
<p id="release-name-help" className="mt-1 text-xs text-red-400">
|
|
578
|
+
Release name {releaseNameError}.
|
|
579
|
+
</p>
|
|
580
|
+
) : (
|
|
581
|
+
<p id="release-name-help" className="mt-1 text-xs text-theme-text-tertiary">
|
|
582
|
+
A unique name for this release in the namespace
|
|
583
|
+
</p>
|
|
584
|
+
)}
|
|
541
585
|
</div>
|
|
542
586
|
|
|
543
587
|
{/* Namespace selection */}
|
|
@@ -545,14 +589,25 @@ function InfoStep({
|
|
|
545
589
|
<label className="block text-sm font-medium text-theme-text-secondary mb-2">
|
|
546
590
|
Namespace
|
|
547
591
|
</label>
|
|
548
|
-
<
|
|
549
|
-
type="text"
|
|
592
|
+
<Input
|
|
550
593
|
list="namespace-suggestions"
|
|
551
594
|
value={namespace}
|
|
552
595
|
onChange={(e) => setNamespace(e.target.value)}
|
|
553
596
|
placeholder="Enter namespace name"
|
|
554
|
-
|
|
597
|
+
aria-invalid={namespaceError ? true : undefined}
|
|
598
|
+
aria-describedby="namespace-help"
|
|
599
|
+
className={clsx(
|
|
600
|
+
'w-full px-3 py-2 bg-theme-elevated border rounded-lg text-sm text-theme-text-primary placeholder-theme-text-disabled focus:outline-none focus:ring-2',
|
|
601
|
+
namespaceError
|
|
602
|
+
? 'border-red-500/60 focus:ring-red-500'
|
|
603
|
+
: 'border-theme-border-light focus:ring-accent',
|
|
604
|
+
)}
|
|
555
605
|
/>
|
|
606
|
+
{namespaceError && (
|
|
607
|
+
<p id="namespace-help" className="mt-1 text-xs text-red-400">
|
|
608
|
+
Namespace {namespaceError}.
|
|
609
|
+
</p>
|
|
610
|
+
)}
|
|
556
611
|
<datalist id="namespace-suggestions">
|
|
557
612
|
{namespaces.map(ns => (
|
|
558
613
|
<option key={ns.name} value={ns.name} />
|
|
@@ -563,7 +618,7 @@ function InfoStep({
|
|
|
563
618
|
type="checkbox"
|
|
564
619
|
checked={createNamespace}
|
|
565
620
|
onChange={(e) => setCreateNamespace(e.target.checked)}
|
|
566
|
-
className="rounded border-theme-border text-
|
|
621
|
+
className="rounded border-theme-border text-accent focus:ring-accent"
|
|
567
622
|
/>
|
|
568
623
|
Create namespace if it doesn't exist
|
|
569
624
|
</label>
|
|
@@ -574,7 +629,7 @@ function InfoStep({
|
|
|
574
629
|
<div>
|
|
575
630
|
<button
|
|
576
631
|
onClick={() => setShowReadme(!showReadme)}
|
|
577
|
-
className="flex items-center gap-2 text-sm text-
|
|
632
|
+
className="flex items-center gap-2 text-sm text-accent-text hover:underline"
|
|
578
633
|
>
|
|
579
634
|
<BookOpen className="w-4 h-4" />
|
|
580
635
|
{showReadme ? 'Hide' : 'Show'} Chart README
|
|
@@ -607,7 +662,7 @@ function ValuesStep({ valuesYaml, setValuesYaml, yamlError, setYamlError, chartD
|
|
|
607
662
|
const ahDetail = chartDetail as ArtifactHubChartDetail | undefined
|
|
608
663
|
|
|
609
664
|
const defaultValues = isLocal
|
|
610
|
-
? (localDetail?.values ? yaml.stringify(localDetail.values) : '')
|
|
665
|
+
? (localDetail?.values ? yaml.stringify(localDetail.values, { lineWidth: 0 }) : '')
|
|
611
666
|
: (ahDetail?.values || '')
|
|
612
667
|
|
|
613
668
|
const hasDefaults = Boolean(defaultValues)
|
|
@@ -619,8 +674,8 @@ function ValuesStep({ valuesYaml, setValuesYaml, yamlError, setYamlError, chartD
|
|
|
619
674
|
return (
|
|
620
675
|
<div className="space-y-4">
|
|
621
676
|
{/* Info banner */}
|
|
622
|
-
<div className="flex items-start gap-3 p-4 bg-
|
|
623
|
-
<CheckCircle className="w-5 h-5 text-
|
|
677
|
+
<div className="flex items-start gap-3 p-4 bg-accent-muted border border-accent/30 rounded-lg">
|
|
678
|
+
<CheckCircle className="w-5 h-5 text-accent shrink-0 mt-0.5" />
|
|
624
679
|
<div>
|
|
625
680
|
<p className="text-sm font-medium text-theme-text-primary">Ready to install with defaults</p>
|
|
626
681
|
<p className="text-xs text-theme-text-secondary mt-1">
|
|
@@ -633,7 +688,7 @@ function ValuesStep({ valuesYaml, setValuesYaml, yamlError, setYamlError, chartD
|
|
|
633
688
|
href={homeUrl}
|
|
634
689
|
target="_blank"
|
|
635
690
|
rel="noopener noreferrer"
|
|
636
|
-
className="inline-flex items-center gap-1 text-xs text-
|
|
691
|
+
className="inline-flex items-center gap-1 text-xs text-accent-text hover:underline mt-2"
|
|
637
692
|
>
|
|
638
693
|
<LinkIcon className="w-3 h-3" />
|
|
639
694
|
View chart documentation
|
|
@@ -701,6 +756,7 @@ function ValuesStep({ valuesYaml, setValuesYaml, yamlError, setYamlError, chartD
|
|
|
701
756
|
<YamlEditor
|
|
702
757
|
value={valuesYaml}
|
|
703
758
|
onChange={setValuesYaml}
|
|
759
|
+
showProblems={false}
|
|
704
760
|
height="300px"
|
|
705
761
|
onValidate={(isValid, errors) => {
|
|
706
762
|
setYamlError(isValid ? null : errors[0] || 'Invalid YAML')
|
|
@@ -819,8 +875,8 @@ function ReviewStep({
|
|
|
819
875
|
|
|
820
876
|
return (
|
|
821
877
|
<div className="space-y-6">
|
|
822
|
-
<div className="flex items-center gap-3 p-4 bg-
|
|
823
|
-
<CheckCircle className="w-6 h-6 text-
|
|
878
|
+
<div className="flex items-center gap-3 p-4 bg-accent-muted border border-accent/30 rounded-lg">
|
|
879
|
+
<CheckCircle className="w-6 h-6 text-accent shrink-0" />
|
|
824
880
|
<div>
|
|
825
881
|
<p className="text-sm font-medium text-theme-text-primary">Ready to install</p>
|
|
826
882
|
<p className="text-xs text-theme-text-secondary mt-0.5">
|
|
@@ -836,7 +892,7 @@ function ReviewStep({
|
|
|
836
892
|
<div>
|
|
837
893
|
<p className="text-sm font-medium text-amber-400">Installing from ArtifactHub</p>
|
|
838
894
|
<p className="text-xs text-theme-text-secondary mt-1">
|
|
839
|
-
This chart will be installed from: <code className="
|
|
895
|
+
This chart will be installed from: <code className="inline-code">{artifactHubRepoUrl || repo}</code>
|
|
840
896
|
</p>
|
|
841
897
|
</div>
|
|
842
898
|
</div>
|
|
@@ -873,7 +929,7 @@ function ReviewStep({
|
|
|
873
929
|
<>Local: {repo}</>
|
|
874
930
|
) : (
|
|
875
931
|
<>
|
|
876
|
-
<Globe className="w-3.5 h-3.5 text-
|
|
932
|
+
<Globe className="w-3.5 h-3.5 text-accent" />
|
|
877
933
|
ArtifactHub: {repo}
|
|
878
934
|
</>
|
|
879
935
|
)}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { X, GitCompare } from 'lucide-react'
|
|
2
|
-
import { PaneLoader } from '@skyhook-io/k8s-ui'
|
|
3
|
-
import { clsx } from 'clsx'
|
|
2
|
+
import { PaneLoader, DiffLine, hasDiffBodyChange } from '@skyhook-io/k8s-ui'
|
|
4
3
|
|
|
5
4
|
interface ManifestDiffViewerProps {
|
|
6
5
|
diff: string
|
|
@@ -8,19 +7,21 @@ interface ManifestDiffViewerProps {
|
|
|
8
7
|
revision1: number
|
|
9
8
|
revision2: number
|
|
10
9
|
onClose: () => void
|
|
10
|
+
title?: string
|
|
11
|
+
emptyLabel?: string
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
export function ManifestDiffViewer({ diff, isLoading, revision1, revision2, onClose }: ManifestDiffViewerProps) {
|
|
14
|
+
export function ManifestDiffViewer({ diff, isLoading, revision1, revision2, onClose, title, emptyLabel }: ManifestDiffViewerProps) {
|
|
14
15
|
if (isLoading) {
|
|
15
16
|
return <PaneLoader label="Computing diff…" className="h-32" />
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
if (!diff) {
|
|
19
|
+
if (!hasDiffBodyChange(diff)) {
|
|
19
20
|
return (
|
|
20
21
|
<div className="p-4">
|
|
21
22
|
<div className="flex flex-col items-center justify-center h-32 text-theme-text-tertiary gap-2">
|
|
22
23
|
<GitCompare className="w-8 h-8 text-theme-text-disabled" />
|
|
23
|
-
<span>No differences found</span>
|
|
24
|
+
<span>{emptyLabel || 'No differences found'}</span>
|
|
24
25
|
</div>
|
|
25
26
|
</div>
|
|
26
27
|
)
|
|
@@ -32,7 +33,7 @@ export function ManifestDiffViewer({ diff, isLoading, revision1, revision2, onCl
|
|
|
32
33
|
<div className="flex items-center gap-2">
|
|
33
34
|
<GitCompare className="w-4 h-4 text-theme-text-secondary" />
|
|
34
35
|
<span className="text-sm font-medium text-theme-text-secondary">
|
|
35
|
-
Comparing Revision {revision1} → {revision2}
|
|
36
|
+
{title || `Comparing Revision ${revision1} → ${revision2}`}
|
|
36
37
|
</span>
|
|
37
38
|
</div>
|
|
38
39
|
<button
|
|
@@ -44,7 +45,7 @@ export function ManifestDiffViewer({ diff, isLoading, revision1, revision2, onCl
|
|
|
44
45
|
</button>
|
|
45
46
|
</div>
|
|
46
47
|
|
|
47
|
-
<div className="rounded-lg
|
|
48
|
+
<div className="rounded-lg max-h-[calc(100vh-300px)] overflow-auto bg-theme-base/50 font-mono text-xs">
|
|
48
49
|
<div className="p-3">
|
|
49
50
|
{diff.split('\n').map((line, index) => (
|
|
50
51
|
<DiffLine key={index} line={line} />
|
|
@@ -66,23 +67,3 @@ export function ManifestDiffViewer({ diff, isLoading, revision1, revision2, onCl
|
|
|
66
67
|
</div>
|
|
67
68
|
)
|
|
68
69
|
}
|
|
69
|
-
|
|
70
|
-
function DiffLine({ line }: { line: string }) {
|
|
71
|
-
const isAddition = line.startsWith('+') && !line.startsWith('+++')
|
|
72
|
-
const isRemoval = line.startsWith('-') && !line.startsWith('---')
|
|
73
|
-
const isHeader = line.startsWith('---') || line.startsWith('+++') || line.startsWith('@@')
|
|
74
|
-
|
|
75
|
-
return (
|
|
76
|
-
<div
|
|
77
|
-
className={clsx(
|
|
78
|
-
'whitespace-pre',
|
|
79
|
-
isAddition && 'bg-green-500/10 text-green-400',
|
|
80
|
-
isRemoval && 'bg-red-500/10 text-red-400',
|
|
81
|
-
isHeader && 'text-theme-text-tertiary font-bold',
|
|
82
|
-
!isAddition && !isRemoval && !isHeader && 'text-theme-text-secondary'
|
|
83
|
-
)}
|
|
84
|
-
>
|
|
85
|
-
{line || ' '}
|
|
86
|
-
</div>
|
|
87
|
-
)
|
|
88
|
-
}
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import { useState, useCallback } from 'react'
|
|
2
|
-
import { Link2, ExternalLink, AlertCircle, Terminal, FileText,
|
|
2
|
+
import { Link2, ExternalLink, AlertCircle, Terminal, FileText, X, Loader2 } from 'lucide-react'
|
|
3
3
|
import { getResourceIcon } from '../../utils/resource-icons'
|
|
4
4
|
import { clsx } from 'clsx'
|
|
5
5
|
import type { HelmOwnedResource } from '../../types'
|
|
6
6
|
import type { NavigateToResource } from '../../utils/navigation'
|
|
7
|
-
import { kindToPlural } from '../../utils/navigation'
|
|
7
|
+
import { kindToPlural, apiVersionToGroup } from '../../utils/navigation'
|
|
8
8
|
import { getResourceStatusColor, SEVERITY_BADGE } from '../../utils/badge-colors'
|
|
9
9
|
import { useQueryClient } from '@tanstack/react-query'
|
|
10
10
|
import { useOpenTerminal, useOpenLogs } from '../dock'
|
|
11
|
-
import {
|
|
11
|
+
import { PortForwardInlineButton } from '../portforward/PortForwardButton'
|
|
12
12
|
import { useAvailablePorts } from '../../api/client'
|
|
13
13
|
import { apiUrl, getAuthHeaders, getCredentialsMode } from '../../api/config'
|
|
14
|
-
import { useNamespacedCapabilities } from '../../contexts/CapabilitiesContext'
|
|
14
|
+
import { useNamespacedCapabilities, useIsLocalDeployment } from '../../contexts/CapabilitiesContext'
|
|
15
15
|
import { pluralize } from '@skyhook-io/k8s-ui'
|
|
16
|
+
import { Tooltip } from '../ui/Tooltip'
|
|
16
17
|
|
|
17
18
|
interface OwnedResourcesProps {
|
|
18
19
|
resources: HelmOwnedResource[]
|
|
@@ -103,13 +104,14 @@ export function OwnedResources({ resources, onNavigate }: OwnedResourcesProps) {
|
|
|
103
104
|
) : (
|
|
104
105
|
<span className="flex items-center gap-2">
|
|
105
106
|
Showing {filteredResources.length} of {resources.length} resources
|
|
107
|
+
<Tooltip content="Clear filter">
|
|
106
108
|
<button
|
|
107
109
|
onClick={() => setHealthFilter('all')}
|
|
108
110
|
className="p-0.5 text-theme-text-tertiary hover:text-theme-text-primary hover:bg-theme-elevated rounded"
|
|
109
|
-
title="Clear filter"
|
|
110
111
|
>
|
|
111
112
|
<X className="w-3.5 h-3.5" />
|
|
112
113
|
</button>
|
|
114
|
+
</Tooltip>
|
|
113
115
|
</span>
|
|
114
116
|
)}
|
|
115
117
|
</div>
|
|
@@ -205,7 +207,7 @@ function ResourceItem({ resource, onNavigate }: ResourceItemProps) {
|
|
|
205
207
|
|
|
206
208
|
const handleClick = () => {
|
|
207
209
|
if (onNavigate) {
|
|
208
|
-
onNavigate({ kind: kindToPlural(resource.kind), namespace: resource.namespace, name: resource.name })
|
|
210
|
+
onNavigate({ kind: kindToPlural(resource.kind), namespace: resource.namespace, name: resource.name, group: apiVersionToGroup(resource.apiVersion) })
|
|
209
211
|
}
|
|
210
212
|
}
|
|
211
213
|
|
|
@@ -255,39 +257,44 @@ function ResourceItem({ resource, onNavigate }: ResourceItemProps) {
|
|
|
255
257
|
|
|
256
258
|
{/* Status badge */}
|
|
257
259
|
{resource.status && (
|
|
260
|
+
<Tooltip content={resource.message || resource.status}>
|
|
258
261
|
<span
|
|
259
262
|
className={clsx('badge-sm', getResourceStatusColor(resource.status || ''))}
|
|
260
|
-
title={resource.message || resource.status}
|
|
261
263
|
>
|
|
262
264
|
{resource.status}
|
|
263
265
|
</span>
|
|
266
|
+
</Tooltip>
|
|
264
267
|
)}
|
|
265
268
|
|
|
266
269
|
{/* Issue summary (e.g., "OOMKilled", "CrashLoopBackOff") */}
|
|
267
270
|
{resource.issue && (
|
|
271
|
+
<Tooltip content={resource.summary || resource.issue}>
|
|
268
272
|
<span
|
|
269
273
|
className="text-xs text-red-400"
|
|
270
|
-
title={resource.summary || resource.issue}
|
|
271
274
|
>
|
|
272
275
|
{resource.issue}
|
|
273
276
|
</span>
|
|
277
|
+
</Tooltip>
|
|
274
278
|
)}
|
|
275
279
|
|
|
276
280
|
{/* Error icon with message tooltip */}
|
|
277
281
|
{isError && resource.message && !resource.issue && (
|
|
278
|
-
<
|
|
282
|
+
<Tooltip content={resource.message}>
|
|
283
|
+
<span>
|
|
279
284
|
<AlertCircle className="w-3.5 h-3.5 text-red-400" />
|
|
280
285
|
</span>
|
|
286
|
+
</Tooltip>
|
|
281
287
|
)}
|
|
282
288
|
|
|
283
289
|
{canNavigate && (
|
|
290
|
+
<Tooltip content="View details">
|
|
284
291
|
<button
|
|
285
292
|
onClick={handleClick}
|
|
286
293
|
className="p-1 text-theme-text-tertiary opacity-0 group-hover:opacity-100 hover:text-theme-text-primary hover:bg-theme-elevated rounded transition-all"
|
|
287
|
-
title="View details"
|
|
288
294
|
>
|
|
289
295
|
<ExternalLink className="w-3.5 h-3.5" />
|
|
290
296
|
</button>
|
|
297
|
+
</Tooltip>
|
|
291
298
|
)}
|
|
292
299
|
</div>
|
|
293
300
|
</div>
|
|
@@ -305,11 +312,14 @@ function PodQuickActions({ namespace, podName, isRunning }: PodQuickActionsProps
|
|
|
305
312
|
const queryClient = useQueryClient()
|
|
306
313
|
const openTerminal = useOpenTerminal()
|
|
307
314
|
const openLogs = useOpenLogs()
|
|
308
|
-
const startPortForward = useStartPortForward()
|
|
309
315
|
const { data: portsData, isLoading: portsLoading } = useAvailablePorts('pod', namespace, podName)
|
|
310
316
|
|
|
311
317
|
// Capabilities (namespace-scoped: re-checks RBAC if globally denied)
|
|
312
318
|
const { canExec, canViewLogs, canPortForward } = useNamespacedCapabilities(namespace)
|
|
319
|
+
// Live forward (local + RBAC) or the kubectl copy-command (in-cluster/Cloud);
|
|
320
|
+
// PortForwardInlineButton picks which by deployment mode.
|
|
321
|
+
const isLocal = useIsLocalDeployment()
|
|
322
|
+
const showPortForward = canPortForward || !isLocal
|
|
313
323
|
|
|
314
324
|
const [isLoadingAction, setIsLoadingAction] = useState(false)
|
|
315
325
|
|
|
@@ -367,14 +377,6 @@ function PodQuickActions({ namespace, podName, isRunning }: PodQuickActionsProps
|
|
|
367
377
|
}
|
|
368
378
|
}, [namespace, podName, openLogs, fetchPodData])
|
|
369
379
|
|
|
370
|
-
const handlePortForward = useCallback((port: number) => {
|
|
371
|
-
startPortForward.mutate({
|
|
372
|
-
namespace,
|
|
373
|
-
podName,
|
|
374
|
-
podPort: port,
|
|
375
|
-
})
|
|
376
|
-
}, [namespace, podName, startPortForward])
|
|
377
|
-
|
|
378
380
|
const ports = portsData?.ports || []
|
|
379
381
|
|
|
380
382
|
return (
|
|
@@ -383,42 +385,33 @@ function PodQuickActions({ namespace, podName, isRunning }: PodQuickActionsProps
|
|
|
383
385
|
|
|
384
386
|
{/* Terminal */}
|
|
385
387
|
{isRunning && canExec && (
|
|
388
|
+
<Tooltip content="Open terminal">
|
|
386
389
|
<button
|
|
387
390
|
onClick={(e) => { e.stopPropagation(); handleOpenTerminal() }}
|
|
388
391
|
disabled={isLoadingAction}
|
|
389
|
-
className="p-1 text-theme-text-tertiary hover:text-blue-400 hover:bg-blue-500/10 rounded transition-colors disabled:opacity-50"
|
|
390
|
-
title="Open terminal"
|
|
392
|
+
className="p-1 text-theme-text-tertiary hover:text-blue-400 hover:bg-blue-500/10 rounded transition-colors disabled:opacity-50 disabled:pointer-events-none"
|
|
391
393
|
>
|
|
392
394
|
<Terminal className="w-3.5 h-3.5" />
|
|
393
395
|
</button>
|
|
396
|
+
</Tooltip>
|
|
394
397
|
)}
|
|
395
398
|
|
|
396
399
|
{/* Logs */}
|
|
397
400
|
{canViewLogs && (
|
|
401
|
+
<Tooltip content="View logs">
|
|
398
402
|
<button
|
|
399
403
|
onClick={(e) => { e.stopPropagation(); handleOpenLogs() }}
|
|
400
404
|
disabled={isLoadingAction}
|
|
401
|
-
className="p-1 text-theme-text-tertiary hover:text-blue-400 hover:bg-blue-500/10 rounded transition-colors disabled:opacity-50"
|
|
402
|
-
title="View logs"
|
|
405
|
+
className="p-1 text-theme-text-tertiary hover:text-blue-400 hover:bg-blue-500/10 rounded transition-colors disabled:opacity-50 disabled:pointer-events-none"
|
|
403
406
|
>
|
|
404
407
|
<FileText className="w-3.5 h-3.5" />
|
|
405
408
|
</button>
|
|
409
|
+
</Tooltip>
|
|
406
410
|
)}
|
|
407
411
|
|
|
408
|
-
{/* Port Forward */}
|
|
409
|
-
{
|
|
410
|
-
<
|
|
411
|
-
onClick={(e) => { e.stopPropagation(); handlePortForward(ports[0].port) }}
|
|
412
|
-
disabled={startPortForward.isPending}
|
|
413
|
-
className="p-1 text-theme-text-tertiary hover:text-blue-400 hover:bg-blue-500/10 rounded transition-colors disabled:opacity-50"
|
|
414
|
-
title={`Port forward :${ports[0].port}`}
|
|
415
|
-
>
|
|
416
|
-
{startPortForward.isPending ? (
|
|
417
|
-
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
|
418
|
-
) : (
|
|
419
|
-
<Plug className="w-3.5 h-3.5" />
|
|
420
|
-
)}
|
|
421
|
-
</button>
|
|
412
|
+
{/* Port Forward (live locally; kubectl copy-command in-cluster/Cloud) */}
|
|
413
|
+
{showPortForward && !portsLoading && ports.length > 0 && (
|
|
414
|
+
<PortForwardInlineButton namespace={namespace} podName={podName} port={ports[0].port} />
|
|
422
415
|
)}
|
|
423
416
|
</div>
|
|
424
417
|
)
|
|
@@ -431,36 +424,18 @@ interface ServiceQuickActionsProps {
|
|
|
431
424
|
}
|
|
432
425
|
|
|
433
426
|
function ServiceQuickActions({ namespace, serviceName }: ServiceQuickActionsProps) {
|
|
434
|
-
const startPortForward = useStartPortForward()
|
|
435
427
|
const { data: portsData, isLoading: portsLoading } = useAvailablePorts('service', namespace, serviceName)
|
|
436
428
|
const { canPortForward } = useNamespacedCapabilities(namespace)
|
|
437
|
-
|
|
438
|
-
const
|
|
439
|
-
startPortForward.mutate({
|
|
440
|
-
namespace,
|
|
441
|
-
serviceName,
|
|
442
|
-
podPort: port,
|
|
443
|
-
})
|
|
444
|
-
}, [namespace, serviceName, startPortForward])
|
|
429
|
+
const isLocal = useIsLocalDeployment()
|
|
430
|
+
const showPortForward = canPortForward || !isLocal
|
|
445
431
|
|
|
446
432
|
const ports = portsData?.ports || []
|
|
447
433
|
|
|
448
|
-
if (!
|
|
434
|
+
if (!showPortForward || portsLoading || ports.length === 0) return null
|
|
449
435
|
|
|
450
436
|
return (
|
|
451
437
|
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
452
|
-
<
|
|
453
|
-
onClick={(e) => { e.stopPropagation(); handlePortForward(ports[0].port) }}
|
|
454
|
-
disabled={startPortForward.isPending}
|
|
455
|
-
className="p-1 text-theme-text-tertiary hover:text-blue-400 hover:bg-blue-500/10 rounded transition-colors disabled:opacity-50"
|
|
456
|
-
title={`Port forward :${ports[0].port}`}
|
|
457
|
-
>
|
|
458
|
-
{startPortForward.isPending ? (
|
|
459
|
-
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
|
460
|
-
) : (
|
|
461
|
-
<Plug className="w-3.5 h-3.5" />
|
|
462
|
-
)}
|
|
463
|
-
</button>
|
|
438
|
+
<PortForwardInlineButton namespace={namespace} serviceName={serviceName} port={ports[0].port} />
|
|
464
439
|
</div>
|
|
465
440
|
)
|
|
466
441
|
}
|