@skyhook-io/radar-app 1.8.2 → 1.8.3
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 +1 -1
- package/src/App.tsx +164 -54
- package/src/RadarApp.tsx +18 -1
- package/src/api/client.ts +112 -6
- package/src/components/NamespaceSwitcher.tsx +52 -30
- package/src/components/curl/ServiceCurlButton.tsx +445 -0
- package/src/components/gitops/GitOpsView.tsx +1 -10
- package/src/components/helm/HelmReleaseDrawer.tsx +575 -31
- package/src/components/helm/ManifestDiffViewer.tsx +15 -4
- package/src/components/helm/OwnedResources.tsx +14 -50
- package/src/components/helm/RevisionHistory.tsx +9 -5
- package/src/components/helm/ValuesViewer.tsx +41 -11
- package/src/components/home/mcpToolCatalog.ts +8 -8
- package/src/components/portforward/PortForwardButton.tsx +69 -25
- package/src/components/portforward/PortForwardManager.tsx +18 -4
- package/src/components/resources/ResourcesView.tsx +42 -1
- package/src/components/resources/renderers/PodRenderer.tsx +7 -2
- package/src/components/resources/renderers/ServiceRenderer.tsx +54 -8
- package/src/components/ui/UpdateNotification.tsx +5 -10
- package/src/components/workload/WorkloadView.tsx +52 -7
- package/src/contexts/CapabilitiesContext.tsx +8 -0
- package/src/hooks/useDocumentTitle.ts +25 -0
- package/src/main.tsx +5 -3
- package/src/utils/auditBadges.ts +53 -0
- package/src/utils/navigation.ts +5 -3
|
@@ -4,15 +4,15 @@ 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, FileText, Settings, Link2, Anchor, GitFork, BookOpen, ArrowUpCircle, Trash2, GitBranch, AlertTriangle, RotateCcw, Clock } from 'lucide-react'
|
|
7
|
+
import { X, Copy, Check, RefreshCw, Package, Code, History, FileText, Settings, Link2, Anchor, GitFork, BookOpen, ArrowUpCircle, Trash2, GitBranch, AlertTriangle, RotateCcw, Clock, GitCompare, Plus, Minus, Equal } from 'lucide-react'
|
|
8
8
|
import { useNavigate } from 'react-router-dom'
|
|
9
9
|
import { clsx } from 'clsx'
|
|
10
|
-
import { useHelmRelease, useHelmManifest, useHelmValues, useHelmManifestDiff, useHelmUpgradeInfo, useHelmReleaseVersions, useHelmUninstall, upgradeWithProgress, rollbackWithProgress } from '../../api/client'
|
|
10
|
+
import { useHelmRelease, useHelmManifest, useHelmValues, useHelmManifestDiff, useHelmValuesDiff, useHelmNotesDiff, useHelmResourceDiff, 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 } from '../../types'
|
|
15
|
+
import type { SelectedHelmRelease, HelmHook, ChartDependency, HelmOperation, HelmRevision, ResourceDiff, HookDiagnostic, HookLogEvidence } 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'
|
|
@@ -34,6 +34,7 @@ interface HelmReleaseDrawerProps {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
type TabId = 'overview' | 'history' | 'manifest' | 'values' | 'resources' | 'hooks' | 'diff'
|
|
37
|
+
type CompareMode = 'summary' | 'values' | 'manifest' | 'notes' | 'resources'
|
|
37
38
|
|
|
38
39
|
const MIN_WIDTH = 500
|
|
39
40
|
const MAX_WIDTH_PERCENT = 0.8
|
|
@@ -44,10 +45,12 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
44
45
|
const [activeTab, setActiveTab] = useState<TabId>('overview')
|
|
45
46
|
const [copied, setCopied] = useState<string | null>(null)
|
|
46
47
|
const [drawerWidth, setDrawerWidth] = useState(DEFAULT_WIDTH)
|
|
48
|
+
const [viewportWidth, setViewportWidth] = useState(() => (typeof window === 'undefined' ? DEFAULT_WIDTH : window.innerWidth))
|
|
47
49
|
const [isResizing, setIsResizing] = useState(false)
|
|
48
50
|
const [selectedRevision, setSelectedRevision] = useState<number | undefined>(undefined)
|
|
49
51
|
const [showAllValues, setShowAllValues] = useState(false)
|
|
50
52
|
const [diffRevisions, setDiffRevisions] = useState<{ rev1: number; rev2: number } | null>(null)
|
|
53
|
+
const [compareMode, setCompareMode] = useState<CompareMode>('summary')
|
|
51
54
|
const [rollbackRevision, setRollbackRevision] = useState<number | null>(null)
|
|
52
55
|
const [showUninstallConfirm, setShowUninstallConfirm] = useState(false)
|
|
53
56
|
const [showUpgradeConfirm, setShowUpgradeConfirm] = useState(false)
|
|
@@ -84,6 +87,7 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
84
87
|
release.name,
|
|
85
88
|
showAllValues,
|
|
86
89
|
canViewSensitive,
|
|
90
|
+
selectedRevision,
|
|
87
91
|
)
|
|
88
92
|
|
|
89
93
|
// Fetch diff if comparing revisions
|
|
@@ -92,7 +96,29 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
92
96
|
release.name,
|
|
93
97
|
diffRevisions?.rev1 || 0,
|
|
94
98
|
diffRevisions?.rev2 || 0,
|
|
95
|
-
canViewSensitive,
|
|
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',
|
|
96
122
|
)
|
|
97
123
|
|
|
98
124
|
// Lazy check for upgrade availability
|
|
@@ -130,13 +156,24 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
130
156
|
return () => document.removeEventListener('keydown', handleKeyDown)
|
|
131
157
|
}, [onClose])
|
|
132
158
|
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
const handleWindowResize = () => setViewportWidth(window.innerWidth)
|
|
161
|
+
handleWindowResize()
|
|
162
|
+
window.addEventListener('resize', handleWindowResize)
|
|
163
|
+
return () => window.removeEventListener('resize', handleWindowResize)
|
|
164
|
+
}, [])
|
|
165
|
+
|
|
166
|
+
const minDrawerWidth = Math.min(MIN_WIDTH, viewportWidth)
|
|
167
|
+
const maxDrawerWidth = viewportWidth < MIN_WIDTH ? viewportWidth : Math.max(MIN_WIDTH, viewportWidth * MAX_WIDTH_PERCENT)
|
|
168
|
+
const renderedDrawerWidth = Math.max(minDrawerWidth, Math.min(drawerWidth, maxDrawerWidth))
|
|
169
|
+
|
|
133
170
|
// Resize handlers
|
|
134
171
|
const handleResizeStart = useCallback((e: React.MouseEvent) => {
|
|
135
172
|
e.preventDefault()
|
|
136
173
|
setIsResizing(true)
|
|
137
174
|
resizeStartX.current = e.clientX
|
|
138
|
-
resizeStartWidth.current =
|
|
139
|
-
}, [
|
|
175
|
+
resizeStartWidth.current = renderedDrawerWidth
|
|
176
|
+
}, [renderedDrawerWidth])
|
|
140
177
|
|
|
141
178
|
useEffect(() => {
|
|
142
179
|
if (!isResizing) return
|
|
@@ -144,11 +181,13 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
144
181
|
document.body.style.cursor = 'ew-resize'
|
|
145
182
|
document.body.style.userSelect = 'none'
|
|
146
183
|
|
|
147
|
-
const maxWidth = window.innerWidth * MAX_WIDTH_PERCENT
|
|
148
184
|
const handleMouseMove = (e: MouseEvent) => {
|
|
149
185
|
const deltaX = resizeStartX.current - e.clientX
|
|
150
186
|
const newWidth = resizeStartWidth.current + deltaX
|
|
151
|
-
|
|
187
|
+
const viewport = window.innerWidth
|
|
188
|
+
const minWidth = Math.min(MIN_WIDTH, viewport)
|
|
189
|
+
const maxWidth = viewport < MIN_WIDTH ? viewport : Math.max(MIN_WIDTH, viewport * MAX_WIDTH_PERCENT)
|
|
190
|
+
setDrawerWidth(Math.max(minWidth, Math.min(newWidth, maxWidth)))
|
|
152
191
|
}
|
|
153
192
|
const handleMouseUp = () => setIsResizing(false)
|
|
154
193
|
document.addEventListener('mousemove', handleMouseMove)
|
|
@@ -176,6 +215,7 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
176
215
|
|
|
177
216
|
const handleCompareRevisions = (rev1: number, rev2: number) => {
|
|
178
217
|
setDiffRevisions({ rev1, rev2 })
|
|
218
|
+
setCompareMode('summary')
|
|
179
219
|
switchTab('diff')
|
|
180
220
|
}
|
|
181
221
|
|
|
@@ -310,7 +350,7 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
310
350
|
|
|
311
351
|
// Add diff tab only when comparing
|
|
312
352
|
if (diffRevisions) {
|
|
313
|
-
tabs.push({ id: 'diff', label: '
|
|
353
|
+
tabs.push({ id: 'diff', label: 'Compare', icon: GitCompare })
|
|
314
354
|
}
|
|
315
355
|
|
|
316
356
|
return (
|
|
@@ -320,7 +360,7 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
320
360
|
TRANSITION_DRAWER,
|
|
321
361
|
isOpen ? 'translate-x-0 opacity-100' : 'translate-x-full opacity-0'
|
|
322
362
|
)}
|
|
323
|
-
style={{ width:
|
|
363
|
+
style={{ width: renderedDrawerWidth, top: headerHeight, height: `calc(100vh - ${headerHeight}px - ${dockInset}px)` }}
|
|
324
364
|
>
|
|
325
365
|
{/* Resize handle */}
|
|
326
366
|
<div
|
|
@@ -409,7 +449,7 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
409
449
|
)}
|
|
410
450
|
>
|
|
411
451
|
<Link2 className="w-3 h-3" />
|
|
412
|
-
source not tracked
|
|
452
|
+
upgrade source not tracked
|
|
413
453
|
</button>
|
|
414
454
|
</Tooltip>
|
|
415
455
|
) : (
|
|
@@ -516,6 +556,7 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
516
556
|
<HelmOperationBanner
|
|
517
557
|
operation={releaseDetail.lastOperation}
|
|
518
558
|
managedByFluxHelmRelease={releaseDetail.managedByFluxHelmRelease}
|
|
559
|
+
hookDiagnostics={releaseDetail.hookDiagnostics}
|
|
519
560
|
/>
|
|
520
561
|
{activeTab === 'overview' && (
|
|
521
562
|
<OverviewTab release={releaseDetail} onCopy={copyToClipboard} copied={copied} />
|
|
@@ -552,6 +593,8 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
552
593
|
copied={copied === 'values'}
|
|
553
594
|
namespace={helmNamespace}
|
|
554
595
|
name={release.name}
|
|
596
|
+
revision={selectedRevision}
|
|
597
|
+
currentRevision={releaseDetail.revision}
|
|
555
598
|
onApplySuccess={() => refetch()}
|
|
556
599
|
/>
|
|
557
600
|
</RoleGatedPanel>
|
|
@@ -563,17 +606,27 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
563
606
|
/>
|
|
564
607
|
)}
|
|
565
608
|
{activeTab === 'hooks' && (
|
|
566
|
-
<HooksTab hooks={releaseDetail.hooks || []} />
|
|
609
|
+
<HooksTab hooks={releaseDetail.hooks || []} hookDiagnostics={releaseDetail.hookDiagnostics || []} />
|
|
567
610
|
)}
|
|
568
611
|
{activeTab === 'diff' && diffRevisions && (
|
|
569
|
-
<RoleGatedPanel min="member" feature="release
|
|
570
|
-
<
|
|
571
|
-
|
|
572
|
-
isLoading={diffLoading}
|
|
612
|
+
<RoleGatedPanel min="member" feature="release revision comparison">
|
|
613
|
+
<HelmRevisionCompareView
|
|
614
|
+
revisions={releaseDetail.history}
|
|
573
615
|
revision1={diffRevisions.rev1}
|
|
574
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}
|
|
575
627
|
onClose={() => {
|
|
576
628
|
setDiffRevisions(null)
|
|
629
|
+
setCompareMode('summary')
|
|
577
630
|
setActiveTab('history')
|
|
578
631
|
}}
|
|
579
632
|
/>
|
|
@@ -730,6 +783,278 @@ function mergeHelmOperations(operations: HelmOperation[] | undefined, lastOperat
|
|
|
730
783
|
return merged
|
|
731
784
|
}
|
|
732
785
|
|
|
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
|
+
|
|
733
1058
|
function helmOperationKey(operation: HelmOperation): string {
|
|
734
1059
|
return [
|
|
735
1060
|
operation.kind,
|
|
@@ -744,19 +1069,41 @@ function helmOperationKey(operation: HelmOperation): string {
|
|
|
744
1069
|
function HelmOperationBanner({
|
|
745
1070
|
operation,
|
|
746
1071
|
managedByFluxHelmRelease,
|
|
1072
|
+
hookDiagnostics,
|
|
747
1073
|
}: {
|
|
748
1074
|
operation?: HelmOperation
|
|
749
1075
|
managedByFluxHelmRelease?: string
|
|
1076
|
+
hookDiagnostics?: HookDiagnostic[]
|
|
750
1077
|
}) {
|
|
751
|
-
|
|
1078
|
+
const primaryHookDiagnostic = hookDiagnostics?.[0]
|
|
1079
|
+
if ((!operation || !shouldShowOperationBanner(operation)) && !primaryHookDiagnostic) {
|
|
752
1080
|
return null
|
|
753
1081
|
}
|
|
1082
|
+
if (!operation || !shouldShowOperationBanner(operation)) {
|
|
1083
|
+
const tone = hookDiagnosticTone(primaryHookDiagnostic!)
|
|
1084
|
+
return (
|
|
1085
|
+
<div className="m-4 mb-0 card-inner-lg">
|
|
1086
|
+
<div className="flex items-start gap-3">
|
|
1087
|
+
<AlertTriangle className={clsx('mt-0.5 h-5 w-5 shrink-0', SEVERITY_TEXT[tone])} />
|
|
1088
|
+
<div className="min-w-0 flex-1">
|
|
1089
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
1090
|
+
<span className="text-sm font-medium text-theme-text-primary">Helm hook needs attention</span>
|
|
1091
|
+
<span className={clsx('badge-sm', SEVERITY_BADGE[tone])}>{primaryHookDiagnostic!.phase}</span>
|
|
1092
|
+
</div>
|
|
1093
|
+
<HookSignal diagnostic={primaryHookDiagnostic!} />
|
|
1094
|
+
</div>
|
|
1095
|
+
</div>
|
|
1096
|
+
</div>
|
|
1097
|
+
)
|
|
1098
|
+
}
|
|
754
1099
|
|
|
755
1100
|
const isFailure = operation.status === 'failed'
|
|
756
1101
|
const isPending = operation.status === 'stuck_pending'
|
|
757
1102
|
const tone: 'error' | 'warning' | 'info' = isFailure ? 'error' : operation.kind === 'rollback' ? 'info' : 'warning'
|
|
758
1103
|
const Icon = operation.kind === 'upgrade_rolled_back' || operation.kind === 'rollback' ? RotateCcw : isPending ? Clock : AlertTriangle
|
|
759
1104
|
const title = operationTitle(operation)
|
|
1105
|
+
const statusLabel = operation.status.replace(/_/g, ' ')
|
|
1106
|
+
const showStatusBadge = !(operation.status === 'failed' && title.toLowerCase().includes('failed'))
|
|
760
1107
|
|
|
761
1108
|
return (
|
|
762
1109
|
<div className="m-4 mb-0 card-inner-lg">
|
|
@@ -765,7 +1112,9 @@ function HelmOperationBanner({
|
|
|
765
1112
|
<div className="min-w-0 flex-1">
|
|
766
1113
|
<div className="flex flex-wrap items-center gap-2">
|
|
767
1114
|
<span className="text-sm font-medium text-theme-text-primary">{title}</span>
|
|
768
|
-
|
|
1115
|
+
{showStatusBadge && (
|
|
1116
|
+
<span className={clsx('badge-sm', SEVERITY_BADGE[tone])}>{statusLabel}</span>
|
|
1117
|
+
)}
|
|
769
1118
|
<OperationRevisionChips operation={operation} />
|
|
770
1119
|
</div>
|
|
771
1120
|
<p className="mt-1 text-sm text-theme-text-secondary">{operation.message}</p>
|
|
@@ -779,6 +1128,7 @@ function HelmOperationBanner({
|
|
|
779
1128
|
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
1129
|
</p>
|
|
781
1130
|
)}
|
|
1131
|
+
{primaryHookDiagnostic && <HookSignal diagnostic={primaryHookDiagnostic} />}
|
|
782
1132
|
{managedByFluxHelmRelease && (
|
|
783
1133
|
<p className="mt-1 text-xs text-theme-text-tertiary">
|
|
784
1134
|
This release is managed by Flux HelmRelease {managedByFluxHelmRelease}; direct Helm changes may be reconciled back.
|
|
@@ -790,6 +1140,27 @@ function HelmOperationBanner({
|
|
|
790
1140
|
)
|
|
791
1141
|
}
|
|
792
1142
|
|
|
1143
|
+
function hookDiagnosticTone(diagnostic: HookDiagnostic): 'error' | 'warning' {
|
|
1144
|
+
return diagnostic.phase.toLowerCase() === 'failed' ? 'error' : 'warning'
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
function HookSignal({ diagnostic }: { diagnostic: HookDiagnostic }) {
|
|
1148
|
+
return (
|
|
1149
|
+
<div className="mt-2 rounded-md bg-theme-base/50 p-2 text-xs">
|
|
1150
|
+
<div className="font-medium text-theme-text-secondary">
|
|
1151
|
+
Hook signal: {formatHookRef(diagnostic)} is {diagnostic.phase}
|
|
1152
|
+
</div>
|
|
1153
|
+
<div className="mt-1 text-theme-text-tertiary">{diagnostic.message}</div>
|
|
1154
|
+
{diagnostic.evidence?.summary && (
|
|
1155
|
+
<div className="mt-1 text-theme-text-secondary">{diagnostic.evidence.summary}</div>
|
|
1156
|
+
)}
|
|
1157
|
+
{diagnostic.evidenceUnavailableReason && (
|
|
1158
|
+
<div className="mt-1 text-theme-text-tertiary">{diagnostic.evidenceUnavailableReason}</div>
|
|
1159
|
+
)}
|
|
1160
|
+
</div>
|
|
1161
|
+
)
|
|
1162
|
+
}
|
|
1163
|
+
|
|
793
1164
|
function shouldShowOperationBanner(operation: HelmOperation): boolean {
|
|
794
1165
|
return operation.kind === 'upgrade_rolled_back' || operation.kind === 'rollback' || operation.status === 'failed' || operation.status === 'stuck_pending'
|
|
795
1166
|
}
|
|
@@ -972,9 +1343,10 @@ function OverviewTab({ release, onCopy, copied }: OverviewTabProps) {
|
|
|
972
1343
|
// Hooks tab content
|
|
973
1344
|
interface HooksTabProps {
|
|
974
1345
|
hooks: HelmHook[]
|
|
1346
|
+
hookDiagnostics: HookDiagnostic[]
|
|
975
1347
|
}
|
|
976
1348
|
|
|
977
|
-
function HooksTab({ hooks }: HooksTabProps) {
|
|
1349
|
+
function HooksTab({ hooks, hookDiagnostics }: HooksTabProps) {
|
|
978
1350
|
if (hooks.length === 0) {
|
|
979
1351
|
return (
|
|
980
1352
|
<div className="flex flex-col items-center justify-center h-48 text-theme-text-tertiary">
|
|
@@ -1006,23 +1378,31 @@ function HooksTab({ hooks }: HooksTabProps) {
|
|
|
1006
1378
|
return SEVERITY_BADGE.neutral
|
|
1007
1379
|
}
|
|
1008
1380
|
|
|
1381
|
+
const diagnostics = new Map<string, HookDiagnostic>()
|
|
1382
|
+
for (const diagnostic of hookDiagnostics) {
|
|
1383
|
+
diagnostics.set(hookDiagnosticKey(diagnostic.namespace, diagnostic.kind, diagnostic.name), diagnostic)
|
|
1384
|
+
diagnostics.set(hookDiagnosticKey(undefined, diagnostic.kind, diagnostic.name), diagnostic)
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1009
1387
|
return (
|
|
1010
1388
|
<div className="p-4 space-y-3">
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
<div key={hook.name} className="bg-theme-elevated/30 rounded-lg p-4">
|
|
1016
|
-
<div className="flex items-start justify-between mb-2">
|
|
1017
|
-
<div>
|
|
1018
|
-
<div className="flex items-center gap-2">
|
|
1019
|
-
<span className="text-theme-text-primary font-medium">{hook.name}</span>
|
|
1020
|
-
<span className="badge-sm bg-theme-hover/50 text-theme-text-secondary">
|
|
1389
|
+
{hooks.map((hook) => {
|
|
1390
|
+
const diagnostic = diagnostics.get(hookDiagnosticKey(hook.namespace, hook.kind, hook.name))
|
|
1391
|
+
|| diagnostics.get(hookDiagnosticKey(undefined, hook.kind, hook.name))
|
|
1392
|
+
return (
|
|
1393
|
+
<div key={`${hook.namespace || '_'}:${hook.kind}:${hook.name}`} className="bg-theme-elevated/30 rounded-lg p-4">
|
|
1394
|
+
<div className="flex items-start justify-between gap-3 mb-2">
|
|
1395
|
+
<div className="min-w-0">
|
|
1396
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
1397
|
+
<span className="break-all text-theme-text-primary font-medium">{hook.name}</span>
|
|
1398
|
+
<span className="badge-sm shrink-0 bg-theme-hover/50 text-theme-text-secondary">
|
|
1021
1399
|
{hook.kind}
|
|
1022
1400
|
</span>
|
|
1023
1401
|
</div>
|
|
1024
|
-
<div className="flex
|
|
1402
|
+
<div className="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-xs text-theme-text-tertiary">
|
|
1403
|
+
{hook.namespace && <span className="break-all">Namespace: {hook.namespace}</span>}
|
|
1025
1404
|
<span>Weight: {hook.weight}</span>
|
|
1405
|
+
{hook.path && <span className="break-all">Path: {hook.path}</span>}
|
|
1026
1406
|
</div>
|
|
1027
1407
|
</div>
|
|
1028
1408
|
{hook.status && (
|
|
@@ -1041,8 +1421,172 @@ function HooksTab({ hooks }: HooksTabProps) {
|
|
|
1041
1421
|
</span>
|
|
1042
1422
|
))}
|
|
1043
1423
|
</div>
|
|
1424
|
+
{(hook.startedAt || hook.completedAt || hook.deletePolicies?.length || hook.outputLogPolicies?.length) && (
|
|
1425
|
+
<div className="mt-3 grid grid-cols-1 gap-2 text-xs text-theme-text-tertiary sm:grid-cols-2">
|
|
1426
|
+
{hook.startedAt && (
|
|
1427
|
+
<div>
|
|
1428
|
+
<span className="text-theme-text-disabled">Started: </span>
|
|
1429
|
+
<span>{formatDate(hook.startedAt)}</span>
|
|
1430
|
+
</div>
|
|
1431
|
+
)}
|
|
1432
|
+
{hook.completedAt && (
|
|
1433
|
+
<div>
|
|
1434
|
+
<span className="text-theme-text-disabled">Completed: </span>
|
|
1435
|
+
<span>{formatDate(hook.completedAt)}</span>
|
|
1436
|
+
</div>
|
|
1437
|
+
)}
|
|
1438
|
+
{hook.deletePolicies && hook.deletePolicies.length > 0 && (
|
|
1439
|
+
<div className="sm:col-span-2">
|
|
1440
|
+
<span className="text-theme-text-disabled">Delete policies: </span>
|
|
1441
|
+
<span>{hook.deletePolicies.join(', ')}</span>
|
|
1442
|
+
</div>
|
|
1443
|
+
)}
|
|
1444
|
+
{hook.outputLogPolicies && hook.outputLogPolicies.length > 0 && (
|
|
1445
|
+
<div className="sm:col-span-2">
|
|
1446
|
+
<span className="text-theme-text-disabled">Output log policies: </span>
|
|
1447
|
+
<span>{hook.outputLogPolicies.join(', ')}</span>
|
|
1448
|
+
</div>
|
|
1449
|
+
)}
|
|
1450
|
+
</div>
|
|
1451
|
+
)}
|
|
1452
|
+
{diagnostic && <HookDiagnosticEvidence diagnostic={diagnostic} />}
|
|
1044
1453
|
</div>
|
|
1045
|
-
|
|
1454
|
+
)
|
|
1455
|
+
})}
|
|
1456
|
+
</div>
|
|
1457
|
+
)
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
function hookDiagnosticKey(namespace: string | undefined, kind: string | undefined, name: string | undefined): string {
|
|
1461
|
+
return `${namespace || ''}/${(kind || '').toLowerCase()}/${name || ''}`
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
function formatHookRef(hook: Pick<HookDiagnostic, 'namespace' | 'kind' | 'name'>): string {
|
|
1465
|
+
return `${hook.namespace ? `${hook.namespace}/` : ''}${hook.name} (${hook.kind})`
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
function HookDiagnosticEvidence({ diagnostic }: { diagnostic: HookDiagnostic }) {
|
|
1469
|
+
const evidence = diagnostic.evidence
|
|
1470
|
+
|
|
1471
|
+
return (
|
|
1472
|
+
<div className="mt-3 border-t border-theme-border/60 pt-3 text-xs">
|
|
1473
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
1474
|
+
<span className="font-medium text-theme-text-secondary">Diagnostic evidence</span>
|
|
1475
|
+
<span className={clsx('badge-sm', diagnostic.phase.toLowerCase() === 'failed' ? SEVERITY_BADGE.error : SEVERITY_BADGE.warning)}>
|
|
1476
|
+
{diagnostic.phase}
|
|
1477
|
+
</span>
|
|
1478
|
+
{evidence?.summary && <span className="text-theme-text-tertiary">{evidence.summary}</span>}
|
|
1479
|
+
</div>
|
|
1480
|
+
<div className="mt-1 text-theme-text-tertiary">{diagnostic.message}</div>
|
|
1481
|
+
{diagnostic.evidenceUnavailableReason && (
|
|
1482
|
+
<div className="mt-1 text-theme-text-tertiary">{diagnostic.evidenceUnavailableReason}</div>
|
|
1483
|
+
)}
|
|
1484
|
+
{evidence && (
|
|
1485
|
+
<div className="mt-3 space-y-3">
|
|
1486
|
+
{evidence.jobs && evidence.jobs.length > 0 && (
|
|
1487
|
+
<div className="space-y-1">
|
|
1488
|
+
{evidence.jobs.map((job) => (
|
|
1489
|
+
<div key={`${job.namespace || ''}/${job.name}`} className="grid grid-cols-[80px_1fr] gap-2 rounded bg-theme-base/40 px-2 py-1">
|
|
1490
|
+
<span className="text-theme-text-tertiary">Job</span>
|
|
1491
|
+
<span className="min-w-0 text-theme-text-primary">
|
|
1492
|
+
<span className="break-all font-medium">{job.namespace ? `${job.namespace}/` : ''}{job.name}</span>
|
|
1493
|
+
{job.status && <span className="ml-2 text-theme-text-secondary">{job.status}</span>}
|
|
1494
|
+
<span className="ml-2 text-theme-text-tertiary">
|
|
1495
|
+
active {job.active || 0} · succeeded {job.succeeded || 0} · failed {job.failed || 0}
|
|
1496
|
+
</span>
|
|
1497
|
+
</span>
|
|
1498
|
+
{job.conditions?.map((condition) => (
|
|
1499
|
+
<span key={condition} className="col-start-2 text-theme-text-tertiary">{condition}</span>
|
|
1500
|
+
))}
|
|
1501
|
+
</div>
|
|
1502
|
+
))}
|
|
1503
|
+
</div>
|
|
1504
|
+
)}
|
|
1505
|
+
|
|
1506
|
+
{evidence.pods && evidence.pods.length > 0 && (
|
|
1507
|
+
<div className="space-y-1">
|
|
1508
|
+
{evidence.pods.map((pod) => (
|
|
1509
|
+
<div key={`${pod.namespace || ''}/${pod.name}`} className="grid grid-cols-[80px_1fr] gap-2 rounded bg-theme-base/40 px-2 py-1">
|
|
1510
|
+
<span className="text-theme-text-tertiary">Pod</span>
|
|
1511
|
+
<span className="min-w-0 text-theme-text-primary">
|
|
1512
|
+
<span className="break-all font-medium">{pod.namespace ? `${pod.namespace}/` : ''}{pod.name}</span>
|
|
1513
|
+
{pod.phase && <span className="ml-2 text-theme-text-secondary">{pod.phase}</span>}
|
|
1514
|
+
{pod.ready && <span className="ml-2 text-theme-text-tertiary">{pod.ready} ready</span>}
|
|
1515
|
+
{Boolean(pod.restartCount) && <span className="ml-2 text-theme-text-tertiary">{pod.restartCount} restarts</span>}
|
|
1516
|
+
</span>
|
|
1517
|
+
{(pod.reason || pod.message) && (
|
|
1518
|
+
<span className="col-start-2 text-theme-text-tertiary">
|
|
1519
|
+
{pod.reason && <span className="font-medium">{pod.reason}: </span>}
|
|
1520
|
+
{pod.message}
|
|
1521
|
+
</span>
|
|
1522
|
+
)}
|
|
1523
|
+
</div>
|
|
1524
|
+
))}
|
|
1525
|
+
</div>
|
|
1526
|
+
)}
|
|
1527
|
+
|
|
1528
|
+
{evidence.events && evidence.events.length > 0 && (
|
|
1529
|
+
<div className="space-y-1">
|
|
1530
|
+
{evidence.events.map((event, index) => (
|
|
1531
|
+
<div key={`${event.involvedKind}/${event.involvedName}/${event.reason || index}`} className="grid grid-cols-[80px_1fr] gap-2 rounded bg-theme-base/40 px-2 py-1">
|
|
1532
|
+
<span className="text-theme-text-tertiary">Event</span>
|
|
1533
|
+
<span className="min-w-0 text-theme-text-primary">
|
|
1534
|
+
<span className={clsx('badge-sm mr-2', event.type === 'Warning' ? SEVERITY_BADGE.warning : SEVERITY_BADGE.neutral)}>
|
|
1535
|
+
{event.type || 'Event'}
|
|
1536
|
+
</span>
|
|
1537
|
+
<span className="font-medium">{event.reason || 'Unknown'}</span>
|
|
1538
|
+
<span className="ml-2 break-all text-theme-text-tertiary">{event.involvedKind}/{event.involvedName}</span>
|
|
1539
|
+
</span>
|
|
1540
|
+
{event.message && <span className="col-start-2 text-theme-text-tertiary">{event.message}</span>}
|
|
1541
|
+
</div>
|
|
1542
|
+
))}
|
|
1543
|
+
</div>
|
|
1544
|
+
)}
|
|
1545
|
+
|
|
1546
|
+
{evidence.logs && evidence.logs.length > 0 && (
|
|
1547
|
+
<div className="space-y-2">
|
|
1548
|
+
{evidence.logs.map((log, index) => (
|
|
1549
|
+
<HookLogEvidenceBlock key={`${log.pod}/${log.container}/${log.previous ? 'previous' : 'current'}/${index}`} log={log} />
|
|
1550
|
+
))}
|
|
1551
|
+
</div>
|
|
1552
|
+
)}
|
|
1553
|
+
|
|
1554
|
+
{evidence.errors && evidence.errors.length > 0 && (
|
|
1555
|
+
<div className="space-y-1 text-theme-text-tertiary">
|
|
1556
|
+
{evidence.errors.map((error) => (
|
|
1557
|
+
<div key={error}>Evidence read error: {error}</div>
|
|
1558
|
+
))}
|
|
1559
|
+
</div>
|
|
1560
|
+
)}
|
|
1561
|
+
</div>
|
|
1562
|
+
)}
|
|
1563
|
+
</div>
|
|
1564
|
+
)
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
function HookLogEvidenceBlock({ log }: { log: HookLogEvidence }) {
|
|
1568
|
+
const visibleLines = (log.lines || []).slice(0, 8)
|
|
1569
|
+
|
|
1570
|
+
return (
|
|
1571
|
+
<div className="rounded bg-theme-base/40 px-2 py-2">
|
|
1572
|
+
<div className="mb-1 flex flex-wrap items-center gap-2 text-theme-text-secondary">
|
|
1573
|
+
<span className="font-medium">Logs</span>
|
|
1574
|
+
<span className="break-all">{log.pod}/{log.container}</span>
|
|
1575
|
+
{log.previous && <span className="badge-sm bg-theme-hover/50 text-theme-text-secondary">previous</span>}
|
|
1576
|
+
{log.fallback && <span className="text-theme-text-tertiary">fallback tail</span>}
|
|
1577
|
+
</div>
|
|
1578
|
+
{log.error ? (
|
|
1579
|
+
<div className="text-theme-text-tertiary">{log.error}</div>
|
|
1580
|
+
) : (
|
|
1581
|
+
<>
|
|
1582
|
+
<pre className="max-h-44 overflow-auto whitespace-pre-wrap rounded bg-theme-base px-2 py-1 font-mono text-[11px] leading-5 text-theme-text-secondary">
|
|
1583
|
+
{visibleLines.join('\n')}
|
|
1584
|
+
</pre>
|
|
1585
|
+
{log.lines && log.lines.length > visibleLines.length && (
|
|
1586
|
+
<div className="mt-1 text-theme-text-tertiary">+{log.lines.length - visibleLines.length} more lines in API response</div>
|
|
1587
|
+
)}
|
|
1588
|
+
</>
|
|
1589
|
+
)}
|
|
1046
1590
|
</div>
|
|
1047
1591
|
)
|
|
1048
1592
|
}
|