@skyhook-io/radar-app 1.8.3 → 1.8.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -5
- package/src/App.tsx +256 -94
- package/src/RadarApp.tsx +4 -1
- package/src/api/client.metrics.test.ts +106 -0
- package/src/api/client.ts +147 -20
- package/src/components/ConnectionErrorView.tsx +1 -1
- package/src/components/ContextSwitcher.tsx +5 -1
- package/src/components/NamespaceSwitcher.tsx +21 -300
- package/src/components/applications/ApplicationsView.tsx +22 -7
- package/src/components/audit/AuditView.tsx +11 -2
- package/src/components/cost/CostView.tsx +12 -2
- package/src/components/gitops/GitOpsView.tsx +22 -7
- package/src/components/helm/HelmCompareRoute.tsx +1342 -0
- package/src/components/helm/HelmReleaseDrawer.tsx +189 -352
- package/src/components/helm/HelmView.tsx +79 -62
- package/src/components/helm/ManifestDiffViewer.tsx +4 -4
- package/src/components/home/ClusterHealthCard.tsx +6 -1
- package/src/components/home/HomeView.tsx +20 -7
- package/src/components/home/mcpToolCatalog.ts +1 -1
- package/src/components/issues/IssuesPane.tsx +29 -18
- package/src/components/resources/ResourceDetailDrawer.tsx +8 -3
- package/src/components/resources/ResourcesView.tsx +3 -0
- package/src/components/resources/renderers/NodeRenderer.tsx +10 -4
- package/src/components/resources/renderers/PodRenderer.tsx +10 -4
- package/src/components/timeline/TimelineView.tsx +26 -2
- package/src/components/traffic/TrafficView.tsx +17 -10
- package/src/components/ui/Markdown.tsx +2 -2
- package/src/components/ui/Omnibar.tsx +1 -1
- package/src/components/workload/WorkloadView.tsx +5 -1
- package/src/filter/FilterLocationBridge.tsx +30 -0
- package/src/hooks/useKeyboardShortcuts.tsx +1 -0
- package/src/index.ts +15 -0
|
@@ -4,25 +4,24 @@ import { FetchResult, useDockReservedHeight, compareVersions } from '@skyhook-io
|
|
|
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,
|
|
7
|
+
import { X, Copy, Check, RefreshCw, Package, Code, History, Settings, Link2, Anchor, GitFork, BookOpen, ArrowUpCircle, Trash2, GitBranch, AlertTriangle, RotateCcw, Clock, GitCompare, ExternalLink } from 'lucide-react'
|
|
8
8
|
import { useNavigate } from 'react-router-dom'
|
|
9
9
|
import { clsx } from 'clsx'
|
|
10
|
-
import { useHelmRelease, useHelmManifest, useHelmValues,
|
|
10
|
+
import { useHelmRelease, useHelmManifest, useHelmValues, 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, HelmOperation,
|
|
16
|
-
import type
|
|
15
|
+
import type { SelectedHelmRelease, HelmHook, ChartDependency, HelmOperation, HelmOperationInsight, HelmOwnedResource, HookDiagnostic, HookLogEvidence } from '../../types'
|
|
16
|
+
import { apiVersionToGroup, kindToPlural, type NavigateToResource } from '../../utils/navigation'
|
|
17
17
|
import { formatDate } from './helm-utils'
|
|
18
|
-
import { getHelmStatusColor, SEVERITY_BADGE, SEVERITY_TEXT } from '../../utils/badge-colors'
|
|
18
|
+
import { getHelmStatusColor, getKindBadgeColor, getResourceStatusColor, SEVERITY_BADGE, SEVERITY_TEXT } from '../../utils/badge-colors'
|
|
19
19
|
import { useCanHelmAct, useCloudRole } from '../../api/client'
|
|
20
20
|
import { RoleGatedPanel } from './RoleGatedPanel'
|
|
21
21
|
import { RevisionHistory } from './RevisionHistory'
|
|
22
22
|
import { ManifestViewer } from './ManifestViewer'
|
|
23
23
|
import { ValuesViewer } from './ValuesViewer'
|
|
24
24
|
import { OwnedResources } from './OwnedResources'
|
|
25
|
-
import { ManifestDiffViewer } from './ManifestDiffViewer'
|
|
26
25
|
import { TrackChartSourceDialog } from './TrackChartSourceDialog'
|
|
27
26
|
|
|
28
27
|
interface HelmReleaseDrawerProps {
|
|
@@ -33,8 +32,7 @@ interface HelmReleaseDrawerProps {
|
|
|
33
32
|
isOpen?: boolean
|
|
34
33
|
}
|
|
35
34
|
|
|
36
|
-
type TabId = 'overview' | 'history' | 'manifest' | 'values' | 'resources' | 'hooks'
|
|
37
|
-
type CompareMode = 'summary' | 'values' | 'manifest' | 'notes' | 'resources'
|
|
35
|
+
type TabId = 'overview' | 'history' | 'manifest' | 'values' | 'resources' | 'hooks'
|
|
38
36
|
|
|
39
37
|
const MIN_WIDTH = 500
|
|
40
38
|
const MAX_WIDTH_PERCENT = 0.8
|
|
@@ -49,8 +47,6 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
49
47
|
const [isResizing, setIsResizing] = useState(false)
|
|
50
48
|
const [selectedRevision, setSelectedRevision] = useState<number | undefined>(undefined)
|
|
51
49
|
const [showAllValues, setShowAllValues] = useState(false)
|
|
52
|
-
const [diffRevisions, setDiffRevisions] = useState<{ rev1: number; rev2: number } | null>(null)
|
|
53
|
-
const [compareMode, setCompareMode] = useState<CompareMode>('summary')
|
|
54
50
|
const [rollbackRevision, setRollbackRevision] = useState<number | null>(null)
|
|
55
51
|
const [showUninstallConfirm, setShowUninstallConfirm] = useState(false)
|
|
56
52
|
const [showUpgradeConfirm, setShowUpgradeConfirm] = useState(false)
|
|
@@ -90,37 +86,6 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
90
86
|
selectedRevision,
|
|
91
87
|
)
|
|
92
88
|
|
|
93
|
-
// Fetch diff if comparing revisions
|
|
94
|
-
const { data: diffData, isLoading: diffLoading } = useHelmManifestDiff(
|
|
95
|
-
helmNamespace,
|
|
96
|
-
release.name,
|
|
97
|
-
diffRevisions?.rev1 || 0,
|
|
98
|
-
diffRevisions?.rev2 || 0,
|
|
99
|
-
canViewSensitive && compareMode === 'manifest',
|
|
100
|
-
)
|
|
101
|
-
const { data: valuesDiffData, isLoading: valuesDiffLoading } = useHelmValuesDiff(
|
|
102
|
-
helmNamespace,
|
|
103
|
-
release.name,
|
|
104
|
-
diffRevisions?.rev1 || 0,
|
|
105
|
-
diffRevisions?.rev2 || 0,
|
|
106
|
-
false,
|
|
107
|
-
canViewSensitive && compareMode === 'values',
|
|
108
|
-
)
|
|
109
|
-
const { data: notesDiffData, isLoading: notesDiffLoading } = useHelmNotesDiff(
|
|
110
|
-
helmNamespace,
|
|
111
|
-
release.name,
|
|
112
|
-
diffRevisions?.rev1 || 0,
|
|
113
|
-
diffRevisions?.rev2 || 0,
|
|
114
|
-
canViewSensitive && compareMode === 'notes',
|
|
115
|
-
)
|
|
116
|
-
const { data: resourceDiffData, isLoading: resourceDiffLoading } = useHelmResourceDiff(
|
|
117
|
-
helmNamespace,
|
|
118
|
-
release.name,
|
|
119
|
-
diffRevisions?.rev1 || 0,
|
|
120
|
-
diffRevisions?.rev2 || 0,
|
|
121
|
-
canViewSensitive && compareMode === 'resources',
|
|
122
|
-
)
|
|
123
|
-
|
|
124
89
|
// Lazy check for upgrade availability
|
|
125
90
|
const { data: upgradeInfo, isLoading: upgradeLoading, error: upgradeError } = useHelmUpgradeInfo(
|
|
126
91
|
helmNamespace,
|
|
@@ -213,11 +178,16 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
213
178
|
startViewTransitionSafe(() => flushSync(() => setActiveTab(tab)))
|
|
214
179
|
}, [])
|
|
215
180
|
|
|
216
|
-
const handleCompareRevisions = (rev1: number, rev2: number) => {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
181
|
+
const handleCompareRevisions = useCallback((rev1: number, rev2: number) => {
|
|
182
|
+
const params = new URLSearchParams()
|
|
183
|
+
const globalNamespaces = new URLSearchParams(window.location.search).get('namespaces')
|
|
184
|
+
if (globalNamespaces) params.set('namespaces', globalNamespaces)
|
|
185
|
+
params.set('release', `${release.namespace}/${release.name}`)
|
|
186
|
+
if (release.storageNamespace) params.set('releaseStorage', release.storageNamespace)
|
|
187
|
+
params.set('revision1', String(rev1))
|
|
188
|
+
params.set('revision2', String(rev2))
|
|
189
|
+
navigate({ pathname: '/helm/compare', search: params.toString() })
|
|
190
|
+
}, [navigate, release.name, release.namespace, release.storageNamespace])
|
|
221
191
|
|
|
222
192
|
const handleViewRevision = (revision: number) => {
|
|
223
193
|
setSelectedRevision(revision)
|
|
@@ -348,11 +318,6 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
348
318
|
{ id: 'hooks', label: 'Hooks', icon: Anchor },
|
|
349
319
|
]
|
|
350
320
|
|
|
351
|
-
// Add diff tab only when comparing
|
|
352
|
-
if (diffRevisions) {
|
|
353
|
-
tabs.push({ id: 'diff', label: 'Compare', icon: GitCompare })
|
|
354
|
-
}
|
|
355
|
-
|
|
356
321
|
return (
|
|
357
322
|
<div
|
|
358
323
|
className={clsx(
|
|
@@ -555,8 +520,13 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
555
520
|
<>
|
|
556
521
|
<HelmOperationBanner
|
|
557
522
|
operation={releaseDetail.lastOperation}
|
|
523
|
+
operationInsight={releaseDetail.operationInsight}
|
|
524
|
+
releaseNamespace={releaseDetail.namespace}
|
|
558
525
|
managedByFluxHelmRelease={releaseDetail.managedByFluxHelmRelease}
|
|
559
526
|
hookDiagnostics={releaseDetail.hookDiagnostics}
|
|
527
|
+
canCompare={canViewSensitive}
|
|
528
|
+
onCompare={handleCompareRevisions}
|
|
529
|
+
onNavigateToResource={onNavigateToResource}
|
|
560
530
|
/>
|
|
561
531
|
{activeTab === 'overview' && (
|
|
562
532
|
<OverviewTab release={releaseDetail} onCopy={copyToClipboard} copied={copied} />
|
|
@@ -608,30 +578,6 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
608
578
|
{activeTab === 'hooks' && (
|
|
609
579
|
<HooksTab hooks={releaseDetail.hooks || []} hookDiagnostics={releaseDetail.hookDiagnostics || []} />
|
|
610
580
|
)}
|
|
611
|
-
{activeTab === 'diff' && diffRevisions && (
|
|
612
|
-
<RoleGatedPanel min="member" feature="release revision comparison">
|
|
613
|
-
<HelmRevisionCompareView
|
|
614
|
-
revisions={releaseDetail.history}
|
|
615
|
-
revision1={diffRevisions.rev1}
|
|
616
|
-
revision2={diffRevisions.rev2}
|
|
617
|
-
mode={compareMode}
|
|
618
|
-
onModeChange={setCompareMode}
|
|
619
|
-
manifestDiff={diffData?.diff || ''}
|
|
620
|
-
manifestLoading={diffLoading}
|
|
621
|
-
valuesDiff={valuesDiffData?.diff || ''}
|
|
622
|
-
valuesLoading={valuesDiffLoading}
|
|
623
|
-
notesDiff={notesDiffData?.diff || ''}
|
|
624
|
-
notesLoading={notesDiffLoading}
|
|
625
|
-
resourceDiff={resourceDiffData}
|
|
626
|
-
resourceLoading={resourceDiffLoading}
|
|
627
|
-
onClose={() => {
|
|
628
|
-
setDiffRevisions(null)
|
|
629
|
-
setCompareMode('summary')
|
|
630
|
-
setActiveTab('history')
|
|
631
|
-
}}
|
|
632
|
-
/>
|
|
633
|
-
</RoleGatedPanel>
|
|
634
|
-
)}
|
|
635
581
|
</>
|
|
636
582
|
)}
|
|
637
583
|
</div>
|
|
@@ -783,278 +729,6 @@ function mergeHelmOperations(operations: HelmOperation[] | undefined, lastOperat
|
|
|
783
729
|
return merged
|
|
784
730
|
}
|
|
785
731
|
|
|
786
|
-
interface HelmRevisionCompareViewProps {
|
|
787
|
-
revisions: HelmRevision[]
|
|
788
|
-
revision1: number
|
|
789
|
-
revision2: number
|
|
790
|
-
mode: CompareMode
|
|
791
|
-
onModeChange: (mode: CompareMode) => void
|
|
792
|
-
manifestDiff: string
|
|
793
|
-
manifestLoading: boolean
|
|
794
|
-
valuesDiff: string
|
|
795
|
-
valuesLoading: boolean
|
|
796
|
-
notesDiff: string
|
|
797
|
-
notesLoading: boolean
|
|
798
|
-
resourceDiff?: ResourceDiff
|
|
799
|
-
resourceLoading: boolean
|
|
800
|
-
onClose: () => void
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
function HelmRevisionCompareView({
|
|
804
|
-
revisions,
|
|
805
|
-
revision1,
|
|
806
|
-
revision2,
|
|
807
|
-
mode,
|
|
808
|
-
onModeChange,
|
|
809
|
-
manifestDiff,
|
|
810
|
-
manifestLoading,
|
|
811
|
-
valuesDiff,
|
|
812
|
-
valuesLoading,
|
|
813
|
-
notesDiff,
|
|
814
|
-
notesLoading,
|
|
815
|
-
resourceDiff,
|
|
816
|
-
resourceLoading,
|
|
817
|
-
onClose,
|
|
818
|
-
}: HelmRevisionCompareViewProps) {
|
|
819
|
-
const left = revisions.find((r) => r.revision === revision1)
|
|
820
|
-
const right = revisions.find((r) => r.revision === revision2)
|
|
821
|
-
const modes: Array<{ id: CompareMode; label: string; icon: typeof GitCompare }> = [
|
|
822
|
-
{ id: 'summary', label: 'Summary', icon: GitCompare },
|
|
823
|
-
{ id: 'values', label: 'Values', icon: Settings },
|
|
824
|
-
{ id: 'manifest', label: 'Manifest', icon: Code },
|
|
825
|
-
{ id: 'notes', label: 'Notes', icon: FileText },
|
|
826
|
-
{ id: 'resources', label: 'Resources', icon: Link2 },
|
|
827
|
-
]
|
|
828
|
-
|
|
829
|
-
return (
|
|
830
|
-
<div className="p-4">
|
|
831
|
-
<div className="mb-3 flex items-center justify-between gap-3">
|
|
832
|
-
<div className="min-w-0">
|
|
833
|
-
<div className="flex items-center gap-2">
|
|
834
|
-
<GitCompare className="h-4 w-4 text-theme-text-secondary" />
|
|
835
|
-
<span className="text-sm font-medium text-theme-text-primary">Revision {revision1} -> {revision2}</span>
|
|
836
|
-
</div>
|
|
837
|
-
<p className="mt-1 text-xs text-theme-text-tertiary">
|
|
838
|
-
Compare rendered output and release metadata between two Helm revisions.
|
|
839
|
-
</p>
|
|
840
|
-
</div>
|
|
841
|
-
<button
|
|
842
|
-
onClick={onClose}
|
|
843
|
-
className="flex items-center gap-1 px-2 py-1 text-xs text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded"
|
|
844
|
-
>
|
|
845
|
-
<X className="w-3.5 h-3.5" />
|
|
846
|
-
Close
|
|
847
|
-
</button>
|
|
848
|
-
</div>
|
|
849
|
-
|
|
850
|
-
<div className="mb-4 flex flex-wrap gap-1 rounded-lg bg-theme-base/50 p-1">
|
|
851
|
-
{modes.map((item) => (
|
|
852
|
-
<button
|
|
853
|
-
key={item.id}
|
|
854
|
-
onClick={() => onModeChange(item.id)}
|
|
855
|
-
className={clsx(
|
|
856
|
-
'flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-xs transition-colors',
|
|
857
|
-
mode === item.id
|
|
858
|
-
? 'bg-theme-elevated text-theme-text-primary shadow-theme-sm'
|
|
859
|
-
: 'text-theme-text-secondary hover:bg-theme-hover/70 hover:text-theme-text-primary'
|
|
860
|
-
)}
|
|
861
|
-
>
|
|
862
|
-
<item.icon className="h-3.5 w-3.5" />
|
|
863
|
-
{item.label}
|
|
864
|
-
</button>
|
|
865
|
-
))}
|
|
866
|
-
</div>
|
|
867
|
-
|
|
868
|
-
{mode === 'summary' && <RevisionCompareSummary left={left} right={right} revision1={revision1} revision2={revision2} />}
|
|
869
|
-
{mode === 'values' && (
|
|
870
|
-
<ManifestDiffViewer
|
|
871
|
-
diff={valuesDiff}
|
|
872
|
-
isLoading={valuesLoading}
|
|
873
|
-
revision1={revision1}
|
|
874
|
-
revision2={revision2}
|
|
875
|
-
title={`Values diff: Revision ${revision1} -> ${revision2}`}
|
|
876
|
-
emptyLabel="No user-supplied value changes found"
|
|
877
|
-
onClose={onClose}
|
|
878
|
-
/>
|
|
879
|
-
)}
|
|
880
|
-
{mode === 'manifest' && (
|
|
881
|
-
<ManifestDiffViewer
|
|
882
|
-
diff={manifestDiff}
|
|
883
|
-
isLoading={manifestLoading}
|
|
884
|
-
revision1={revision1}
|
|
885
|
-
revision2={revision2}
|
|
886
|
-
title={`Manifest diff: Revision ${revision1} -> ${revision2}`}
|
|
887
|
-
onClose={onClose}
|
|
888
|
-
/>
|
|
889
|
-
)}
|
|
890
|
-
{mode === 'notes' && (
|
|
891
|
-
<ManifestDiffViewer
|
|
892
|
-
diff={notesDiff}
|
|
893
|
-
isLoading={notesLoading}
|
|
894
|
-
revision1={revision1}
|
|
895
|
-
revision2={revision2}
|
|
896
|
-
title={`Notes diff: Revision ${revision1} -> ${revision2}`}
|
|
897
|
-
emptyLabel="No release notes changes found"
|
|
898
|
-
onClose={onClose}
|
|
899
|
-
/>
|
|
900
|
-
)}
|
|
901
|
-
{mode === 'resources' && (
|
|
902
|
-
<ResourceDiffView diff={resourceDiff} isLoading={resourceLoading} revision1={revision1} revision2={revision2} />
|
|
903
|
-
)}
|
|
904
|
-
</div>
|
|
905
|
-
)
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
function RevisionCompareSummary({
|
|
909
|
-
left,
|
|
910
|
-
right,
|
|
911
|
-
revision1,
|
|
912
|
-
revision2,
|
|
913
|
-
}: {
|
|
914
|
-
left?: HelmRevision
|
|
915
|
-
right?: HelmRevision
|
|
916
|
-
revision1: number
|
|
917
|
-
revision2: number
|
|
918
|
-
}) {
|
|
919
|
-
return (
|
|
920
|
-
<div className="space-y-4">
|
|
921
|
-
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
|
|
922
|
-
<RevisionSummaryCard revision={left} fallbackRevision={revision1} label="From" />
|
|
923
|
-
<RevisionSummaryCard revision={right} fallbackRevision={revision2} label="To" />
|
|
924
|
-
</div>
|
|
925
|
-
<div className="card-inner-lg">
|
|
926
|
-
<h3 className="text-sm font-medium text-theme-text-secondary">Changed fields</h3>
|
|
927
|
-
<div className="mt-3 grid grid-cols-1 gap-2 text-sm md:grid-cols-2">
|
|
928
|
-
<RevisionFieldDelta label="Chart" left={left?.chart} right={right?.chart} />
|
|
929
|
-
<RevisionFieldDelta label="App version" left={left?.appVersion || '-'} right={right?.appVersion || '-'} />
|
|
930
|
-
<RevisionFieldDelta label="Status" left={left?.status} right={right?.status} />
|
|
931
|
-
<RevisionFieldDelta label="Description" left={left?.description || '-'} right={right?.description || '-'} />
|
|
932
|
-
</div>
|
|
933
|
-
</div>
|
|
934
|
-
</div>
|
|
935
|
-
)
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
function RevisionSummaryCard({ revision, fallbackRevision, label }: { revision?: HelmRevision; fallbackRevision: number; label: string }) {
|
|
939
|
-
return (
|
|
940
|
-
<div className="card-inner-lg">
|
|
941
|
-
<div className="flex items-center justify-between gap-2">
|
|
942
|
-
<span className="text-xs font-medium uppercase tracking-wide text-theme-text-tertiary">{label}</span>
|
|
943
|
-
<span className="badge-sm bg-theme-hover/50 text-theme-text-secondary">rev {revision?.revision || fallbackRevision}</span>
|
|
944
|
-
</div>
|
|
945
|
-
<div className="mt-3 space-y-2 text-sm">
|
|
946
|
-
<div className="flex items-center justify-between gap-3">
|
|
947
|
-
<span className="text-theme-text-tertiary">Chart</span>
|
|
948
|
-
<span className="truncate text-theme-text-primary">{revision?.chart || '-'}</span>
|
|
949
|
-
</div>
|
|
950
|
-
<div className="flex items-center justify-between gap-3">
|
|
951
|
-
<span className="text-theme-text-tertiary">Status</span>
|
|
952
|
-
<span className={clsx('badge-sm', revision ? getHelmStatusColor(revision.status) : SEVERITY_BADGE.neutral)}>
|
|
953
|
-
{revision?.status || 'unknown'}
|
|
954
|
-
</span>
|
|
955
|
-
</div>
|
|
956
|
-
<div className="flex items-center justify-between gap-3">
|
|
957
|
-
<span className="text-theme-text-tertiary">Updated</span>
|
|
958
|
-
<span className="text-theme-text-secondary">{revision?.updated ? formatDate(revision.updated) : '-'}</span>
|
|
959
|
-
</div>
|
|
960
|
-
</div>
|
|
961
|
-
</div>
|
|
962
|
-
)
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
function RevisionFieldDelta({ label, left, right }: { label: string; left?: string; right?: string }) {
|
|
966
|
-
const changed = (left || '') !== (right || '')
|
|
967
|
-
return (
|
|
968
|
-
<div className="rounded-md bg-theme-base/40 p-2">
|
|
969
|
-
<div className="mb-1 flex items-center justify-between gap-2">
|
|
970
|
-
<span className="text-xs text-theme-text-tertiary">{label}</span>
|
|
971
|
-
<span className={clsx('badge-sm', changed ? SEVERITY_BADGE.info : SEVERITY_BADGE.neutral)}>
|
|
972
|
-
{changed ? 'changed' : 'same'}
|
|
973
|
-
</span>
|
|
974
|
-
</div>
|
|
975
|
-
<div className="grid grid-cols-[1fr_auto_1fr] items-center gap-2 text-xs">
|
|
976
|
-
<span className="truncate text-theme-text-secondary">{left || '-'}</span>
|
|
977
|
-
<span className="text-theme-text-tertiary">-></span>
|
|
978
|
-
<span className="truncate text-theme-text-primary">{right || '-'}</span>
|
|
979
|
-
</div>
|
|
980
|
-
</div>
|
|
981
|
-
)
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
function ResourceDiffView({
|
|
985
|
-
diff,
|
|
986
|
-
isLoading,
|
|
987
|
-
revision1,
|
|
988
|
-
revision2,
|
|
989
|
-
}: {
|
|
990
|
-
diff?: ResourceDiff
|
|
991
|
-
isLoading: boolean
|
|
992
|
-
revision1: number
|
|
993
|
-
revision2: number
|
|
994
|
-
}) {
|
|
995
|
-
if (isLoading) {
|
|
996
|
-
return <FetchResult loading className="h-32" />
|
|
997
|
-
}
|
|
998
|
-
if (!diff) {
|
|
999
|
-
return <FetchResult loading={false} notFoundMessage="Resource diff not available" className="h-32" />
|
|
1000
|
-
}
|
|
1001
|
-
return (
|
|
1002
|
-
<div className="space-y-3">
|
|
1003
|
-
<div className="text-sm font-medium text-theme-text-secondary">
|
|
1004
|
-
Resource set diff: Revision {revision1} -> {revision2}
|
|
1005
|
-
</div>
|
|
1006
|
-
<ResourceDiffGroup title="Added" icon={Plus} tone="success" resources={diff.added} />
|
|
1007
|
-
<ResourceDiffGroup title="Removed" icon={Minus} tone="error" resources={diff.removed} />
|
|
1008
|
-
<ResourceDiffGroup title="Unchanged" icon={Equal} tone="neutral" resources={diff.unchanged} collapsed />
|
|
1009
|
-
</div>
|
|
1010
|
-
)
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
function ResourceDiffGroup({
|
|
1014
|
-
title,
|
|
1015
|
-
icon: Icon,
|
|
1016
|
-
tone,
|
|
1017
|
-
resources,
|
|
1018
|
-
collapsed = false,
|
|
1019
|
-
}: {
|
|
1020
|
-
title: string
|
|
1021
|
-
icon: typeof Plus
|
|
1022
|
-
tone: keyof typeof SEVERITY_BADGE
|
|
1023
|
-
resources: ResourceDiff['added']
|
|
1024
|
-
collapsed?: boolean
|
|
1025
|
-
}) {
|
|
1026
|
-
const items = resources || []
|
|
1027
|
-
const visible = collapsed ? items.slice(0, 12) : items
|
|
1028
|
-
return (
|
|
1029
|
-
<div className="card-inner-lg">
|
|
1030
|
-
<div className="mb-2 flex items-center justify-between gap-2">
|
|
1031
|
-
<div className="flex items-center gap-2">
|
|
1032
|
-
<Icon className="h-4 w-4 text-theme-text-secondary" />
|
|
1033
|
-
<span className="text-sm font-medium text-theme-text-primary">{title}</span>
|
|
1034
|
-
</div>
|
|
1035
|
-
<span className={clsx('badge-sm', SEVERITY_BADGE[tone])}>{items.length}</span>
|
|
1036
|
-
</div>
|
|
1037
|
-
{items.length === 0 ? (
|
|
1038
|
-
<div className="text-sm text-theme-text-tertiary">None</div>
|
|
1039
|
-
) : (
|
|
1040
|
-
<div className="space-y-1">
|
|
1041
|
-
{visible.map((resource) => (
|
|
1042
|
-
<div key={`${resource.apiVersion}/${resource.kind}/${resource.namespace}/${resource.name}`} className="grid grid-cols-[110px_1fr] gap-2 rounded bg-theme-base/40 px-2 py-1 text-xs">
|
|
1043
|
-
<span className="text-theme-text-tertiary">{resource.kind}</span>
|
|
1044
|
-
<span className="truncate text-theme-text-primary">
|
|
1045
|
-
{resource.namespace ? `${resource.namespace}/` : ''}{resource.name}
|
|
1046
|
-
</span>
|
|
1047
|
-
</div>
|
|
1048
|
-
))}
|
|
1049
|
-
{visible.length < items.length && (
|
|
1050
|
-
<div className="text-xs text-theme-text-tertiary">+{items.length - visible.length} more unchanged resources</div>
|
|
1051
|
-
)}
|
|
1052
|
-
</div>
|
|
1053
|
-
)}
|
|
1054
|
-
</div>
|
|
1055
|
-
)
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
732
|
function helmOperationKey(operation: HelmOperation): string {
|
|
1059
733
|
return [
|
|
1060
734
|
operation.kind,
|
|
@@ -1068,12 +742,22 @@ function helmOperationKey(operation: HelmOperation): string {
|
|
|
1068
742
|
|
|
1069
743
|
function HelmOperationBanner({
|
|
1070
744
|
operation,
|
|
745
|
+
operationInsight,
|
|
746
|
+
releaseNamespace,
|
|
1071
747
|
managedByFluxHelmRelease,
|
|
1072
748
|
hookDiagnostics,
|
|
749
|
+
canCompare,
|
|
750
|
+
onCompare,
|
|
751
|
+
onNavigateToResource,
|
|
1073
752
|
}: {
|
|
1074
753
|
operation?: HelmOperation
|
|
754
|
+
operationInsight?: HelmOperationInsight
|
|
755
|
+
releaseNamespace: string
|
|
1075
756
|
managedByFluxHelmRelease?: string
|
|
1076
757
|
hookDiagnostics?: HookDiagnostic[]
|
|
758
|
+
canCompare?: boolean
|
|
759
|
+
onCompare?: (rev1: number, rev2: number) => void
|
|
760
|
+
onNavigateToResource?: NavigateToResource
|
|
1077
761
|
}) {
|
|
1078
762
|
const primaryHookDiagnostic = hookDiagnostics?.[0]
|
|
1079
763
|
if ((!operation || !shouldShowOperationBanner(operation)) && !primaryHookDiagnostic) {
|
|
@@ -1104,6 +788,8 @@ function HelmOperationBanner({
|
|
|
1104
788
|
const title = operationTitle(operation)
|
|
1105
789
|
const statusLabel = operation.status.replace(/_/g, ' ')
|
|
1106
790
|
const showStatusBadge = !(operation.status === 'failed' && title.toLowerCase().includes('failed'))
|
|
791
|
+
const rawMessage = operation.rawMessage?.trim()
|
|
792
|
+
const hasRawMessage = !!rawMessage && rawMessage !== operation.message.trim()
|
|
1107
793
|
|
|
1108
794
|
return (
|
|
1109
795
|
<div className="m-4 mb-0 card-inner-lg">
|
|
@@ -1118,16 +804,35 @@ function HelmOperationBanner({
|
|
|
1118
804
|
<OperationRevisionChips operation={operation} />
|
|
1119
805
|
</div>
|
|
1120
806
|
<p className="mt-1 text-sm text-theme-text-secondary">{operation.message}</p>
|
|
1121
|
-
{
|
|
1122
|
-
<
|
|
1123
|
-
|
|
1124
|
-
|
|
807
|
+
{hasRawMessage && (
|
|
808
|
+
<details className="mt-2">
|
|
809
|
+
<summary className="cursor-pointer select-none text-xs font-medium text-theme-text-tertiary hover:text-theme-text-secondary">
|
|
810
|
+
Show raw Helm error
|
|
811
|
+
</summary>
|
|
812
|
+
<pre className="mt-2 max-h-32 overflow-auto whitespace-pre-wrap break-words rounded bg-theme-base/60 p-2 font-mono text-[11px] leading-relaxed text-theme-text-tertiary">
|
|
813
|
+
{rawMessage}
|
|
814
|
+
</pre>
|
|
815
|
+
</details>
|
|
816
|
+
)}
|
|
817
|
+
{operation.failureDescription && !hasRawMessage && (
|
|
818
|
+
<Tooltip content={operation.failureDescription} wrapperClassName="mt-1 flex">
|
|
819
|
+
<span className="line-clamp-2 text-xs text-theme-text-tertiary">
|
|
820
|
+
{operation.failureDescription}
|
|
821
|
+
</span>
|
|
822
|
+
</Tooltip>
|
|
1125
823
|
)}
|
|
1126
824
|
{operation.kind === 'upgrade_rolled_back' && (
|
|
1127
825
|
<p className="mt-1 text-xs text-theme-text-tertiary">
|
|
1128
826
|
Helm history does not record whether <code className="inline-code text-[11px]">--atomic</code> was set; the rollback is inferred from adjacent release revisions.
|
|
1129
827
|
</p>
|
|
1130
828
|
)}
|
|
829
|
+
<OperationInsightSignals
|
|
830
|
+
insight={operationInsight}
|
|
831
|
+
releaseNamespace={releaseNamespace}
|
|
832
|
+
canCompare={Boolean(canCompare)}
|
|
833
|
+
onCompare={onCompare}
|
|
834
|
+
onNavigateToResource={onNavigateToResource}
|
|
835
|
+
/>
|
|
1131
836
|
{primaryHookDiagnostic && <HookSignal diagnostic={primaryHookDiagnostic} />}
|
|
1132
837
|
{managedByFluxHelmRelease && (
|
|
1133
838
|
<p className="mt-1 text-xs text-theme-text-tertiary">
|
|
@@ -1140,6 +845,125 @@ function HelmOperationBanner({
|
|
|
1140
845
|
)
|
|
1141
846
|
}
|
|
1142
847
|
|
|
848
|
+
function OperationInsightSignals({
|
|
849
|
+
insight,
|
|
850
|
+
releaseNamespace,
|
|
851
|
+
canCompare,
|
|
852
|
+
onCompare,
|
|
853
|
+
onNavigateToResource,
|
|
854
|
+
}: {
|
|
855
|
+
insight?: HelmOperationInsight
|
|
856
|
+
releaseNamespace: string
|
|
857
|
+
canCompare: boolean
|
|
858
|
+
onCompare?: (rev1: number, rev2: number) => void
|
|
859
|
+
onNavigateToResource?: NavigateToResource
|
|
860
|
+
}) {
|
|
861
|
+
const compare = insight?.suggestedCompare
|
|
862
|
+
const primaryResource = insight?.primaryResource
|
|
863
|
+
const relatedCount = primaryResource
|
|
864
|
+
? Math.max(0, (insight?.signalCount ?? 0) - 1)
|
|
865
|
+
: insight?.relatedResources?.length ?? 0
|
|
866
|
+
const showCompare = Boolean(compare && canCompare && onCompare)
|
|
867
|
+
if (!primaryResource && !showCompare) {
|
|
868
|
+
return null
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
return (
|
|
872
|
+
<div className="mt-2 space-y-2">
|
|
873
|
+
{primaryResource && (
|
|
874
|
+
<PrimaryOperationResourceSignal
|
|
875
|
+
resource={primaryResource}
|
|
876
|
+
releaseNamespace={releaseNamespace}
|
|
877
|
+
relatedCount={relatedCount}
|
|
878
|
+
onNavigateToResource={onNavigateToResource}
|
|
879
|
+
/>
|
|
880
|
+
)}
|
|
881
|
+
{showCompare && compare && (
|
|
882
|
+
<Tooltip content={compare.reason || 'Compare the two Helm revisions'}>
|
|
883
|
+
<button
|
|
884
|
+
type="button"
|
|
885
|
+
onClick={() => onCompare?.(compare.revision1, compare.revision2)}
|
|
886
|
+
className="inline-flex items-center gap-1.5 rounded-md border border-theme-border-light bg-theme-elevated px-2.5 py-1.5 text-xs font-medium text-theme-text-primary shadow-theme-sm transition-colors hover:bg-theme-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent"
|
|
887
|
+
>
|
|
888
|
+
<GitCompare className="h-3.5 w-3.5" />
|
|
889
|
+
Compare rev {compare.revision1} to {compare.revision2}
|
|
890
|
+
</button>
|
|
891
|
+
</Tooltip>
|
|
892
|
+
)}
|
|
893
|
+
</div>
|
|
894
|
+
)
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function PrimaryOperationResourceSignal({
|
|
898
|
+
resource,
|
|
899
|
+
releaseNamespace,
|
|
900
|
+
relatedCount,
|
|
901
|
+
onNavigateToResource,
|
|
902
|
+
}: {
|
|
903
|
+
resource: HelmOwnedResource
|
|
904
|
+
releaseNamespace: string
|
|
905
|
+
relatedCount: number
|
|
906
|
+
onNavigateToResource?: NavigateToResource
|
|
907
|
+
}) {
|
|
908
|
+
const canNavigate = Boolean(onNavigateToResource)
|
|
909
|
+
const resourceLabel = `${resource.kind}/${resource.name}`
|
|
910
|
+
const resourceTarget = resource.namespace ? `${resource.namespace}/${resource.name}` : resource.name
|
|
911
|
+
const showNamespace = Boolean(resource.namespace && resource.namespace !== releaseNamespace)
|
|
912
|
+
const resourceTooltip = `Open ${resource.kind} ${resourceTarget}`
|
|
913
|
+
const showInlineReady = Boolean(resource.ready && !resource.summary)
|
|
914
|
+
const openResource = () => {
|
|
915
|
+
onNavigateToResource?.({
|
|
916
|
+
kind: kindToPlural(resource.kind),
|
|
917
|
+
namespace: resource.namespace,
|
|
918
|
+
name: resource.name,
|
|
919
|
+
group: apiVersionToGroup(resource.apiVersion),
|
|
920
|
+
})
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
return (
|
|
924
|
+
<div className="rounded-md bg-theme-base/50 p-2 text-xs">
|
|
925
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
926
|
+
<span className="font-medium text-theme-text-secondary">Likely resource to inspect</span>
|
|
927
|
+
{canNavigate ? (
|
|
928
|
+
<Tooltip content={resourceTooltip}>
|
|
929
|
+
<button
|
|
930
|
+
type="button"
|
|
931
|
+
onClick={openResource}
|
|
932
|
+
aria-label={resourceTooltip}
|
|
933
|
+
className={clsx(
|
|
934
|
+
'badge-sm max-w-full min-w-0 transition-colors hover:brightness-95 focus:outline-none focus:ring-2 focus:ring-accent focus:ring-offset-1 focus:ring-offset-theme-base',
|
|
935
|
+
getKindBadgeColor(resource.kind)
|
|
936
|
+
)}
|
|
937
|
+
>
|
|
938
|
+
<span className="min-w-0 truncate">{resourceLabel}</span>
|
|
939
|
+
<ExternalLink className="h-3 w-3 shrink-0 opacity-70" />
|
|
940
|
+
</button>
|
|
941
|
+
</Tooltip>
|
|
942
|
+
) : (
|
|
943
|
+
<span className={clsx('badge-sm max-w-full min-w-0', getKindBadgeColor(resource.kind))}>
|
|
944
|
+
<span className="min-w-0 truncate">{resourceLabel}</span>
|
|
945
|
+
</span>
|
|
946
|
+
)}
|
|
947
|
+
{showNamespace && <span className="text-theme-text-tertiary">ns/{resource.namespace}</span>}
|
|
948
|
+
{showInlineReady && <span className="font-mono text-theme-text-secondary">{resource.ready}</span>}
|
|
949
|
+
{resource.status && (
|
|
950
|
+
<span className={clsx('badge-sm', getResourceStatusColor(resource.status))}>{resource.status}</span>
|
|
951
|
+
)}
|
|
952
|
+
</div>
|
|
953
|
+
{(resource.issue || resource.summary || resource.message || relatedCount > 0) && (
|
|
954
|
+
<div className="mt-1 text-theme-text-tertiary">
|
|
955
|
+
{[
|
|
956
|
+
resource.issue || resource.summary || resource.message,
|
|
957
|
+
relatedCount > 0 ? `${relatedCount} more affected resource${relatedCount === 1 ? '' : 's'} in Resources tab` : null,
|
|
958
|
+
]
|
|
959
|
+
.filter(Boolean)
|
|
960
|
+
.join(' - ')}
|
|
961
|
+
</div>
|
|
962
|
+
)}
|
|
963
|
+
</div>
|
|
964
|
+
)
|
|
965
|
+
}
|
|
966
|
+
|
|
1143
967
|
function hookDiagnosticTone(diagnostic: HookDiagnostic): 'error' | 'warning' {
|
|
1144
968
|
return diagnostic.phase.toLowerCase() === 'failed' ? 'error' : 'warning'
|
|
1145
969
|
}
|
|
@@ -1220,12 +1044,15 @@ interface OverviewTabProps {
|
|
|
1220
1044
|
notes: string
|
|
1221
1045
|
readme?: string
|
|
1222
1046
|
dependencies?: ChartDependency[]
|
|
1047
|
+
lastOperation?: HelmOperation
|
|
1223
1048
|
}
|
|
1224
1049
|
onCopy: (text: string, key: string) => void
|
|
1225
1050
|
copied: string | null
|
|
1226
1051
|
}
|
|
1227
1052
|
|
|
1228
1053
|
function OverviewTab({ release, onCopy, copied }: OverviewTabProps) {
|
|
1054
|
+
const showDescription = !!release.description && !isDuplicateOperationRawDescription(release.description, release.lastOperation)
|
|
1055
|
+
|
|
1229
1056
|
return (
|
|
1230
1057
|
<div className="p-4 space-y-4">
|
|
1231
1058
|
{/* Chart info */}
|
|
@@ -1256,7 +1083,7 @@ function OverviewTab({ release, onCopy, copied }: OverviewTabProps) {
|
|
|
1256
1083
|
</div>
|
|
1257
1084
|
|
|
1258
1085
|
{/* Description */}
|
|
1259
|
-
{
|
|
1086
|
+
{showDescription && (
|
|
1260
1087
|
<div className="bg-theme-elevated/30 rounded-lg p-4">
|
|
1261
1088
|
<h3 className="text-sm font-medium text-theme-text-secondary mb-2">Description</h3>
|
|
1262
1089
|
<p className="text-sm text-theme-text-secondary">{release.description}</p>
|
|
@@ -1340,6 +1167,16 @@ function OverviewTab({ release, onCopy, copied }: OverviewTabProps) {
|
|
|
1340
1167
|
)
|
|
1341
1168
|
}
|
|
1342
1169
|
|
|
1170
|
+
function isDuplicateOperationRawDescription(description: string, operation?: HelmOperation): boolean {
|
|
1171
|
+
const desc = normalizeComparableText(description)
|
|
1172
|
+
const raw = normalizeComparableText(operation?.rawMessage || '')
|
|
1173
|
+
return desc !== '' && raw.includes(desc)
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
function normalizeComparableText(value: string): string {
|
|
1177
|
+
return value.trim().replace(/\s+/g, ' ')
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1343
1180
|
// Hooks tab content
|
|
1344
1181
|
interface HooksTabProps {
|
|
1345
1182
|
hooks: HelmHook[]
|