@skyhook-io/radar-app 1.8.2 → 1.8.5
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 +412 -146
- package/src/RadarApp.tsx +21 -1
- package/src/api/client.ts +144 -12
- package/src/components/ConnectionErrorView.tsx +1 -1
- package/src/components/ContextSwitcher.tsx +5 -1
- package/src/components/NamespaceSwitcher.tsx +21 -278
- package/src/components/applications/ApplicationsView.tsx +13 -1
- package/src/components/audit/AuditView.tsx +11 -2
- package/src/components/cost/CostView.tsx +12 -2
- package/src/components/curl/ServiceCurlButton.tsx +445 -0
- package/src/components/gitops/GitOpsView.tsx +23 -17
- package/src/components/helm/HelmCompareRoute.tsx +1342 -0
- package/src/components/helm/HelmReleaseDrawer.tsx +448 -67
- package/src/components/helm/HelmView.tsx +79 -62
- package/src/components/helm/ManifestDiffViewer.tsx +18 -7
- 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/ClusterHealthCard.tsx +6 -1
- package/src/components/home/HomeView.tsx +12 -1
- package/src/components/home/mcpToolCatalog.ts +8 -8
- package/src/components/issues/IssuesPane.tsx +29 -18
- package/src/components/portforward/PortForwardButton.tsx +69 -25
- package/src/components/portforward/PortForwardManager.tsx +18 -4
- package/src/components/resources/ResourceDetailDrawer.tsx +8 -3
- package/src/components/resources/ResourcesView.tsx +45 -1
- package/src/components/resources/renderers/PodRenderer.tsx +7 -2
- package/src/components/resources/renderers/ServiceRenderer.tsx +54 -8
- 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/ui/UpdateNotification.tsx +5 -10
- package/src/components/workload/WorkloadView.tsx +57 -8
- package/src/contexts/CapabilitiesContext.tsx +8 -0
- package/src/filter/FilterLocationBridge.tsx +30 -0
- package/src/hooks/useDocumentTitle.ts +25 -0
- package/src/hooks/useKeyboardShortcuts.tsx +1 -0
- package/src/index.ts +15 -0
- package/src/main.tsx +5 -3
- package/src/utils/auditBadges.ts +53 -0
- package/src/utils/navigation.ts +5 -3
|
@@ -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 } from '../../types'
|
|
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,7 +32,7 @@ interface HelmReleaseDrawerProps {
|
|
|
33
32
|
isOpen?: boolean
|
|
34
33
|
}
|
|
35
34
|
|
|
36
|
-
type TabId = 'overview' | 'history' | 'manifest' | 'values' | 'resources' | 'hooks'
|
|
35
|
+
type TabId = 'overview' | 'history' | 'manifest' | 'values' | 'resources' | 'hooks'
|
|
37
36
|
|
|
38
37
|
const MIN_WIDTH = 500
|
|
39
38
|
const MAX_WIDTH_PERCENT = 0.8
|
|
@@ -44,10 +43,10 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
44
43
|
const [activeTab, setActiveTab] = useState<TabId>('overview')
|
|
45
44
|
const [copied, setCopied] = useState<string | null>(null)
|
|
46
45
|
const [drawerWidth, setDrawerWidth] = useState(DEFAULT_WIDTH)
|
|
46
|
+
const [viewportWidth, setViewportWidth] = useState(() => (typeof window === 'undefined' ? DEFAULT_WIDTH : window.innerWidth))
|
|
47
47
|
const [isResizing, setIsResizing] = useState(false)
|
|
48
48
|
const [selectedRevision, setSelectedRevision] = useState<number | undefined>(undefined)
|
|
49
49
|
const [showAllValues, setShowAllValues] = useState(false)
|
|
50
|
-
const [diffRevisions, setDiffRevisions] = useState<{ rev1: number; rev2: number } | null>(null)
|
|
51
50
|
const [rollbackRevision, setRollbackRevision] = useState<number | null>(null)
|
|
52
51
|
const [showUninstallConfirm, setShowUninstallConfirm] = useState(false)
|
|
53
52
|
const [showUpgradeConfirm, setShowUpgradeConfirm] = useState(false)
|
|
@@ -84,15 +83,7 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
84
83
|
release.name,
|
|
85
84
|
showAllValues,
|
|
86
85
|
canViewSensitive,
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
// Fetch diff if comparing revisions
|
|
90
|
-
const { data: diffData, isLoading: diffLoading } = useHelmManifestDiff(
|
|
91
|
-
helmNamespace,
|
|
92
|
-
release.name,
|
|
93
|
-
diffRevisions?.rev1 || 0,
|
|
94
|
-
diffRevisions?.rev2 || 0,
|
|
95
|
-
canViewSensitive,
|
|
86
|
+
selectedRevision,
|
|
96
87
|
)
|
|
97
88
|
|
|
98
89
|
// Lazy check for upgrade availability
|
|
@@ -130,13 +121,24 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
130
121
|
return () => document.removeEventListener('keydown', handleKeyDown)
|
|
131
122
|
}, [onClose])
|
|
132
123
|
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
const handleWindowResize = () => setViewportWidth(window.innerWidth)
|
|
126
|
+
handleWindowResize()
|
|
127
|
+
window.addEventListener('resize', handleWindowResize)
|
|
128
|
+
return () => window.removeEventListener('resize', handleWindowResize)
|
|
129
|
+
}, [])
|
|
130
|
+
|
|
131
|
+
const minDrawerWidth = Math.min(MIN_WIDTH, viewportWidth)
|
|
132
|
+
const maxDrawerWidth = viewportWidth < MIN_WIDTH ? viewportWidth : Math.max(MIN_WIDTH, viewportWidth * MAX_WIDTH_PERCENT)
|
|
133
|
+
const renderedDrawerWidth = Math.max(minDrawerWidth, Math.min(drawerWidth, maxDrawerWidth))
|
|
134
|
+
|
|
133
135
|
// Resize handlers
|
|
134
136
|
const handleResizeStart = useCallback((e: React.MouseEvent) => {
|
|
135
137
|
e.preventDefault()
|
|
136
138
|
setIsResizing(true)
|
|
137
139
|
resizeStartX.current = e.clientX
|
|
138
|
-
resizeStartWidth.current =
|
|
139
|
-
}, [
|
|
140
|
+
resizeStartWidth.current = renderedDrawerWidth
|
|
141
|
+
}, [renderedDrawerWidth])
|
|
140
142
|
|
|
141
143
|
useEffect(() => {
|
|
142
144
|
if (!isResizing) return
|
|
@@ -144,11 +146,13 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
144
146
|
document.body.style.cursor = 'ew-resize'
|
|
145
147
|
document.body.style.userSelect = 'none'
|
|
146
148
|
|
|
147
|
-
const maxWidth = window.innerWidth * MAX_WIDTH_PERCENT
|
|
148
149
|
const handleMouseMove = (e: MouseEvent) => {
|
|
149
150
|
const deltaX = resizeStartX.current - e.clientX
|
|
150
151
|
const newWidth = resizeStartWidth.current + deltaX
|
|
151
|
-
|
|
152
|
+
const viewport = window.innerWidth
|
|
153
|
+
const minWidth = Math.min(MIN_WIDTH, viewport)
|
|
154
|
+
const maxWidth = viewport < MIN_WIDTH ? viewport : Math.max(MIN_WIDTH, viewport * MAX_WIDTH_PERCENT)
|
|
155
|
+
setDrawerWidth(Math.max(minWidth, Math.min(newWidth, maxWidth)))
|
|
152
156
|
}
|
|
153
157
|
const handleMouseUp = () => setIsResizing(false)
|
|
154
158
|
document.addEventListener('mousemove', handleMouseMove)
|
|
@@ -174,10 +178,16 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
174
178
|
startViewTransitionSafe(() => flushSync(() => setActiveTab(tab)))
|
|
175
179
|
}, [])
|
|
176
180
|
|
|
177
|
-
const handleCompareRevisions = (rev1: number, rev2: number) => {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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])
|
|
181
191
|
|
|
182
192
|
const handleViewRevision = (revision: number) => {
|
|
183
193
|
setSelectedRevision(revision)
|
|
@@ -308,11 +318,6 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
308
318
|
{ id: 'hooks', label: 'Hooks', icon: Anchor },
|
|
309
319
|
]
|
|
310
320
|
|
|
311
|
-
// Add diff tab only when comparing
|
|
312
|
-
if (diffRevisions) {
|
|
313
|
-
tabs.push({ id: 'diff', label: 'Diff', icon: FileText })
|
|
314
|
-
}
|
|
315
|
-
|
|
316
321
|
return (
|
|
317
322
|
<div
|
|
318
323
|
className={clsx(
|
|
@@ -320,7 +325,7 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
320
325
|
TRANSITION_DRAWER,
|
|
321
326
|
isOpen ? 'translate-x-0 opacity-100' : 'translate-x-full opacity-0'
|
|
322
327
|
)}
|
|
323
|
-
style={{ width:
|
|
328
|
+
style={{ width: renderedDrawerWidth, top: headerHeight, height: `calc(100vh - ${headerHeight}px - ${dockInset}px)` }}
|
|
324
329
|
>
|
|
325
330
|
{/* Resize handle */}
|
|
326
331
|
<div
|
|
@@ -409,7 +414,7 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
409
414
|
)}
|
|
410
415
|
>
|
|
411
416
|
<Link2 className="w-3 h-3" />
|
|
412
|
-
source not tracked
|
|
417
|
+
upgrade source not tracked
|
|
413
418
|
</button>
|
|
414
419
|
</Tooltip>
|
|
415
420
|
) : (
|
|
@@ -515,7 +520,13 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
515
520
|
<>
|
|
516
521
|
<HelmOperationBanner
|
|
517
522
|
operation={releaseDetail.lastOperation}
|
|
523
|
+
operationInsight={releaseDetail.operationInsight}
|
|
524
|
+
releaseNamespace={releaseDetail.namespace}
|
|
518
525
|
managedByFluxHelmRelease={releaseDetail.managedByFluxHelmRelease}
|
|
526
|
+
hookDiagnostics={releaseDetail.hookDiagnostics}
|
|
527
|
+
canCompare={canViewSensitive}
|
|
528
|
+
onCompare={handleCompareRevisions}
|
|
529
|
+
onNavigateToResource={onNavigateToResource}
|
|
519
530
|
/>
|
|
520
531
|
{activeTab === 'overview' && (
|
|
521
532
|
<OverviewTab release={releaseDetail} onCopy={copyToClipboard} copied={copied} />
|
|
@@ -552,6 +563,8 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
552
563
|
copied={copied === 'values'}
|
|
553
564
|
namespace={helmNamespace}
|
|
554
565
|
name={release.name}
|
|
566
|
+
revision={selectedRevision}
|
|
567
|
+
currentRevision={releaseDetail.revision}
|
|
555
568
|
onApplySuccess={() => refetch()}
|
|
556
569
|
/>
|
|
557
570
|
</RoleGatedPanel>
|
|
@@ -563,21 +576,7 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
563
576
|
/>
|
|
564
577
|
)}
|
|
565
578
|
{activeTab === 'hooks' && (
|
|
566
|
-
<HooksTab hooks={releaseDetail.hooks || []} />
|
|
567
|
-
)}
|
|
568
|
-
{activeTab === 'diff' && diffRevisions && (
|
|
569
|
-
<RoleGatedPanel min="member" feature="release manifest diffs">
|
|
570
|
-
<ManifestDiffViewer
|
|
571
|
-
diff={diffData?.diff || ''}
|
|
572
|
-
isLoading={diffLoading}
|
|
573
|
-
revision1={diffRevisions.rev1}
|
|
574
|
-
revision2={diffRevisions.rev2}
|
|
575
|
-
onClose={() => {
|
|
576
|
-
setDiffRevisions(null)
|
|
577
|
-
setActiveTab('history')
|
|
578
|
-
}}
|
|
579
|
-
/>
|
|
580
|
-
</RoleGatedPanel>
|
|
579
|
+
<HooksTab hooks={releaseDetail.hooks || []} hookDiagnostics={releaseDetail.hookDiagnostics || []} />
|
|
581
580
|
)}
|
|
582
581
|
</>
|
|
583
582
|
)}
|
|
@@ -743,20 +742,54 @@ function helmOperationKey(operation: HelmOperation): string {
|
|
|
743
742
|
|
|
744
743
|
function HelmOperationBanner({
|
|
745
744
|
operation,
|
|
745
|
+
operationInsight,
|
|
746
|
+
releaseNamespace,
|
|
746
747
|
managedByFluxHelmRelease,
|
|
748
|
+
hookDiagnostics,
|
|
749
|
+
canCompare,
|
|
750
|
+
onCompare,
|
|
751
|
+
onNavigateToResource,
|
|
747
752
|
}: {
|
|
748
753
|
operation?: HelmOperation
|
|
754
|
+
operationInsight?: HelmOperationInsight
|
|
755
|
+
releaseNamespace: string
|
|
749
756
|
managedByFluxHelmRelease?: string
|
|
757
|
+
hookDiagnostics?: HookDiagnostic[]
|
|
758
|
+
canCompare?: boolean
|
|
759
|
+
onCompare?: (rev1: number, rev2: number) => void
|
|
760
|
+
onNavigateToResource?: NavigateToResource
|
|
750
761
|
}) {
|
|
751
|
-
|
|
762
|
+
const primaryHookDiagnostic = hookDiagnostics?.[0]
|
|
763
|
+
if ((!operation || !shouldShowOperationBanner(operation)) && !primaryHookDiagnostic) {
|
|
752
764
|
return null
|
|
753
765
|
}
|
|
766
|
+
if (!operation || !shouldShowOperationBanner(operation)) {
|
|
767
|
+
const tone = hookDiagnosticTone(primaryHookDiagnostic!)
|
|
768
|
+
return (
|
|
769
|
+
<div className="m-4 mb-0 card-inner-lg">
|
|
770
|
+
<div className="flex items-start gap-3">
|
|
771
|
+
<AlertTriangle className={clsx('mt-0.5 h-5 w-5 shrink-0', SEVERITY_TEXT[tone])} />
|
|
772
|
+
<div className="min-w-0 flex-1">
|
|
773
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
774
|
+
<span className="text-sm font-medium text-theme-text-primary">Helm hook needs attention</span>
|
|
775
|
+
<span className={clsx('badge-sm', SEVERITY_BADGE[tone])}>{primaryHookDiagnostic!.phase}</span>
|
|
776
|
+
</div>
|
|
777
|
+
<HookSignal diagnostic={primaryHookDiagnostic!} />
|
|
778
|
+
</div>
|
|
779
|
+
</div>
|
|
780
|
+
</div>
|
|
781
|
+
)
|
|
782
|
+
}
|
|
754
783
|
|
|
755
784
|
const isFailure = operation.status === 'failed'
|
|
756
785
|
const isPending = operation.status === 'stuck_pending'
|
|
757
786
|
const tone: 'error' | 'warning' | 'info' = isFailure ? 'error' : operation.kind === 'rollback' ? 'info' : 'warning'
|
|
758
787
|
const Icon = operation.kind === 'upgrade_rolled_back' || operation.kind === 'rollback' ? RotateCcw : isPending ? Clock : AlertTriangle
|
|
759
788
|
const title = operationTitle(operation)
|
|
789
|
+
const statusLabel = operation.status.replace(/_/g, ' ')
|
|
790
|
+
const showStatusBadge = !(operation.status === 'failed' && title.toLowerCase().includes('failed'))
|
|
791
|
+
const rawMessage = operation.rawMessage?.trim()
|
|
792
|
+
const hasRawMessage = !!rawMessage && rawMessage !== operation.message.trim()
|
|
760
793
|
|
|
761
794
|
return (
|
|
762
795
|
<div className="m-4 mb-0 card-inner-lg">
|
|
@@ -765,20 +798,42 @@ function HelmOperationBanner({
|
|
|
765
798
|
<div className="min-w-0 flex-1">
|
|
766
799
|
<div className="flex flex-wrap items-center gap-2">
|
|
767
800
|
<span className="text-sm font-medium text-theme-text-primary">{title}</span>
|
|
768
|
-
|
|
801
|
+
{showStatusBadge && (
|
|
802
|
+
<span className={clsx('badge-sm', SEVERITY_BADGE[tone])}>{statusLabel}</span>
|
|
803
|
+
)}
|
|
769
804
|
<OperationRevisionChips operation={operation} />
|
|
770
805
|
</div>
|
|
771
806
|
<p className="mt-1 text-sm text-theme-text-secondary">{operation.message}</p>
|
|
772
|
-
{
|
|
773
|
-
<
|
|
774
|
-
|
|
775
|
-
|
|
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>
|
|
776
823
|
)}
|
|
777
824
|
{operation.kind === 'upgrade_rolled_back' && (
|
|
778
825
|
<p className="mt-1 text-xs text-theme-text-tertiary">
|
|
779
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.
|
|
780
827
|
</p>
|
|
781
828
|
)}
|
|
829
|
+
<OperationInsightSignals
|
|
830
|
+
insight={operationInsight}
|
|
831
|
+
releaseNamespace={releaseNamespace}
|
|
832
|
+
canCompare={Boolean(canCompare)}
|
|
833
|
+
onCompare={onCompare}
|
|
834
|
+
onNavigateToResource={onNavigateToResource}
|
|
835
|
+
/>
|
|
836
|
+
{primaryHookDiagnostic && <HookSignal diagnostic={primaryHookDiagnostic} />}
|
|
782
837
|
{managedByFluxHelmRelease && (
|
|
783
838
|
<p className="mt-1 text-xs text-theme-text-tertiary">
|
|
784
839
|
This release is managed by Flux HelmRelease {managedByFluxHelmRelease}; direct Helm changes may be reconciled back.
|
|
@@ -790,6 +845,146 @@ function HelmOperationBanner({
|
|
|
790
845
|
)
|
|
791
846
|
}
|
|
792
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
|
+
|
|
967
|
+
function hookDiagnosticTone(diagnostic: HookDiagnostic): 'error' | 'warning' {
|
|
968
|
+
return diagnostic.phase.toLowerCase() === 'failed' ? 'error' : 'warning'
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
function HookSignal({ diagnostic }: { diagnostic: HookDiagnostic }) {
|
|
972
|
+
return (
|
|
973
|
+
<div className="mt-2 rounded-md bg-theme-base/50 p-2 text-xs">
|
|
974
|
+
<div className="font-medium text-theme-text-secondary">
|
|
975
|
+
Hook signal: {formatHookRef(diagnostic)} is {diagnostic.phase}
|
|
976
|
+
</div>
|
|
977
|
+
<div className="mt-1 text-theme-text-tertiary">{diagnostic.message}</div>
|
|
978
|
+
{diagnostic.evidence?.summary && (
|
|
979
|
+
<div className="mt-1 text-theme-text-secondary">{diagnostic.evidence.summary}</div>
|
|
980
|
+
)}
|
|
981
|
+
{diagnostic.evidenceUnavailableReason && (
|
|
982
|
+
<div className="mt-1 text-theme-text-tertiary">{diagnostic.evidenceUnavailableReason}</div>
|
|
983
|
+
)}
|
|
984
|
+
</div>
|
|
985
|
+
)
|
|
986
|
+
}
|
|
987
|
+
|
|
793
988
|
function shouldShowOperationBanner(operation: HelmOperation): boolean {
|
|
794
989
|
return operation.kind === 'upgrade_rolled_back' || operation.kind === 'rollback' || operation.status === 'failed' || operation.status === 'stuck_pending'
|
|
795
990
|
}
|
|
@@ -849,12 +1044,15 @@ interface OverviewTabProps {
|
|
|
849
1044
|
notes: string
|
|
850
1045
|
readme?: string
|
|
851
1046
|
dependencies?: ChartDependency[]
|
|
1047
|
+
lastOperation?: HelmOperation
|
|
852
1048
|
}
|
|
853
1049
|
onCopy: (text: string, key: string) => void
|
|
854
1050
|
copied: string | null
|
|
855
1051
|
}
|
|
856
1052
|
|
|
857
1053
|
function OverviewTab({ release, onCopy, copied }: OverviewTabProps) {
|
|
1054
|
+
const showDescription = !!release.description && !isDuplicateOperationRawDescription(release.description, release.lastOperation)
|
|
1055
|
+
|
|
858
1056
|
return (
|
|
859
1057
|
<div className="p-4 space-y-4">
|
|
860
1058
|
{/* Chart info */}
|
|
@@ -885,7 +1083,7 @@ function OverviewTab({ release, onCopy, copied }: OverviewTabProps) {
|
|
|
885
1083
|
</div>
|
|
886
1084
|
|
|
887
1085
|
{/* Description */}
|
|
888
|
-
{
|
|
1086
|
+
{showDescription && (
|
|
889
1087
|
<div className="bg-theme-elevated/30 rounded-lg p-4">
|
|
890
1088
|
<h3 className="text-sm font-medium text-theme-text-secondary mb-2">Description</h3>
|
|
891
1089
|
<p className="text-sm text-theme-text-secondary">{release.description}</p>
|
|
@@ -969,12 +1167,23 @@ function OverviewTab({ release, onCopy, copied }: OverviewTabProps) {
|
|
|
969
1167
|
)
|
|
970
1168
|
}
|
|
971
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
|
+
|
|
972
1180
|
// Hooks tab content
|
|
973
1181
|
interface HooksTabProps {
|
|
974
1182
|
hooks: HelmHook[]
|
|
1183
|
+
hookDiagnostics: HookDiagnostic[]
|
|
975
1184
|
}
|
|
976
1185
|
|
|
977
|
-
function HooksTab({ hooks }: HooksTabProps) {
|
|
1186
|
+
function HooksTab({ hooks, hookDiagnostics }: HooksTabProps) {
|
|
978
1187
|
if (hooks.length === 0) {
|
|
979
1188
|
return (
|
|
980
1189
|
<div className="flex flex-col items-center justify-center h-48 text-theme-text-tertiary">
|
|
@@ -1006,23 +1215,31 @@ function HooksTab({ hooks }: HooksTabProps) {
|
|
|
1006
1215
|
return SEVERITY_BADGE.neutral
|
|
1007
1216
|
}
|
|
1008
1217
|
|
|
1218
|
+
const diagnostics = new Map<string, HookDiagnostic>()
|
|
1219
|
+
for (const diagnostic of hookDiagnostics) {
|
|
1220
|
+
diagnostics.set(hookDiagnosticKey(diagnostic.namespace, diagnostic.kind, diagnostic.name), diagnostic)
|
|
1221
|
+
diagnostics.set(hookDiagnosticKey(undefined, diagnostic.kind, diagnostic.name), diagnostic)
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1009
1224
|
return (
|
|
1010
1225
|
<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">
|
|
1226
|
+
{hooks.map((hook) => {
|
|
1227
|
+
const diagnostic = diagnostics.get(hookDiagnosticKey(hook.namespace, hook.kind, hook.name))
|
|
1228
|
+
|| diagnostics.get(hookDiagnosticKey(undefined, hook.kind, hook.name))
|
|
1229
|
+
return (
|
|
1230
|
+
<div key={`${hook.namespace || '_'}:${hook.kind}:${hook.name}`} className="bg-theme-elevated/30 rounded-lg p-4">
|
|
1231
|
+
<div className="flex items-start justify-between gap-3 mb-2">
|
|
1232
|
+
<div className="min-w-0">
|
|
1233
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
1234
|
+
<span className="break-all text-theme-text-primary font-medium">{hook.name}</span>
|
|
1235
|
+
<span className="badge-sm shrink-0 bg-theme-hover/50 text-theme-text-secondary">
|
|
1021
1236
|
{hook.kind}
|
|
1022
1237
|
</span>
|
|
1023
1238
|
</div>
|
|
1024
|
-
<div className="flex
|
|
1239
|
+
<div className="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-xs text-theme-text-tertiary">
|
|
1240
|
+
{hook.namespace && <span className="break-all">Namespace: {hook.namespace}</span>}
|
|
1025
1241
|
<span>Weight: {hook.weight}</span>
|
|
1242
|
+
{hook.path && <span className="break-all">Path: {hook.path}</span>}
|
|
1026
1243
|
</div>
|
|
1027
1244
|
</div>
|
|
1028
1245
|
{hook.status && (
|
|
@@ -1041,8 +1258,172 @@ function HooksTab({ hooks }: HooksTabProps) {
|
|
|
1041
1258
|
</span>
|
|
1042
1259
|
))}
|
|
1043
1260
|
</div>
|
|
1261
|
+
{(hook.startedAt || hook.completedAt || hook.deletePolicies?.length || hook.outputLogPolicies?.length) && (
|
|
1262
|
+
<div className="mt-3 grid grid-cols-1 gap-2 text-xs text-theme-text-tertiary sm:grid-cols-2">
|
|
1263
|
+
{hook.startedAt && (
|
|
1264
|
+
<div>
|
|
1265
|
+
<span className="text-theme-text-disabled">Started: </span>
|
|
1266
|
+
<span>{formatDate(hook.startedAt)}</span>
|
|
1267
|
+
</div>
|
|
1268
|
+
)}
|
|
1269
|
+
{hook.completedAt && (
|
|
1270
|
+
<div>
|
|
1271
|
+
<span className="text-theme-text-disabled">Completed: </span>
|
|
1272
|
+
<span>{formatDate(hook.completedAt)}</span>
|
|
1273
|
+
</div>
|
|
1274
|
+
)}
|
|
1275
|
+
{hook.deletePolicies && hook.deletePolicies.length > 0 && (
|
|
1276
|
+
<div className="sm:col-span-2">
|
|
1277
|
+
<span className="text-theme-text-disabled">Delete policies: </span>
|
|
1278
|
+
<span>{hook.deletePolicies.join(', ')}</span>
|
|
1279
|
+
</div>
|
|
1280
|
+
)}
|
|
1281
|
+
{hook.outputLogPolicies && hook.outputLogPolicies.length > 0 && (
|
|
1282
|
+
<div className="sm:col-span-2">
|
|
1283
|
+
<span className="text-theme-text-disabled">Output log policies: </span>
|
|
1284
|
+
<span>{hook.outputLogPolicies.join(', ')}</span>
|
|
1285
|
+
</div>
|
|
1286
|
+
)}
|
|
1287
|
+
</div>
|
|
1288
|
+
)}
|
|
1289
|
+
{diagnostic && <HookDiagnosticEvidence diagnostic={diagnostic} />}
|
|
1044
1290
|
</div>
|
|
1045
|
-
|
|
1291
|
+
)
|
|
1292
|
+
})}
|
|
1293
|
+
</div>
|
|
1294
|
+
)
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
function hookDiagnosticKey(namespace: string | undefined, kind: string | undefined, name: string | undefined): string {
|
|
1298
|
+
return `${namespace || ''}/${(kind || '').toLowerCase()}/${name || ''}`
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
function formatHookRef(hook: Pick<HookDiagnostic, 'namespace' | 'kind' | 'name'>): string {
|
|
1302
|
+
return `${hook.namespace ? `${hook.namespace}/` : ''}${hook.name} (${hook.kind})`
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
function HookDiagnosticEvidence({ diagnostic }: { diagnostic: HookDiagnostic }) {
|
|
1306
|
+
const evidence = diagnostic.evidence
|
|
1307
|
+
|
|
1308
|
+
return (
|
|
1309
|
+
<div className="mt-3 border-t border-theme-border/60 pt-3 text-xs">
|
|
1310
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
1311
|
+
<span className="font-medium text-theme-text-secondary">Diagnostic evidence</span>
|
|
1312
|
+
<span className={clsx('badge-sm', diagnostic.phase.toLowerCase() === 'failed' ? SEVERITY_BADGE.error : SEVERITY_BADGE.warning)}>
|
|
1313
|
+
{diagnostic.phase}
|
|
1314
|
+
</span>
|
|
1315
|
+
{evidence?.summary && <span className="text-theme-text-tertiary">{evidence.summary}</span>}
|
|
1316
|
+
</div>
|
|
1317
|
+
<div className="mt-1 text-theme-text-tertiary">{diagnostic.message}</div>
|
|
1318
|
+
{diagnostic.evidenceUnavailableReason && (
|
|
1319
|
+
<div className="mt-1 text-theme-text-tertiary">{diagnostic.evidenceUnavailableReason}</div>
|
|
1320
|
+
)}
|
|
1321
|
+
{evidence && (
|
|
1322
|
+
<div className="mt-3 space-y-3">
|
|
1323
|
+
{evidence.jobs && evidence.jobs.length > 0 && (
|
|
1324
|
+
<div className="space-y-1">
|
|
1325
|
+
{evidence.jobs.map((job) => (
|
|
1326
|
+
<div key={`${job.namespace || ''}/${job.name}`} className="grid grid-cols-[80px_1fr] gap-2 rounded bg-theme-base/40 px-2 py-1">
|
|
1327
|
+
<span className="text-theme-text-tertiary">Job</span>
|
|
1328
|
+
<span className="min-w-0 text-theme-text-primary">
|
|
1329
|
+
<span className="break-all font-medium">{job.namespace ? `${job.namespace}/` : ''}{job.name}</span>
|
|
1330
|
+
{job.status && <span className="ml-2 text-theme-text-secondary">{job.status}</span>}
|
|
1331
|
+
<span className="ml-2 text-theme-text-tertiary">
|
|
1332
|
+
active {job.active || 0} · succeeded {job.succeeded || 0} · failed {job.failed || 0}
|
|
1333
|
+
</span>
|
|
1334
|
+
</span>
|
|
1335
|
+
{job.conditions?.map((condition) => (
|
|
1336
|
+
<span key={condition} className="col-start-2 text-theme-text-tertiary">{condition}</span>
|
|
1337
|
+
))}
|
|
1338
|
+
</div>
|
|
1339
|
+
))}
|
|
1340
|
+
</div>
|
|
1341
|
+
)}
|
|
1342
|
+
|
|
1343
|
+
{evidence.pods && evidence.pods.length > 0 && (
|
|
1344
|
+
<div className="space-y-1">
|
|
1345
|
+
{evidence.pods.map((pod) => (
|
|
1346
|
+
<div key={`${pod.namespace || ''}/${pod.name}`} className="grid grid-cols-[80px_1fr] gap-2 rounded bg-theme-base/40 px-2 py-1">
|
|
1347
|
+
<span className="text-theme-text-tertiary">Pod</span>
|
|
1348
|
+
<span className="min-w-0 text-theme-text-primary">
|
|
1349
|
+
<span className="break-all font-medium">{pod.namespace ? `${pod.namespace}/` : ''}{pod.name}</span>
|
|
1350
|
+
{pod.phase && <span className="ml-2 text-theme-text-secondary">{pod.phase}</span>}
|
|
1351
|
+
{pod.ready && <span className="ml-2 text-theme-text-tertiary">{pod.ready} ready</span>}
|
|
1352
|
+
{Boolean(pod.restartCount) && <span className="ml-2 text-theme-text-tertiary">{pod.restartCount} restarts</span>}
|
|
1353
|
+
</span>
|
|
1354
|
+
{(pod.reason || pod.message) && (
|
|
1355
|
+
<span className="col-start-2 text-theme-text-tertiary">
|
|
1356
|
+
{pod.reason && <span className="font-medium">{pod.reason}: </span>}
|
|
1357
|
+
{pod.message}
|
|
1358
|
+
</span>
|
|
1359
|
+
)}
|
|
1360
|
+
</div>
|
|
1361
|
+
))}
|
|
1362
|
+
</div>
|
|
1363
|
+
)}
|
|
1364
|
+
|
|
1365
|
+
{evidence.events && evidence.events.length > 0 && (
|
|
1366
|
+
<div className="space-y-1">
|
|
1367
|
+
{evidence.events.map((event, index) => (
|
|
1368
|
+
<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">
|
|
1369
|
+
<span className="text-theme-text-tertiary">Event</span>
|
|
1370
|
+
<span className="min-w-0 text-theme-text-primary">
|
|
1371
|
+
<span className={clsx('badge-sm mr-2', event.type === 'Warning' ? SEVERITY_BADGE.warning : SEVERITY_BADGE.neutral)}>
|
|
1372
|
+
{event.type || 'Event'}
|
|
1373
|
+
</span>
|
|
1374
|
+
<span className="font-medium">{event.reason || 'Unknown'}</span>
|
|
1375
|
+
<span className="ml-2 break-all text-theme-text-tertiary">{event.involvedKind}/{event.involvedName}</span>
|
|
1376
|
+
</span>
|
|
1377
|
+
{event.message && <span className="col-start-2 text-theme-text-tertiary">{event.message}</span>}
|
|
1378
|
+
</div>
|
|
1379
|
+
))}
|
|
1380
|
+
</div>
|
|
1381
|
+
)}
|
|
1382
|
+
|
|
1383
|
+
{evidence.logs && evidence.logs.length > 0 && (
|
|
1384
|
+
<div className="space-y-2">
|
|
1385
|
+
{evidence.logs.map((log, index) => (
|
|
1386
|
+
<HookLogEvidenceBlock key={`${log.pod}/${log.container}/${log.previous ? 'previous' : 'current'}/${index}`} log={log} />
|
|
1387
|
+
))}
|
|
1388
|
+
</div>
|
|
1389
|
+
)}
|
|
1390
|
+
|
|
1391
|
+
{evidence.errors && evidence.errors.length > 0 && (
|
|
1392
|
+
<div className="space-y-1 text-theme-text-tertiary">
|
|
1393
|
+
{evidence.errors.map((error) => (
|
|
1394
|
+
<div key={error}>Evidence read error: {error}</div>
|
|
1395
|
+
))}
|
|
1396
|
+
</div>
|
|
1397
|
+
)}
|
|
1398
|
+
</div>
|
|
1399
|
+
)}
|
|
1400
|
+
</div>
|
|
1401
|
+
)
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
function HookLogEvidenceBlock({ log }: { log: HookLogEvidence }) {
|
|
1405
|
+
const visibleLines = (log.lines || []).slice(0, 8)
|
|
1406
|
+
|
|
1407
|
+
return (
|
|
1408
|
+
<div className="rounded bg-theme-base/40 px-2 py-2">
|
|
1409
|
+
<div className="mb-1 flex flex-wrap items-center gap-2 text-theme-text-secondary">
|
|
1410
|
+
<span className="font-medium">Logs</span>
|
|
1411
|
+
<span className="break-all">{log.pod}/{log.container}</span>
|
|
1412
|
+
{log.previous && <span className="badge-sm bg-theme-hover/50 text-theme-text-secondary">previous</span>}
|
|
1413
|
+
{log.fallback && <span className="text-theme-text-tertiary">fallback tail</span>}
|
|
1414
|
+
</div>
|
|
1415
|
+
{log.error ? (
|
|
1416
|
+
<div className="text-theme-text-tertiary">{log.error}</div>
|
|
1417
|
+
) : (
|
|
1418
|
+
<>
|
|
1419
|
+
<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">
|
|
1420
|
+
{visibleLines.join('\n')}
|
|
1421
|
+
</pre>
|
|
1422
|
+
{log.lines && log.lines.length > visibleLines.length && (
|
|
1423
|
+
<div className="mt-1 text-theme-text-tertiary">+{log.lines.length - visibleLines.length} more lines in API response</div>
|
|
1424
|
+
)}
|
|
1425
|
+
</>
|
|
1426
|
+
)}
|
|
1046
1427
|
</div>
|
|
1047
1428
|
)
|
|
1048
1429
|
}
|