@skyhook-io/radar-app 1.8.0 → 1.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/App.tsx +95 -16
- package/src/api/client.ts +64 -3
- package/src/components/DebugOverlay.tsx +1 -1
- package/src/components/applications/ApplicationsView.tsx +1 -1
- package/src/components/compare/CompareViewRoute.tsx +13 -5
- package/src/components/cost/CostTrendChart.tsx +3 -3
- package/src/components/cost/CostView.tsx +1 -1
- package/src/components/helm/ChartBrowser.tsx +1 -1
- package/src/components/helm/HelmReleaseDrawer.tsx +233 -19
- package/src/components/helm/HelmView.tsx +85 -16
- package/src/components/helm/InstallWizard.tsx +1 -1
- package/src/components/helm/RevisionHistory.tsx +46 -2
- package/src/components/helm/TrackChartSourceDialog.tsx +141 -0
- package/src/components/home/ActivitySummary.tsx +4 -1
- package/src/components/home/TrafficSummary.tsx +2 -2
- package/src/components/home/mcpToolCatalog.ts +5 -5
- package/src/components/issues/IssuesPane.tsx +2 -2
- package/src/components/nav/PrimaryNavRail.tsx +1 -1
- package/src/components/portforward/PortForwardManager.tsx +70 -8
- package/src/components/resource/PrometheusChartsGrid.tsx +1 -1
- package/src/components/resources/ResourcesView.tsx +2 -0
- package/src/components/settings/MyPermissionsDialog.tsx +64 -4
- package/src/components/shared/LargeClusterNamespacePicker.tsx +1 -1
- package/src/components/traffic/TrafficGraph.tsx +29 -20
- package/src/components/traffic/TrafficView.tsx +3 -3
- package/src/components/ui/DiagnosticsOverlay.tsx +1 -1
- package/src/components/ui/ShortcutHelpOverlay.tsx +1 -1
- package/src/components/ui/command-items.ts +1 -1
- package/src/components/workload/WorkloadView.tsx +17 -17
- package/src/context/ConnectionContext.tsx +29 -2
- package/src/main.tsx +1 -1
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import { useState, useCallback, useEffect, useRef } from 'react'
|
|
2
2
|
import { flushSync } from 'react-dom'
|
|
3
|
-
import { FetchResult, useDockReservedHeight } from '@skyhook-io/k8s-ui'
|
|
3
|
+
import { FetchResult, useDockReservedHeight, compareVersions } from '@skyhook-io/k8s-ui'
|
|
4
4
|
import { startViewTransitionSafe } from '@skyhook-io/k8s-ui/utils/view-transition'
|
|
5
5
|
import { TRANSITION_DRAWER } from '../../utils/animation'
|
|
6
6
|
import { useRefreshAnimation } from '../../hooks/useRefreshAnimation'
|
|
7
|
-
import { X, Copy, Check, RefreshCw, Package, Code, History, FileText, Settings, Link2, Anchor, GitFork, BookOpen, ArrowUpCircle, Trash2, GitBranch } from 'lucide-react'
|
|
7
|
+
import { X, Copy, Check, RefreshCw, Package, Code, History, FileText, Settings, Link2, Anchor, GitFork, BookOpen, ArrowUpCircle, Trash2, GitBranch, AlertTriangle, RotateCcw, Clock } from 'lucide-react'
|
|
8
8
|
import { useNavigate } from 'react-router-dom'
|
|
9
9
|
import { clsx } from 'clsx'
|
|
10
|
-
import { useHelmRelease, useHelmManifest, useHelmValues, useHelmManifestDiff, useHelmUpgradeInfo, useHelmUninstall, upgradeWithProgress, rollbackWithProgress } from '../../api/client'
|
|
10
|
+
import { useHelmRelease, useHelmManifest, useHelmValues, useHelmManifestDiff, useHelmUpgradeInfo, useHelmReleaseVersions, useHelmUninstall, upgradeWithProgress, rollbackWithProgress } from '../../api/client'
|
|
11
11
|
import { useQueryClient } from '@tanstack/react-query'
|
|
12
12
|
import { ConfirmDialog } from '../ui/ConfirmDialog'
|
|
13
13
|
import { Tooltip } from '../ui/Tooltip'
|
|
14
14
|
import { Markdown } from '../ui/Markdown'
|
|
15
|
-
import type { SelectedHelmRelease, HelmHook, ChartDependency } from '../../types'
|
|
15
|
+
import type { SelectedHelmRelease, HelmHook, ChartDependency, HelmOperation } from '../../types'
|
|
16
16
|
import type { NavigateToResource } from '../../utils/navigation'
|
|
17
17
|
import { formatDate } from './helm-utils'
|
|
18
18
|
import { getHelmStatusColor, SEVERITY_BADGE, SEVERITY_TEXT } from '../../utils/badge-colors'
|
|
@@ -23,6 +23,7 @@ import { ManifestViewer } from './ManifestViewer'
|
|
|
23
23
|
import { ValuesViewer } from './ValuesViewer'
|
|
24
24
|
import { OwnedResources } from './OwnedResources'
|
|
25
25
|
import { ManifestDiffViewer } from './ManifestDiffViewer'
|
|
26
|
+
import { TrackChartSourceDialog } from './TrackChartSourceDialog'
|
|
26
27
|
|
|
27
28
|
interface HelmReleaseDrawerProps {
|
|
28
29
|
release: SelectedHelmRelease
|
|
@@ -50,6 +51,8 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
50
51
|
const [rollbackRevision, setRollbackRevision] = useState<number | null>(null)
|
|
51
52
|
const [showUninstallConfirm, setShowUninstallConfirm] = useState(false)
|
|
52
53
|
const [showUpgradeConfirm, setShowUpgradeConfirm] = useState(false)
|
|
54
|
+
const [showTrackSource, setShowTrackSource] = useState(false)
|
|
55
|
+
const [selectedVersion, setSelectedVersion] = useState<string | null>(null)
|
|
53
56
|
const resizeStartX = useRef(0)
|
|
54
57
|
const resizeStartWidth = useRef(DEFAULT_WIDTH)
|
|
55
58
|
const { allowed: canHelmWrite, reason: helmActReason } = useCanHelmAct()
|
|
@@ -99,6 +102,17 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
99
102
|
)
|
|
100
103
|
const upgradeErrorMessage = upgradeError instanceof Error ? upgradeError.message : 'Upgrade check failed'
|
|
101
104
|
|
|
105
|
+
// Available versions for the upgrade dialog's picker — only fetched while the
|
|
106
|
+
// confirm dialog is open. Default the selection to latest when it opens.
|
|
107
|
+
const { data: availableVersions } = useHelmReleaseVersions(helmNamespace, release.name, showUpgradeConfirm)
|
|
108
|
+
const targetVersion = selectedVersion ?? upgradeInfo?.latestVersion ?? ''
|
|
109
|
+
// Semver compare, not list-position: the installed version may be older than
|
|
110
|
+
// the newest-N versions the picker shows, so it isn't always in the list.
|
|
111
|
+
const isDowngrade = Boolean(
|
|
112
|
+
targetVersion && upgradeInfo?.currentVersion &&
|
|
113
|
+
compareVersions(targetVersion, upgradeInfo.currentVersion) === -1
|
|
114
|
+
)
|
|
115
|
+
|
|
102
116
|
// Mutations for actions
|
|
103
117
|
const uninstallMutation = useHelmUninstall()
|
|
104
118
|
const queryClient = useQueryClient()
|
|
@@ -234,7 +248,7 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
234
248
|
}
|
|
235
249
|
|
|
236
250
|
const handleUpgradeConfirm = async () => {
|
|
237
|
-
if (!
|
|
251
|
+
if (!targetVersion) return
|
|
238
252
|
setIsUpgrading(true)
|
|
239
253
|
setUpgradeProgress([])
|
|
240
254
|
|
|
@@ -242,8 +256,8 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
242
256
|
await upgradeWithProgress(
|
|
243
257
|
helmNamespace,
|
|
244
258
|
release.name,
|
|
245
|
-
|
|
246
|
-
upgradeInfo
|
|
259
|
+
targetVersion,
|
|
260
|
+
upgradeInfo?.repositoryName,
|
|
247
261
|
(event) => {
|
|
248
262
|
if (event.type === 'progress' && event.message) {
|
|
249
263
|
setUpgradeProgress(prev => [...prev, {
|
|
@@ -256,7 +270,7 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
256
270
|
|
|
257
271
|
setUpgradeProgress(prev => [...prev, {
|
|
258
272
|
phase: 'complete',
|
|
259
|
-
message: `Successfully upgraded to ${
|
|
273
|
+
message: `Successfully upgraded to ${targetVersion}`,
|
|
260
274
|
}])
|
|
261
275
|
|
|
262
276
|
// Invalidate queries
|
|
@@ -268,6 +282,7 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
268
282
|
setTimeout(() => {
|
|
269
283
|
setShowUpgradeConfirm(false)
|
|
270
284
|
setUpgradeProgress([])
|
|
285
|
+
setSelectedVersion(null)
|
|
271
286
|
refetch()
|
|
272
287
|
switchTab('resources')
|
|
273
288
|
}, 1500)
|
|
@@ -342,8 +357,18 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
342
357
|
upgrade check failed
|
|
343
358
|
</span>
|
|
344
359
|
</Tooltip>
|
|
360
|
+
) : upgradeInfo?.updateAvailable && releaseDetail?.managedByFluxHelmRelease ? (
|
|
361
|
+
// Route-only for GitOps-managed releases: a direct `helm upgrade`
|
|
362
|
+
// would be reverted at the next reconcile, so surface the available
|
|
363
|
+
// version as info and point at GitOps rather than offer the upgrade.
|
|
364
|
+
<Tooltip content={`${upgradeInfo.latestVersion} available — managed by Flux, upgrade via the GitOps view (a direct upgrade would be reverted at the next reconcile).`}>
|
|
365
|
+
<span className={clsx('badge', SEVERITY_BADGE.warning, 'opacity-90')}>
|
|
366
|
+
<ArrowUpCircle className="w-3 h-3" />
|
|
367
|
+
{upgradeInfo.latestVersion}
|
|
368
|
+
</span>
|
|
369
|
+
</Tooltip>
|
|
345
370
|
) : upgradeInfo?.updateAvailable ? (
|
|
346
|
-
<Tooltip content={canHelmWrite ? `Click to upgrade: ${upgradeInfo.currentVersion} → ${upgradeInfo.latestVersion}${upgradeInfo.repositoryName ? ` (${upgradeInfo.repositoryName})` : ''}` : helmActReason}>
|
|
371
|
+
<Tooltip content={canHelmWrite ? `Click to upgrade: ${upgradeInfo.currentVersion} → ${upgradeInfo.latestVersion}${upgradeInfo.repositoryName ? ` (${upgradeInfo.repositoryName})` : upgradeInfo.sourceType === 'oci' ? ' (OCI)' : ''}` : helmActReason}>
|
|
347
372
|
<button
|
|
348
373
|
onClick={() => setShowUpgradeConfirm(true)}
|
|
349
374
|
disabled={!canHelmWrite}
|
|
@@ -363,13 +388,39 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
363
388
|
</span>
|
|
364
389
|
</Tooltip>
|
|
365
390
|
) : upgradeInfo?.error ? (
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
391
|
+
releaseDetail?.managedByFluxHelmRelease ? (
|
|
392
|
+
// Managed by Flux — the "Managed by Flux" badge routes to GitOps,
|
|
393
|
+
// where the chart source lives. Don't push a Helm source here.
|
|
394
|
+
<Tooltip content="Chart source is managed by Flux — track upgrades from the GitOps view.">
|
|
395
|
+
<span className="badge bg-theme-hover/50 text-theme-text-secondary">
|
|
396
|
+
source via GitOps
|
|
397
|
+
</span>
|
|
398
|
+
</Tooltip>
|
|
399
|
+
) : upgradeInfo.untracked ? (
|
|
400
|
+
// Helm doesn't record the install source. Offer to register one so
|
|
401
|
+
// Radar can track upgrades for the user's own (e.g. OCI) charts.
|
|
402
|
+
<Tooltip content={canHelmWrite ? "Radar can't tell where this chart was installed from. Register your chart source to track upgrades." : upgradeInfo.error}>
|
|
403
|
+
<button
|
|
404
|
+
onClick={() => canHelmWrite && setShowTrackSource(true)}
|
|
405
|
+
disabled={!canHelmWrite}
|
|
406
|
+
className={clsx(
|
|
407
|
+
'badge transition-colors disabled:pointer-events-none bg-theme-hover/50 text-theme-text-secondary',
|
|
408
|
+
canHelmWrite ? 'hover:bg-theme-hover cursor-pointer' : 'opacity-50 cursor-not-allowed'
|
|
409
|
+
)}
|
|
410
|
+
>
|
|
411
|
+
<Link2 className="w-3 h-3" />
|
|
412
|
+
source not tracked
|
|
413
|
+
</button>
|
|
414
|
+
</Tooltip>
|
|
415
|
+
) : (
|
|
416
|
+
// Repo-side error (stale/broken index, classic ambiguity) — not an
|
|
417
|
+
// OCI tracking issue, so surface it without steering to registration.
|
|
418
|
+
<Tooltip content={upgradeInfo.error}>
|
|
419
|
+
<span className="badge bg-theme-hover/50 text-theme-text-secondary">
|
|
420
|
+
upgrade source unresolved
|
|
421
|
+
</span>
|
|
422
|
+
</Tooltip>
|
|
423
|
+
)
|
|
373
424
|
) : null}
|
|
374
425
|
</div>
|
|
375
426
|
<div className="flex items-center gap-1">
|
|
@@ -462,6 +513,10 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
462
513
|
<FetchResult loading={isLoading} error={releaseError} notFoundMessage="Release not found" className="h-32" />
|
|
463
514
|
) : (
|
|
464
515
|
<>
|
|
516
|
+
<HelmOperationBanner
|
|
517
|
+
operation={releaseDetail.lastOperation}
|
|
518
|
+
managedByFluxHelmRelease={releaseDetail.managedByFluxHelmRelease}
|
|
519
|
+
/>
|
|
465
520
|
{activeTab === 'overview' && (
|
|
466
521
|
<OverviewTab release={releaseDetail} onCopy={copyToClipboard} copied={copied} />
|
|
467
522
|
)}
|
|
@@ -469,6 +524,7 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
469
524
|
<RevisionHistory
|
|
470
525
|
history={releaseDetail.history}
|
|
471
526
|
currentRevision={releaseDetail.revision}
|
|
527
|
+
operations={mergeHelmOperations(releaseDetail.operations, releaseDetail.lastOperation)}
|
|
472
528
|
onViewRevision={handleViewRevision}
|
|
473
529
|
onCompare={handleCompareRevisions}
|
|
474
530
|
onRollback={canHelmWrite ? handleRollbackRequest : undefined}
|
|
@@ -572,6 +628,7 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
572
628
|
onClose={() => {
|
|
573
629
|
setShowUpgradeConfirm(false)
|
|
574
630
|
setUpgradeProgress([])
|
|
631
|
+
setSelectedVersion(null)
|
|
575
632
|
if (isUpgrading) {
|
|
576
633
|
// Upgrade continues server-side — switch to resources tab to monitor
|
|
577
634
|
setIsUpgrading(false)
|
|
@@ -580,18 +637,56 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
580
637
|
}}
|
|
581
638
|
onConfirm={handleUpgradeConfirm}
|
|
582
639
|
title="Upgrade Release"
|
|
583
|
-
message={`Upgrade "${release.name}" to version ${
|
|
640
|
+
message={`Upgrade "${release.name}" to version ${targetVersion}?`}
|
|
584
641
|
details={upgradeProgress.length === 0
|
|
585
|
-
? `
|
|
642
|
+
? `The chart will move from version ${upgradeInfo?.currentVersion} to ${targetVersion}. Your existing values will be preserved. The change is applied to your cluster immediately.`
|
|
586
643
|
: undefined
|
|
587
644
|
}
|
|
588
|
-
confirmLabel=
|
|
645
|
+
confirmLabel={isDowngrade ? 'Downgrade' : 'Upgrade'}
|
|
589
646
|
variant="warning"
|
|
590
647
|
isLoading={isUpgrading}
|
|
591
648
|
isClosable
|
|
592
649
|
>
|
|
650
|
+
{upgradeProgress.length === 0 && availableVersions && availableVersions.length > 1 && (
|
|
651
|
+
<div className="mb-1">
|
|
652
|
+
<label htmlFor="upgrade-version" className="block text-sm font-medium text-theme-text-secondary mb-1.5">
|
|
653
|
+
Target version
|
|
654
|
+
</label>
|
|
655
|
+
<select
|
|
656
|
+
id="upgrade-version"
|
|
657
|
+
value={targetVersion}
|
|
658
|
+
onChange={(e) => setSelectedVersion(e.target.value)}
|
|
659
|
+
disabled={isUpgrading}
|
|
660
|
+
className="w-full px-3 py-2 bg-theme-elevated border border-theme-border-light rounded-lg text-sm text-theme-text-primary focus:outline-none focus:ring-2 focus:ring-accent disabled:opacity-50"
|
|
661
|
+
>
|
|
662
|
+
{availableVersions.map((v) => (
|
|
663
|
+
<option key={v} value={v}>
|
|
664
|
+
{v}
|
|
665
|
+
{v === upgradeInfo?.latestVersion ? ' (latest)' : ''}
|
|
666
|
+
{v === upgradeInfo?.currentVersion ? ' (current)' : ''}
|
|
667
|
+
</option>
|
|
668
|
+
))}
|
|
669
|
+
</select>
|
|
670
|
+
{isDowngrade && (
|
|
671
|
+
<p className="mt-1 text-xs text-amber-600 dark:text-amber-400">
|
|
672
|
+
This is a downgrade from {upgradeInfo?.currentVersion}.
|
|
673
|
+
</p>
|
|
674
|
+
)}
|
|
675
|
+
{availableVersions.length >= 50 && (
|
|
676
|
+
<p className="mt-1 text-xs text-theme-text-tertiary">
|
|
677
|
+
Showing the 50 newest versions. Type to filter.
|
|
678
|
+
</p>
|
|
679
|
+
)}
|
|
680
|
+
</div>
|
|
681
|
+
)}
|
|
593
682
|
{upgradeProgress.length > 0 && <ProgressLog entries={upgradeProgress} />}
|
|
594
683
|
</ConfirmDialog>
|
|
684
|
+
|
|
685
|
+
<TrackChartSourceDialog
|
|
686
|
+
open={showTrackSource}
|
|
687
|
+
onClose={() => setShowTrackSource(false)}
|
|
688
|
+
chartName={releaseDetail?.chart}
|
|
689
|
+
/>
|
|
595
690
|
</div>
|
|
596
691
|
)
|
|
597
692
|
}
|
|
@@ -623,6 +718,125 @@ function ProgressLog({ entries }: { entries: { phase: string; message: string }[
|
|
|
623
718
|
)
|
|
624
719
|
}
|
|
625
720
|
|
|
721
|
+
function mergeHelmOperations(operations: HelmOperation[] | undefined, lastOperation: HelmOperation | undefined): HelmOperation[] {
|
|
722
|
+
const merged: HelmOperation[] = []
|
|
723
|
+
const seen = new Set<string>()
|
|
724
|
+
for (const op of [...(operations || []), ...(lastOperation ? [lastOperation] : [])]) {
|
|
725
|
+
const key = helmOperationKey(op)
|
|
726
|
+
if (seen.has(key)) continue
|
|
727
|
+
seen.add(key)
|
|
728
|
+
merged.push(op)
|
|
729
|
+
}
|
|
730
|
+
return merged
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function helmOperationKey(operation: HelmOperation): string {
|
|
734
|
+
return [
|
|
735
|
+
operation.kind,
|
|
736
|
+
operation.status,
|
|
737
|
+
operation.revision || 0,
|
|
738
|
+
operation.failedRevision || 0,
|
|
739
|
+
operation.rollbackRevision || 0,
|
|
740
|
+
operation.targetRevision || 0,
|
|
741
|
+
].join(':')
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function HelmOperationBanner({
|
|
745
|
+
operation,
|
|
746
|
+
managedByFluxHelmRelease,
|
|
747
|
+
}: {
|
|
748
|
+
operation?: HelmOperation
|
|
749
|
+
managedByFluxHelmRelease?: string
|
|
750
|
+
}) {
|
|
751
|
+
if (!operation || !shouldShowOperationBanner(operation)) {
|
|
752
|
+
return null
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
const isFailure = operation.status === 'failed'
|
|
756
|
+
const isPending = operation.status === 'stuck_pending'
|
|
757
|
+
const tone: 'error' | 'warning' | 'info' = isFailure ? 'error' : operation.kind === 'rollback' ? 'info' : 'warning'
|
|
758
|
+
const Icon = operation.kind === 'upgrade_rolled_back' || operation.kind === 'rollback' ? RotateCcw : isPending ? Clock : AlertTriangle
|
|
759
|
+
const title = operationTitle(operation)
|
|
760
|
+
|
|
761
|
+
return (
|
|
762
|
+
<div className="m-4 mb-0 card-inner-lg">
|
|
763
|
+
<div className="flex items-start gap-3">
|
|
764
|
+
<Icon className={clsx('mt-0.5 h-5 w-5 shrink-0', SEVERITY_TEXT[tone])} />
|
|
765
|
+
<div className="min-w-0 flex-1">
|
|
766
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
767
|
+
<span className="text-sm font-medium text-theme-text-primary">{title}</span>
|
|
768
|
+
<span className={clsx('badge-sm', SEVERITY_BADGE[tone])}>{operation.status.replace(/_/g, ' ')}</span>
|
|
769
|
+
<OperationRevisionChips operation={operation} />
|
|
770
|
+
</div>
|
|
771
|
+
<p className="mt-1 text-sm text-theme-text-secondary">{operation.message}</p>
|
|
772
|
+
{operation.failureDescription && (
|
|
773
|
+
<p className="mt-1 text-xs text-theme-text-tertiary truncate">
|
|
774
|
+
{operation.failureDescription}
|
|
775
|
+
</p>
|
|
776
|
+
)}
|
|
777
|
+
{operation.kind === 'upgrade_rolled_back' && (
|
|
778
|
+
<p className="mt-1 text-xs text-theme-text-tertiary">
|
|
779
|
+
Helm history does not record whether <code className="inline-code text-[11px]">--atomic</code> was set; the rollback is inferred from adjacent release revisions.
|
|
780
|
+
</p>
|
|
781
|
+
)}
|
|
782
|
+
{managedByFluxHelmRelease && (
|
|
783
|
+
<p className="mt-1 text-xs text-theme-text-tertiary">
|
|
784
|
+
This release is managed by Flux HelmRelease {managedByFluxHelmRelease}; direct Helm changes may be reconciled back.
|
|
785
|
+
</p>
|
|
786
|
+
)}
|
|
787
|
+
</div>
|
|
788
|
+
</div>
|
|
789
|
+
</div>
|
|
790
|
+
)
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function shouldShowOperationBanner(operation: HelmOperation): boolean {
|
|
794
|
+
return operation.kind === 'upgrade_rolled_back' || operation.kind === 'rollback' || operation.status === 'failed' || operation.status === 'stuck_pending'
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function operationTitle(operation: HelmOperation): string {
|
|
798
|
+
switch (operation.kind) {
|
|
799
|
+
case 'upgrade_rolled_back':
|
|
800
|
+
return 'Helm rolled back after failed upgrade'
|
|
801
|
+
case 'rollback':
|
|
802
|
+
return 'Helm rollback applied'
|
|
803
|
+
case 'pending':
|
|
804
|
+
return 'Helm operation may be stuck'
|
|
805
|
+
case 'upgrade_failed':
|
|
806
|
+
return 'Helm upgrade failed'
|
|
807
|
+
case 'release_failed':
|
|
808
|
+
return 'Helm release failed'
|
|
809
|
+
default:
|
|
810
|
+
return 'Helm operation'
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function OperationRevisionChips({ operation }: { operation: HelmOperation }) {
|
|
815
|
+
const chips: Array<{ key: string; label: string; className: string }> = []
|
|
816
|
+
if (operation.failedRevision) {
|
|
817
|
+
chips.push({ key: 'failed', label: `failed rev ${operation.failedRevision}`, className: SEVERITY_BADGE.error })
|
|
818
|
+
}
|
|
819
|
+
if (operation.rollbackRevision) {
|
|
820
|
+
chips.push({ key: 'rollback', label: `rollback rev ${operation.rollbackRevision}`, className: SEVERITY_BADGE.warning })
|
|
821
|
+
}
|
|
822
|
+
if (operation.targetRevision) {
|
|
823
|
+
chips.push({ key: 'target', label: `target rev ${operation.targetRevision}`, className: SEVERITY_BADGE.info })
|
|
824
|
+
}
|
|
825
|
+
if (!operation.failedRevision && !operation.rollbackRevision && operation.revision) {
|
|
826
|
+
chips.push({ key: 'revision', label: `rev ${operation.revision}`, className: SEVERITY_BADGE.neutral })
|
|
827
|
+
}
|
|
828
|
+
if (chips.length === 0) {
|
|
829
|
+
return null
|
|
830
|
+
}
|
|
831
|
+
return (
|
|
832
|
+
<>
|
|
833
|
+
{chips.map((chip) => (
|
|
834
|
+
<span key={chip.key} className={clsx('badge-sm', chip.className)}>{chip.label}</span>
|
|
835
|
+
))}
|
|
836
|
+
</>
|
|
837
|
+
)
|
|
838
|
+
}
|
|
839
|
+
|
|
626
840
|
// Overview tab content
|
|
627
841
|
interface OverviewTabProps {
|
|
628
842
|
release: {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { useState, useMemo, useRef, useEffect, useCallback, forwardRef } from 'react'
|
|
2
2
|
import { useRefreshAnimation } from '../../hooks/useRefreshAnimation'
|
|
3
3
|
import { useRegisterShortcuts } from '../../hooks/useKeyboardShortcuts'
|
|
4
|
-
import { Package, Search, RefreshCw, ArrowUpCircle, LayoutGrid, List, Shield, GitBranch, ChevronRight } from 'lucide-react'
|
|
4
|
+
import { Package, Search, RefreshCw, ArrowUpCircle, LayoutGrid, List, Shield, GitBranch, ChevronRight, RotateCcw, Clock } from 'lucide-react'
|
|
5
5
|
import { PaneLoader, PageHeader } from '@skyhook-io/k8s-ui'
|
|
6
6
|
import { clsx } from 'clsx'
|
|
7
7
|
import { useHelmReleases, useHelmBatchUpgradeInfo, isForbiddenError } from '../../api/client'
|
|
8
|
-
import type { HelmRelease, SelectedHelmRelease, UpgradeInfo, ChartSource } from '../../types'
|
|
9
|
-
import { getStatusColor, formatAge,
|
|
8
|
+
import type { HelmOperation, HelmRelease, SelectedHelmRelease, UpgradeInfo, ChartSource } from '../../types'
|
|
9
|
+
import { getStatusColor, formatAge, isHelmReleaseActionable } from './helm-utils'
|
|
10
10
|
import { SEVERITY_BADGE } from '../../utils/badge-colors'
|
|
11
11
|
import { Tooltip } from '../ui/Tooltip'
|
|
12
12
|
import { ChartBrowser } from './ChartBrowser'
|
|
@@ -291,22 +291,22 @@ export function HelmView({ namespaces, selectedRelease, onReleaseClick }: HelmVi
|
|
|
291
291
|
<table className="w-full table-fixed">
|
|
292
292
|
<thead className="bg-theme-surface sticky top-0 z-10">
|
|
293
293
|
<tr>
|
|
294
|
-
<th className="text-left px-4 py-3 text-xs font-medium text-theme-text-secondary uppercase tracking-wide">
|
|
294
|
+
<th className="text-left px-4 py-3 text-xs font-medium text-theme-text-secondary uppercase tracking-wide w-[28%]">
|
|
295
295
|
Name
|
|
296
296
|
</th>
|
|
297
|
-
<th className="text-left px-4 py-3 text-xs font-medium text-theme-text-secondary uppercase tracking-wide w-
|
|
297
|
+
<th className="text-left px-4 py-3 text-xs font-medium text-theme-text-secondary uppercase tracking-wide w-[18%]">
|
|
298
298
|
Namespace
|
|
299
299
|
</th>
|
|
300
|
-
<th className="text-left px-4 py-3 text-xs font-medium text-theme-text-secondary uppercase tracking-wide w-
|
|
300
|
+
<th className="text-left px-4 py-3 text-xs font-medium text-theme-text-secondary uppercase tracking-wide w-[22%]">
|
|
301
301
|
Chart
|
|
302
302
|
</th>
|
|
303
303
|
<th className="text-left px-4 py-3 text-xs font-medium text-theme-text-secondary uppercase tracking-wide w-24 hidden xl:table-cell">
|
|
304
304
|
App Version
|
|
305
305
|
</th>
|
|
306
|
-
<th className="text-left px-4 py-3 text-xs font-medium text-theme-text-secondary uppercase tracking-wide w-
|
|
306
|
+
<th className="text-left px-4 py-3 text-xs font-medium text-theme-text-secondary uppercase tracking-wide w-40">
|
|
307
307
|
Status
|
|
308
308
|
</th>
|
|
309
|
-
<th className="text-left px-4 py-3 text-xs font-medium text-theme-text-secondary uppercase tracking-wide w-
|
|
309
|
+
<th className="text-left px-4 py-3 text-xs font-medium text-theme-text-secondary uppercase tracking-wide w-16">
|
|
310
310
|
Rev
|
|
311
311
|
</th>
|
|
312
312
|
<th className="text-left px-4 py-3 text-xs font-medium text-theme-text-secondary uppercase tracking-wide w-24">
|
|
@@ -411,8 +411,74 @@ function getActionableTooltip(issue: string | undefined, summary: string | undef
|
|
|
411
411
|
)
|
|
412
412
|
}
|
|
413
413
|
|
|
414
|
+
function getListOperation(release: HelmRelease): HelmOperation | undefined {
|
|
415
|
+
if (release.lastOperation) {
|
|
416
|
+
const releaseStatus = release.status.toLowerCase()
|
|
417
|
+
if (release.lastOperation.status === 'failed' && releaseStatus === 'failed') {
|
|
418
|
+
return undefined
|
|
419
|
+
}
|
|
420
|
+
if (isListOperation(release.lastOperation)) {
|
|
421
|
+
return release.lastOperation
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return release.operations?.find(isListOperation)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function isListOperation(operation: HelmOperation): boolean {
|
|
428
|
+
return operation.kind === 'upgrade_rolled_back' || operation.kind === 'rollback' || operation.status === 'stuck_pending'
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function HelmOperationChip({ operation }: { operation: HelmOperation }) {
|
|
432
|
+
const isPending = operation.status === 'stuck_pending'
|
|
433
|
+
const isRollback = operation.kind === 'upgrade_rolled_back' || operation.kind === 'rollback'
|
|
434
|
+
const Icon = isRollback ? RotateCcw : Clock
|
|
435
|
+
const tone: keyof typeof SEVERITY_BADGE = operation.kind === 'rollback' ? 'info' : isPending ? 'warning' : 'alert'
|
|
436
|
+
const label = operation.kind === 'upgrade_rolled_back'
|
|
437
|
+
? 'rolled back'
|
|
438
|
+
: operation.kind === 'rollback'
|
|
439
|
+
? 'rollback'
|
|
440
|
+
: 'stuck'
|
|
441
|
+
|
|
442
|
+
return (
|
|
443
|
+
<Tooltip content={
|
|
444
|
+
<div className="max-w-xs">
|
|
445
|
+
<div className="font-medium text-theme-text-primary">{operationSummary(operation)}</div>
|
|
446
|
+
<div className="mt-1 text-[10px] text-theme-text-secondary">{operation.message}</div>
|
|
447
|
+
{operation.failureDescription && (
|
|
448
|
+
<div className="mt-1.5 border-t border-theme-border pt-1.5 text-[10px] text-theme-text-tertiary">
|
|
449
|
+
{operation.failureDescription}
|
|
450
|
+
</div>
|
|
451
|
+
)}
|
|
452
|
+
</div>
|
|
453
|
+
}>
|
|
454
|
+
<span className={clsx('badge-sm shrink-0', SEVERITY_BADGE[tone])} aria-label={operationSummary(operation)}>
|
|
455
|
+
<Icon className="h-3 w-3" />
|
|
456
|
+
<span className="sr-only">{label}</span>
|
|
457
|
+
<span className="hidden 2xl:inline" aria-hidden="true">{label}</span>
|
|
458
|
+
</span>
|
|
459
|
+
</Tooltip>
|
|
460
|
+
)
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function operationSummary(operation: HelmOperation): string {
|
|
464
|
+
switch (operation.kind) {
|
|
465
|
+
case 'upgrade_rolled_back':
|
|
466
|
+
return 'Helm rolled back after failed upgrade'
|
|
467
|
+
case 'rollback':
|
|
468
|
+
return 'Helm rollback applied'
|
|
469
|
+
case 'pending':
|
|
470
|
+
return 'Helm operation may be stuck'
|
|
471
|
+
case 'upgrade_failed':
|
|
472
|
+
return 'Helm upgrade failed'
|
|
473
|
+
default:
|
|
474
|
+
return 'Helm release failed'
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
414
478
|
const ReleaseRow = forwardRef<HTMLTableRowElement, ReleaseRowProps>(
|
|
415
479
|
function ReleaseRow({ release, upgradeInfo, isSelected, isHighlighted, onClick, onMouseEnter }, ref) {
|
|
480
|
+
const listOperation = getListOperation(release)
|
|
481
|
+
|
|
416
482
|
// Health badge styling
|
|
417
483
|
const getHealthBadge = () => {
|
|
418
484
|
if (!release.resourceHealth || release.resourceHealth === 'unknown') return null
|
|
@@ -454,10 +520,11 @@ const ReleaseRow = forwardRef<HTMLTableRowElement, ReleaseRowProps>(
|
|
|
454
520
|
)}
|
|
455
521
|
>
|
|
456
522
|
<td className="px-4 py-3">
|
|
457
|
-
<div className="flex items-center gap-2">
|
|
523
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
458
524
|
<Package className="w-4 h-4 text-theme-text-tertiary shrink-0" />
|
|
459
|
-
<span className="text-sm text-theme-text-primary
|
|
525
|
+
<span className="min-w-0 truncate text-sm font-medium text-theme-text-primary">{release.name}</span>
|
|
460
526
|
{getHealthBadge()}
|
|
527
|
+
{listOperation && <HelmOperationChip operation={listOperation} />}
|
|
461
528
|
{release.managedByFluxHelmRelease && (
|
|
462
529
|
<Tooltip content={`Installed by Flux helm-controller via HelmRelease ${release.managedByFluxHelmRelease}. Changes here will be reverted at the next reconcile — manage via the GitOps tab.`}>
|
|
463
530
|
<span className="badge-sm shrink-0 border border-theme-border bg-theme-elevated text-theme-text-secondary">
|
|
@@ -475,13 +542,15 @@ const ReleaseRow = forwardRef<HTMLTableRowElement, ReleaseRowProps>(
|
|
|
475
542
|
)}
|
|
476
543
|
</div>
|
|
477
544
|
</td>
|
|
478
|
-
<td className="px-4 py-3 w-
|
|
479
|
-
<
|
|
545
|
+
<td className="px-4 py-3 w-[18%]">
|
|
546
|
+
<Tooltip content={release.namespace}>
|
|
547
|
+
<span className="block truncate text-sm text-theme-text-secondary">{release.namespace}</span>
|
|
548
|
+
</Tooltip>
|
|
480
549
|
</td>
|
|
481
|
-
<td className="px-4 py-3 w-
|
|
550
|
+
<td className="px-4 py-3 w-[22%]">
|
|
482
551
|
<Tooltip content={`${release.chart}-${release.chartVersion}`}>
|
|
483
552
|
<span className="text-sm text-theme-text-secondary truncate block">
|
|
484
|
-
{
|
|
553
|
+
{release.chart}-{release.chartVersion}
|
|
485
554
|
</span>
|
|
486
555
|
</Tooltip>
|
|
487
556
|
</td>
|
|
@@ -494,7 +563,7 @@ const ReleaseRow = forwardRef<HTMLTableRowElement, ReleaseRowProps>(
|
|
|
494
563
|
</Tooltip>
|
|
495
564
|
)}
|
|
496
565
|
</td>
|
|
497
|
-
<td className="px-4 py-3 w-
|
|
566
|
+
<td className="px-4 py-3 w-40">
|
|
498
567
|
{isHelmReleaseActionable(release.status) ? (
|
|
499
568
|
<Tooltip content="Click row to view rollback / history / logs and recover">
|
|
500
569
|
<span
|
|
@@ -515,7 +584,7 @@ const ReleaseRow = forwardRef<HTMLTableRowElement, ReleaseRowProps>(
|
|
|
515
584
|
</span>
|
|
516
585
|
)}
|
|
517
586
|
</td>
|
|
518
|
-
<td className="px-4 py-3 w-
|
|
587
|
+
<td className="px-4 py-3 w-16">
|
|
519
588
|
<span className="text-sm text-theme-text-secondary">{release.revision}</span>
|
|
520
589
|
</td>
|
|
521
590
|
<td className="px-4 py-3 w-24">
|
|
@@ -191,7 +191,7 @@ export function InstallWizard({ repo, chartName, version, source, repoUrl, defau
|
|
|
191
191
|
} finally {
|
|
192
192
|
setIsInstalling(false)
|
|
193
193
|
}
|
|
194
|
-
}, [releaseName, namespace, chartName, version, repo, valuesYaml, createNamespace, onSuccess, isLocal, artifactHubDetail, queryClient])
|
|
194
|
+
}, [releaseName, namespace, chartName, version, repo, repoUrl, valuesYaml, createNamespace, onSuccess, isLocal, artifactHubDetail, queryClient])
|
|
195
195
|
|
|
196
196
|
// Validate release name + namespace before letting the user
|
|
197
197
|
// advance. Without this, a name like "Invalid Name With Spaces!"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useState } from 'react'
|
|
2
2
|
import { History, Eye, GitCompare, Check, RotateCcw } from 'lucide-react'
|
|
3
3
|
import { clsx } from 'clsx'
|
|
4
|
-
import type { HelmRevision } from '../../types'
|
|
4
|
+
import type { HelmOperation, HelmRevision } from '../../types'
|
|
5
5
|
import { getStatusColor, formatDate, formatAge } from './helm-utils'
|
|
6
6
|
import { SEVERITY_BADGE } from '../../utils/badge-colors'
|
|
7
7
|
import { Tooltip } from '../ui/Tooltip'
|
|
@@ -9,12 +9,13 @@ import { Tooltip } from '../ui/Tooltip'
|
|
|
9
9
|
interface RevisionHistoryProps {
|
|
10
10
|
history: HelmRevision[]
|
|
11
11
|
currentRevision: number
|
|
12
|
+
operations?: HelmOperation[]
|
|
12
13
|
onViewRevision: (revision: number) => void
|
|
13
14
|
onCompare: (rev1: number, rev2: number) => void
|
|
14
15
|
onRollback?: (revision: number) => void
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
export function RevisionHistory({ history, currentRevision, onViewRevision, onCompare, onRollback }: RevisionHistoryProps) {
|
|
18
|
+
export function RevisionHistory({ history, currentRevision, operations = [], onViewRevision, onCompare, onRollback }: RevisionHistoryProps) {
|
|
18
19
|
const [selectedForCompare, setSelectedForCompare] = useState<number | null>(null)
|
|
19
20
|
|
|
20
21
|
const handleCompareClick = (revision: number) => {
|
|
@@ -63,6 +64,7 @@ export function RevisionHistory({ history, currentRevision, onViewRevision, onCo
|
|
|
63
64
|
{history.map((revision, index) => {
|
|
64
65
|
const isCurrent = revision.revision === currentRevision
|
|
65
66
|
const isSelectedForCompare = selectedForCompare === revision.revision
|
|
67
|
+
const annotations = operationAnnotationsForRevision(operations, revision.revision)
|
|
66
68
|
|
|
67
69
|
return (
|
|
68
70
|
<div
|
|
@@ -106,6 +108,11 @@ export function RevisionHistory({ history, currentRevision, onViewRevision, onCo
|
|
|
106
108
|
Current
|
|
107
109
|
</span>
|
|
108
110
|
)}
|
|
111
|
+
{annotations.map((annotation) => (
|
|
112
|
+
<span key={annotation.label} className={clsx('badge-sm', annotation.className)}>
|
|
113
|
+
{annotation.label}
|
|
114
|
+
</span>
|
|
115
|
+
))}
|
|
109
116
|
</div>
|
|
110
117
|
|
|
111
118
|
<div className="flex items-center gap-4 mt-1 text-xs text-theme-text-tertiary">
|
|
@@ -166,3 +173,40 @@ export function RevisionHistory({ history, currentRevision, onViewRevision, onCo
|
|
|
166
173
|
</div>
|
|
167
174
|
)
|
|
168
175
|
}
|
|
176
|
+
|
|
177
|
+
function operationAnnotationsForRevision(operations: HelmOperation[], revision: number): Array<{ label: string; className: string }> {
|
|
178
|
+
const annotations: Array<{ label: string; className: string }> = []
|
|
179
|
+
const seen = new Set<string>()
|
|
180
|
+
const add = (label: string, className: string) => {
|
|
181
|
+
if (seen.has(label)) return
|
|
182
|
+
seen.add(label)
|
|
183
|
+
annotations.push({ label, className })
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
for (const op of operations) {
|
|
187
|
+
if (op.failedRevision === revision) {
|
|
188
|
+
add('Failed upgrade', SEVERITY_BADGE.error)
|
|
189
|
+
}
|
|
190
|
+
if (op.rollbackRevision === revision) {
|
|
191
|
+
add('Rollback revision', SEVERITY_BADGE.warning)
|
|
192
|
+
}
|
|
193
|
+
if (op.revision === revision) {
|
|
194
|
+
switch (op.kind) {
|
|
195
|
+
case 'upgrade_failed':
|
|
196
|
+
add('Failed upgrade', SEVERITY_BADGE.error)
|
|
197
|
+
break
|
|
198
|
+
case 'release_failed':
|
|
199
|
+
add('Failed', SEVERITY_BADGE.error)
|
|
200
|
+
break
|
|
201
|
+
case 'rollback':
|
|
202
|
+
add('Rollback', SEVERITY_BADGE.warning)
|
|
203
|
+
break
|
|
204
|
+
case 'pending':
|
|
205
|
+
add('Pending', SEVERITY_BADGE.warning)
|
|
206
|
+
break
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return annotations
|
|
212
|
+
}
|