@skyhook-io/radar-app 0.2.2 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1
- package/package.json +33 -25
- package/src/App.tsx +1449 -382
- package/src/RadarApp.tsx +132 -19
- package/src/api/apiResources.ts +1 -1
- package/src/api/client.argoResourceSync.test.ts +69 -0
- package/src/api/client.delta.test.ts +89 -0
- package/src/api/client.deltaSync.test.ts +216 -0
- package/src/api/client.metrics.test.ts +106 -0
- package/src/api/client.rightsizing.test.ts +32 -0
- package/src/api/client.ts +2730 -271
- package/src/api/client.yaml.test.ts +45 -0
- package/src/api/diagnose.ts +289 -0
- package/src/api/quotas.ts +16 -0
- package/src/api/rbac.ts +57 -0
- package/src/api/timelineSource.test.ts +217 -0
- package/src/api/timelineSource.ts +582 -0
- package/src/components/ConnectionErrorView.tsx +186 -70
- package/src/components/ContextSwitcher.tsx +63 -18
- package/src/components/DebugOverlay.tsx +5 -3
- package/src/components/NamespaceSwitcher.tsx +41 -0
- package/src/components/UserMenu.tsx +69 -21
- package/src/components/applications/ApplicationsView.tsx +936 -0
- package/src/components/audit/AuditSettingsDialog.tsx +79 -17
- package/src/components/audit/AuditView.tsx +65 -62
- package/src/components/compare/CompareViewRoute.tsx +124 -0
- package/src/components/compare/useCompareCandidates.ts +27 -0
- package/src/components/compare/useCompareLauncher.tsx +79 -0
- package/src/components/cost/ApplicationCostTab.test.ts +204 -0
- package/src/components/cost/ApplicationCostTab.tsx +571 -0
- package/src/components/cost/CostTrendChart.tsx +106 -75
- package/src/components/cost/CostView.test.ts +12 -0
- package/src/components/cost/CostView.tsx +507 -223
- package/src/components/cost/CostViewTabs.test.tsx +21 -0
- package/src/components/cost/CostViewTabs.tsx +40 -0
- package/src/components/cost/CurrentAllocationUse.test.ts +21 -0
- package/src/components/cost/CurrentAllocationUse.tsx +126 -0
- package/src/components/cost/WorkloadCostTab.test.ts +153 -0
- package/src/components/cost/WorkloadCostTab.tsx +372 -0
- package/src/components/cost/cloud-console.test.ts +39 -0
- package/src/components/cost/cloud-console.ts +81 -0
- package/src/components/cost/errors.ts +8 -0
- package/src/components/cost/format.test.ts +27 -0
- package/src/components/cost/format.ts +46 -0
- package/src/components/cost/kinds.ts +5 -0
- package/src/components/curl/ServiceCurlButton.tsx +445 -0
- package/src/components/diagnose/AISettings.tsx +147 -0
- package/src/components/diagnose/DiagnoseContext.tsx +495 -0
- package/src/components/diagnose/DiagnoseSurface.tsx +394 -0
- package/src/components/diagnose/Home.tsx +163 -0
- package/src/components/diagnose/InvestigationView.tsx +622 -0
- package/src/components/diagnose/LocalDiagnoseAction.tsx +162 -0
- package/src/components/diagnose/launch.ts +65 -0
- package/src/components/diagnose/parts.tsx +1756 -0
- package/src/components/dock/BottomDock.tsx +2 -3
- package/src/components/dock/DockContext.tsx +1 -0
- package/src/components/dock/TerminalTab.tsx +1 -1
- package/src/components/dock/WorkloadLogsTab.tsx +21 -5
- package/src/components/dock/index.ts +1 -1
- package/src/components/execution/BatchExecutionView.test.ts +170 -0
- package/src/components/execution/BatchExecutionView.tsx +1329 -0
- package/src/components/execution/batch-run-actions.test.ts +48 -0
- package/src/components/execution/batch-run-actions.ts +24 -0
- package/src/components/execution/batch-timeline.test.ts +57 -0
- package/src/components/execution/batch-timeline.ts +46 -0
- package/src/components/execution/execution-definition.test.ts +208 -0
- package/src/components/execution/execution-definition.ts +245 -0
- package/src/components/gitops/ArgoResourceDiffLoader.tsx +23 -0
- package/src/components/gitops/GitOpsView.tsx +1042 -0
- package/src/components/gitops/RevisionMetaChip.tsx +63 -0
- package/src/components/helm/ChartBrowser.tsx +87 -31
- package/src/components/helm/HelmCompareRoute.tsx +1341 -0
- package/src/components/helm/HelmReleaseDrawer.test.ts +17 -0
- package/src/components/helm/HelmReleaseDrawer.tsx +1073 -102
- package/src/components/helm/HelmView.tsx +237 -96
- package/src/components/helm/InstallWizard.tsx +94 -38
- package/src/components/helm/ManifestDiffViewer.tsx +8 -27
- package/src/components/helm/OwnedResources.tsx +34 -59
- package/src/components/helm/RevisionHistory.tsx +52 -3
- package/src/components/helm/RoleGatedPanel.tsx +3 -3
- package/src/components/helm/TrackChartSourceDialog.tsx +185 -0
- package/src/components/helm/ValuesDiffPreview.tsx +17 -7
- package/src/components/helm/ValuesViewer.tsx +49 -53
- package/src/components/helm/helm-utils.ts +4 -0
- package/src/components/home/ActivitySummary.tsx +4 -1
- package/src/components/home/ClusterHealthCard.tsx +56 -42
- package/src/components/home/CostCard.tsx +21 -36
- package/src/components/home/GitOpsControllersCard.tsx +110 -0
- package/src/components/home/HelmSummary.tsx +3 -1
- package/src/components/home/HomeView.tsx +339 -105
- package/src/components/home/MCPSetupDialog.tsx +29 -87
- package/src/components/home/TrafficSummary.tsx +2 -2
- package/src/components/home/mcpToolCatalog.ts +333 -0
- package/src/components/issues/IssuesPane.tsx +151 -0
- package/src/components/logs/LogsViewer.tsx +4 -1
- package/src/components/logs/ScheduledWorkloadLogsViewer.tsx +135 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +4 -1
- package/src/components/nav/PrimaryNavRail.tsx +285 -0
- package/src/components/portforward/PortForwardButton.tsx +118 -47
- package/src/components/portforward/PortForwardManager.tsx +253 -131
- package/src/components/resource/HPACharts.tsx +237 -0
- package/src/components/resource/PVCUsageBar.tsx +59 -0
- package/src/components/resource/PrometheusCharts.tsx +160 -584
- package/src/components/resource/PrometheusChartsGrid.tsx +270 -0
- package/src/components/resource/RestartChart.tsx +133 -0
- package/src/components/resource/RightsizingStrip.test.ts +109 -0
- package/src/components/resource/RightsizingStrip.tsx +363 -0
- package/src/components/resource-drawer/ResourceDrawer.tsx +3 -1
- package/src/components/resources/CompositeRenderer.tsx +101 -0
- package/src/components/resources/ImageFilesystemModal.tsx +19 -12
- package/src/components/resources/PodFilesystemModal.tsx +6 -5
- package/src/components/resources/ResourceDetailDrawer.tsx +13 -3
- package/src/components/resources/ResourcesView.tsx +194 -17
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +1 -0
- package/src/components/resources/renderers/HPARenderer.tsx +20 -1
- package/src/components/resources/renderers/NamespaceRenderer.tsx +31 -0
- package/src/components/resources/renderers/NodeRenderer.tsx +10 -4
- package/src/components/resources/renderers/PVCRenderer.tsx +19 -1
- package/src/components/resources/renderers/PodRenderer.tsx +30 -6
- package/src/components/resources/renderers/RoleBindingRenderer.tsx +45 -1
- package/src/components/resources/renderers/RoleRenderer.tsx +27 -1
- package/src/components/resources/renderers/ServiceAccountRenderer.tsx +28 -1
- package/src/components/resources/renderers/ServiceRenderer.tsx +81 -8
- package/src/components/resources/renderers/WorkloadRenderer.tsx +51 -4
- package/src/components/resources/renderers/index.ts +2 -0
- package/src/components/resources/resource-utils.ts +2 -1
- package/src/components/rightsizing/RightsizingScanView.tsx +938 -0
- package/src/components/rightsizing/copy.test.ts +56 -0
- package/src/components/rightsizing/model.test.ts +227 -0
- package/src/components/rightsizing/model.ts +158 -0
- package/src/components/rightsizing/presentation.test.ts +104 -0
- package/src/components/rightsizing/presentation.ts +94 -0
- package/src/components/settings/MyPermissionsDialog.tsx +241 -0
- package/src/components/settings/SettingsDialog.tsx +1505 -165
- package/src/components/shared/CreateResourceDialog.tsx +9 -2
- package/src/components/shared/LargeClusterNamespacePicker.tsx +3 -3
- package/src/components/timeline/LocalTimelineScrubber.tsx +212 -0
- package/src/components/timeline/RetainedTimelineScrubber.tsx +311 -0
- package/src/components/timeline/TimelineList.tsx +86 -13
- package/src/components/timeline/TimelineSwimlanes.tsx +9 -1299
- package/src/components/timeline/TimelineView.tsx +873 -24
- package/src/components/timeline/TimelineView.urlparams.test.ts +335 -0
- package/src/components/traffic/TrafficFilterSidebar.tsx +10 -45
- package/src/components/traffic/TrafficFlowList.tsx +29 -15
- package/src/components/traffic/TrafficGraph.tsx +42 -24
- package/src/components/traffic/TrafficView.tsx +32 -19
- package/src/components/ui/CommandPalette.tsx +8 -215
- package/src/components/ui/DiagnosticsOverlay.tsx +219 -9
- package/src/components/ui/Markdown.tsx +3 -3
- package/src/components/ui/Omnibar.tsx +602 -0
- package/src/components/ui/RadarOmnibar.tsx +52 -0
- package/src/components/ui/SearchSyntaxHelp.tsx +89 -0
- package/src/components/ui/ShortcutHelpOverlay.tsx +3 -2
- package/src/components/ui/UpdateNotification.tsx +48 -36
- package/src/components/ui/command-items.ts +178 -0
- package/src/components/workload/WorkloadView.tsx +1342 -158
- package/src/context/ConnectionContext.tsx +146 -21
- package/src/context/DiagnoseCustomization.tsx +93 -0
- package/src/context/NavCustomization.tsx +75 -0
- package/src/context/TimelineSource.tsx +50 -0
- package/src/contexts/CapabilitiesContext.tsx +32 -8
- package/src/filter/FilterLocationBridge.tsx +30 -0
- package/src/hooks/useClusterLoadState.ts +73 -0
- package/src/hooks/useDocumentTitle.ts +25 -0
- package/src/hooks/useEventSource.ts +6 -0
- package/src/hooks/useKeyboardShortcuts.tsx +1 -0
- package/src/hooks/useMediaQuery.ts +21 -0
- package/src/hooks/useNavRailPinned.ts +46 -0
- package/src/hooks/useRecentResources.ts +49 -0
- package/src/index.css +162 -1
- package/src/index.ts +73 -1
- package/src/main.tsx +7 -5
- package/src/types/clusterLoadState.ts +33 -0
- package/src/types.ts +2 -0
- package/src/utils/auditBadges.ts +53 -0
- package/src/utils/navigation.ts +64 -1
- package/src/components/ui/NamespaceSelector.tsx +0 -436
|
@@ -1,25 +1,30 @@
|
|
|
1
1
|
import { useState, useCallback, useEffect, useRef } from 'react'
|
|
2
2
|
import { flushSync } from 'react-dom'
|
|
3
|
-
import {
|
|
3
|
+
import { FetchResult, useDockReservedHeight, compareVersions } from '@skyhook-io/k8s-ui'
|
|
4
|
+
import { startViewTransitionSafe } from '@skyhook-io/k8s-ui/utils/view-transition'
|
|
4
5
|
import { TRANSITION_DRAWER } from '../../utils/animation'
|
|
5
6
|
import { useRefreshAnimation } from '../../hooks/useRefreshAnimation'
|
|
6
|
-
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, ChevronRight, SlidersHorizontal, Eye, Loader2 } from 'lucide-react'
|
|
8
|
+
import yaml from 'yaml'
|
|
9
|
+
import { useNavigate } from 'react-router-dom'
|
|
7
10
|
import { clsx } from 'clsx'
|
|
8
|
-
import { useHelmRelease, useHelmManifest, useHelmValues,
|
|
11
|
+
import { useHelmRelease, useHelmManifest, useHelmValues, useHelmUpgradeInfo, useHelmReleaseVersions, useHelmUninstall, upgradeWithProgress, rollbackWithProgress, useHelmPreviewValues } from '../../api/client'
|
|
12
|
+
import { ValuesDiffPreview } from './ValuesDiffPreview'
|
|
9
13
|
import { useQueryClient } from '@tanstack/react-query'
|
|
10
14
|
import { ConfirmDialog } from '../ui/ConfirmDialog'
|
|
15
|
+
import { Tooltip } from '../ui/Tooltip'
|
|
11
16
|
import { Markdown } from '../ui/Markdown'
|
|
12
|
-
import type { SelectedHelmRelease, HelmHook, ChartDependency } from '../../types'
|
|
13
|
-
import type
|
|
17
|
+
import type { SelectedHelmRelease, HelmHook, ChartDependency, HelmOperation, HelmOperationInsight, HelmOwnedResource, HookDiagnostic, HookLogEvidence, UpgradeInfo, ValuesPreviewResponse } from '../../types'
|
|
18
|
+
import { apiVersionToGroup, kindToPlural, type NavigateToResource } from '../../utils/navigation'
|
|
14
19
|
import { formatDate } from './helm-utils'
|
|
15
|
-
import { getHelmStatusColor, SEVERITY_BADGE, SEVERITY_TEXT } from '../../utils/badge-colors'
|
|
20
|
+
import { getHelmStatusColor, getKindBadgeColor, getResourceStatusColor, SEVERITY_BADGE, SEVERITY_TEXT } from '../../utils/badge-colors'
|
|
16
21
|
import { useCanHelmAct, useCloudRole } from '../../api/client'
|
|
17
22
|
import { RoleGatedPanel } from './RoleGatedPanel'
|
|
18
23
|
import { RevisionHistory } from './RevisionHistory'
|
|
19
24
|
import { ManifestViewer } from './ManifestViewer'
|
|
20
25
|
import { ValuesViewer } from './ValuesViewer'
|
|
21
26
|
import { OwnedResources } from './OwnedResources'
|
|
22
|
-
import {
|
|
27
|
+
import { TrackChartSourceDialog } from './TrackChartSourceDialog'
|
|
23
28
|
|
|
24
29
|
interface HelmReleaseDrawerProps {
|
|
25
30
|
release: SelectedHelmRelease
|
|
@@ -27,27 +32,102 @@ interface HelmReleaseDrawerProps {
|
|
|
27
32
|
onNavigateToResource?: NavigateToResource
|
|
28
33
|
/** Controls slide-in/out animation (driven by useAnimatedUnmount) */
|
|
29
34
|
isOpen?: boolean
|
|
35
|
+
/** Right inset in px so the drawer sits beside a docked side panel (AI), not under it. */
|
|
36
|
+
rightInset?: number
|
|
30
37
|
}
|
|
31
38
|
|
|
32
|
-
type TabId = 'overview' | 'history' | 'manifest' | 'values' | 'resources' | 'hooks'
|
|
39
|
+
type TabId = 'overview' | 'history' | 'manifest' | 'values' | 'resources' | 'hooks'
|
|
40
|
+
|
|
41
|
+
interface UpgradePreviewRequest {
|
|
42
|
+
targetVersion: string
|
|
43
|
+
values: Record<string, unknown>
|
|
44
|
+
repositoryName?: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface ParsedUpgradeValues {
|
|
48
|
+
values: Record<string, unknown> | null
|
|
49
|
+
error: string | null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type UpgradeSourceIssue = NonNullable<UpgradeInfo['sourceIssue']>
|
|
53
|
+
type ActionableUpgradeSourceIssue = Exclude<UpgradeSourceIssue, 'ambiguous_repository'>
|
|
54
|
+
|
|
55
|
+
function getUpgradeSourceIssue(upgradeInfo: UpgradeInfo): UpgradeSourceIssue | undefined {
|
|
56
|
+
return upgradeInfo.sourceIssue
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getUpgradeSourceIssueLabel(issue: UpgradeSourceIssue) {
|
|
60
|
+
switch (issue) {
|
|
61
|
+
case 'repo_index_error':
|
|
62
|
+
return 'upgrade source blocked'
|
|
63
|
+
case 'ambiguous_repository':
|
|
64
|
+
return 'upgrade source ambiguous'
|
|
65
|
+
case 'untracked':
|
|
66
|
+
return 'upgrade source not tracked'
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getUpgradeSourceIssueTooltip(issue: UpgradeSourceIssue, error: string | undefined, canHelmWrite: boolean, disabledReason: string | undefined) {
|
|
71
|
+
if (!canHelmWrite) {
|
|
72
|
+
return disabledReason ?? error ?? 'Helm actions are disabled.'
|
|
73
|
+
}
|
|
74
|
+
switch (issue) {
|
|
75
|
+
case 'repo_index_error':
|
|
76
|
+
return 'A configured Helm repo index failed. Fix or refresh that repo, or register an OCI prefix if this chart came from OCI.'
|
|
77
|
+
case 'ambiguous_repository':
|
|
78
|
+
return 'Multiple configured Helm repos match this chart. Fix the repo list or source metadata so Radar can identify one source.'
|
|
79
|
+
case 'untracked':
|
|
80
|
+
return "Radar can't tell where this chart was installed from. Register an OCI chart source to track upgrades."
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function parseUpgradeValuesYaml(raw: string): ParsedUpgradeValues {
|
|
85
|
+
try {
|
|
86
|
+
const parsed = yaml.parse(raw)
|
|
87
|
+
if (parsed === null || parsed === undefined) return { values: {}, error: null }
|
|
88
|
+
if (typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
89
|
+
return { values: null, error: 'Values must be a YAML mapping (key: value), not a list or single value' }
|
|
90
|
+
}
|
|
91
|
+
return { values: parsed as Record<string, unknown>, error: null }
|
|
92
|
+
} catch (err) {
|
|
93
|
+
return { values: null, error: err instanceof Error ? err.message : 'Invalid YAML' }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function isUpgradeSourceIssueActionable(issue: UpgradeInfo['sourceIssue']): issue is ActionableUpgradeSourceIssue {
|
|
98
|
+
return Boolean(issue && issue !== 'ambiguous_repository')
|
|
99
|
+
}
|
|
33
100
|
|
|
34
101
|
const MIN_WIDTH = 500
|
|
35
102
|
const MAX_WIDTH_PERCENT = 0.8
|
|
36
103
|
const DEFAULT_WIDTH = 1000
|
|
37
104
|
|
|
38
|
-
export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOpen = true }: HelmReleaseDrawerProps) {
|
|
105
|
+
export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOpen = true, rightInset = 0 }: HelmReleaseDrawerProps) {
|
|
106
|
+
const navigate = useNavigate()
|
|
39
107
|
const [activeTab, setActiveTab] = useState<TabId>('overview')
|
|
40
108
|
const [copied, setCopied] = useState<string | null>(null)
|
|
41
109
|
const [drawerWidth, setDrawerWidth] = useState(DEFAULT_WIDTH)
|
|
110
|
+
const [viewportWidth, setViewportWidth] = useState(() => (typeof window === 'undefined' ? DEFAULT_WIDTH : window.innerWidth))
|
|
42
111
|
const [isResizing, setIsResizing] = useState(false)
|
|
43
112
|
const [selectedRevision, setSelectedRevision] = useState<number | undefined>(undefined)
|
|
44
113
|
const [showAllValues, setShowAllValues] = useState(false)
|
|
45
|
-
const [diffRevisions, setDiffRevisions] = useState<{ rev1: number; rev2: number } | null>(null)
|
|
46
114
|
const [rollbackRevision, setRollbackRevision] = useState<number | null>(null)
|
|
47
115
|
const [showUninstallConfirm, setShowUninstallConfirm] = useState(false)
|
|
48
116
|
const [showUpgradeConfirm, setShowUpgradeConfirm] = useState(false)
|
|
117
|
+
const [showTrackSource, setShowTrackSource] = useState(false)
|
|
118
|
+
const [selectedVersion, setSelectedVersion] = useState<string | null>(null)
|
|
119
|
+
const [adjustValues, setAdjustValues] = useState(false)
|
|
120
|
+
const [renderAdjustValues, setRenderAdjustValues] = useState(false)
|
|
121
|
+
const [editedUpgradeYaml, setEditedUpgradeYaml] = useState('')
|
|
122
|
+
const [upgradeYamlError, setUpgradeYamlError] = useState<string | null>(null)
|
|
123
|
+
const [upgradeValuesSeeded, setUpgradeValuesSeeded] = useState(false)
|
|
124
|
+
const [upgradePreview, setUpgradePreview] = useState<ValuesPreviewResponse | null>(null)
|
|
125
|
+
const [upgradePreviewRequest, setUpgradePreviewRequest] = useState<UpgradePreviewRequest | null>(null)
|
|
126
|
+
const [showUpgradePreview, setShowUpgradePreview] = useState(false)
|
|
49
127
|
const resizeStartX = useRef(0)
|
|
50
128
|
const resizeStartWidth = useRef(DEFAULT_WIDTH)
|
|
129
|
+
const targetVersionRef = useRef('')
|
|
130
|
+
const editedUpgradeYamlRef = useRef('')
|
|
51
131
|
const { allowed: canHelmWrite, reason: helmActReason } = useCanHelmAct()
|
|
52
132
|
// Cloud viewers can't view release manifests / values / diffs
|
|
53
133
|
// (backend gate at requireCloudRole('member')). Skip the queries
|
|
@@ -55,16 +135,17 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
55
135
|
// transient error state under the role-gated panel.
|
|
56
136
|
const { canAtLeast } = useCloudRole()
|
|
57
137
|
const canViewSensitive = canAtLeast('member')
|
|
138
|
+
const helmNamespace = release.storageNamespace || release.namespace
|
|
58
139
|
|
|
59
|
-
const { data: releaseDetail, isLoading, refetch: refetchRelease } = useHelmRelease(
|
|
60
|
-
|
|
140
|
+
const { data: releaseDetail, isLoading, error: releaseError, refetch: refetchRelease } = useHelmRelease(
|
|
141
|
+
helmNamespace,
|
|
61
142
|
release.name
|
|
62
143
|
)
|
|
63
144
|
const [refetch, isRefreshAnimating] = useRefreshAnimation(refetchRelease)
|
|
64
145
|
|
|
65
146
|
// Fetch manifest for selected revision (or latest)
|
|
66
147
|
const { data: manifest, isLoading: manifestLoading } = useHelmManifest(
|
|
67
|
-
|
|
148
|
+
helmNamespace,
|
|
68
149
|
release.name,
|
|
69
150
|
selectedRevision,
|
|
70
151
|
canViewSensitive,
|
|
@@ -72,35 +153,61 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
72
153
|
|
|
73
154
|
// Fetch values
|
|
74
155
|
const { data: values, isLoading: valuesLoading } = useHelmValues(
|
|
75
|
-
|
|
156
|
+
helmNamespace,
|
|
76
157
|
release.name,
|
|
77
158
|
showAllValues,
|
|
78
159
|
canViewSensitive,
|
|
160
|
+
selectedRevision,
|
|
79
161
|
)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
162
|
+
const {
|
|
163
|
+
data: upgradeValues,
|
|
164
|
+
isLoading: upgradeValuesLoading,
|
|
165
|
+
error: upgradeValuesError,
|
|
166
|
+
} = useHelmValues(
|
|
167
|
+
helmNamespace,
|
|
84
168
|
release.name,
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
canViewSensitive,
|
|
169
|
+
false,
|
|
170
|
+
canViewSensitive && showUpgradeConfirm && adjustValues,
|
|
88
171
|
)
|
|
89
172
|
|
|
90
173
|
// Lazy check for upgrade availability
|
|
91
|
-
const { data: upgradeInfo, isLoading: upgradeLoading } = useHelmUpgradeInfo(
|
|
92
|
-
|
|
174
|
+
const { data: upgradeInfo, isLoading: upgradeLoading, error: upgradeError } = useHelmUpgradeInfo(
|
|
175
|
+
helmNamespace,
|
|
93
176
|
release.name
|
|
94
177
|
)
|
|
178
|
+
const upgradeErrorMessage = upgradeError instanceof Error ? upgradeError.message : 'Upgrade check failed'
|
|
179
|
+
const upgradeSourceIssue = upgradeInfo ? getUpgradeSourceIssue(upgradeInfo) : undefined
|
|
180
|
+
|
|
181
|
+
// Available versions for the upgrade dialog's picker — only fetched while the
|
|
182
|
+
// confirm dialog is open. Default the selection to latest when it opens.
|
|
183
|
+
const {
|
|
184
|
+
data: availableVersions,
|
|
185
|
+
isLoading: availableVersionsLoading,
|
|
186
|
+
error: availableVersionsError,
|
|
187
|
+
} = useHelmReleaseVersions(helmNamespace, release.name, showUpgradeConfirm)
|
|
188
|
+
const targetVersion = selectedVersion ?? upgradeInfo?.latestVersion ?? ''
|
|
189
|
+
const selectableVersions = availableVersions && availableVersions.length > 1 ? availableVersions : null
|
|
190
|
+
// Semver compare, not list-position: the installed version may be older than
|
|
191
|
+
// the newest-N versions the picker shows, so it isn't always in the list.
|
|
192
|
+
const isDowngrade = Boolean(
|
|
193
|
+
targetVersion && upgradeInfo?.currentVersion &&
|
|
194
|
+
compareVersions(targetVersion, upgradeInfo.currentVersion) === -1
|
|
195
|
+
)
|
|
95
196
|
|
|
96
197
|
// Mutations for actions
|
|
97
198
|
const uninstallMutation = useHelmUninstall()
|
|
199
|
+
const upgradePreviewMutation = useHelmPreviewValues()
|
|
200
|
+
const isPreviewingUpgrade = upgradePreviewMutation.isPending
|
|
98
201
|
const queryClient = useQueryClient()
|
|
99
202
|
const [upgradeProgress, setUpgradeProgress] = useState<{ phase: string; message: string }[]>([])
|
|
100
203
|
const [isUpgrading, setIsUpgrading] = useState(false)
|
|
101
204
|
const [rollbackProgress, setRollbackProgress] = useState<{ phase: string; message: string }[]>([])
|
|
102
205
|
const [isRollingBack, setIsRollingBack] = useState(false)
|
|
103
206
|
|
|
207
|
+
useEffect(() => {
|
|
208
|
+
targetVersionRef.current = targetVersion
|
|
209
|
+
}, [targetVersion])
|
|
210
|
+
|
|
104
211
|
// ESC key handler
|
|
105
212
|
useEffect(() => {
|
|
106
213
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
@@ -110,13 +217,24 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
110
217
|
return () => document.removeEventListener('keydown', handleKeyDown)
|
|
111
218
|
}, [onClose])
|
|
112
219
|
|
|
220
|
+
useEffect(() => {
|
|
221
|
+
const handleWindowResize = () => setViewportWidth(window.innerWidth)
|
|
222
|
+
handleWindowResize()
|
|
223
|
+
window.addEventListener('resize', handleWindowResize)
|
|
224
|
+
return () => window.removeEventListener('resize', handleWindowResize)
|
|
225
|
+
}, [])
|
|
226
|
+
|
|
227
|
+
const minDrawerWidth = Math.min(MIN_WIDTH, viewportWidth)
|
|
228
|
+
const maxDrawerWidth = viewportWidth < MIN_WIDTH ? viewportWidth : Math.max(MIN_WIDTH, viewportWidth * MAX_WIDTH_PERCENT)
|
|
229
|
+
const renderedDrawerWidth = Math.max(minDrawerWidth, Math.min(drawerWidth, maxDrawerWidth))
|
|
230
|
+
|
|
113
231
|
// Resize handlers
|
|
114
232
|
const handleResizeStart = useCallback((e: React.MouseEvent) => {
|
|
115
233
|
e.preventDefault()
|
|
116
234
|
setIsResizing(true)
|
|
117
235
|
resizeStartX.current = e.clientX
|
|
118
|
-
resizeStartWidth.current =
|
|
119
|
-
}, [
|
|
236
|
+
resizeStartWidth.current = renderedDrawerWidth
|
|
237
|
+
}, [renderedDrawerWidth])
|
|
120
238
|
|
|
121
239
|
useEffect(() => {
|
|
122
240
|
if (!isResizing) return
|
|
@@ -124,11 +242,13 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
124
242
|
document.body.style.cursor = 'ew-resize'
|
|
125
243
|
document.body.style.userSelect = 'none'
|
|
126
244
|
|
|
127
|
-
const maxWidth = window.innerWidth * MAX_WIDTH_PERCENT
|
|
128
245
|
const handleMouseMove = (e: MouseEvent) => {
|
|
129
246
|
const deltaX = resizeStartX.current - e.clientX
|
|
130
247
|
const newWidth = resizeStartWidth.current + deltaX
|
|
131
|
-
|
|
248
|
+
const viewport = window.innerWidth
|
|
249
|
+
const minWidth = Math.min(MIN_WIDTH, viewport)
|
|
250
|
+
const maxWidth = viewport < MIN_WIDTH ? viewport : Math.max(MIN_WIDTH, viewport * MAX_WIDTH_PERCENT)
|
|
251
|
+
setDrawerWidth(Math.max(minWidth, Math.min(newWidth, maxWidth)))
|
|
132
252
|
}
|
|
133
253
|
const handleMouseUp = () => setIsResizing(false)
|
|
134
254
|
document.addEventListener('mousemove', handleMouseMove)
|
|
@@ -148,18 +268,22 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
148
268
|
}, [])
|
|
149
269
|
|
|
150
270
|
const switchTab = useCallback((tab: TabId) => {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
setActiveTab(tab)
|
|
156
|
-
}
|
|
271
|
+
// Swallow the InvalidStateError the API rejects with on rapid
|
|
272
|
+
// tab clicks (SKY-833 bug 49); fall back synchronously when the
|
|
273
|
+
// API isn't available.
|
|
274
|
+
startViewTransitionSafe(() => flushSync(() => setActiveTab(tab)))
|
|
157
275
|
}, [])
|
|
158
276
|
|
|
159
|
-
const handleCompareRevisions = (rev1: number, rev2: number) => {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
277
|
+
const handleCompareRevisions = useCallback((rev1: number, rev2: number) => {
|
|
278
|
+
const params = new URLSearchParams()
|
|
279
|
+
const globalNamespaces = new URLSearchParams(window.location.search).get('namespaces')
|
|
280
|
+
if (globalNamespaces) params.set('namespaces', globalNamespaces)
|
|
281
|
+
params.set('release', `${release.namespace}/${release.name}`)
|
|
282
|
+
if (release.storageNamespace) params.set('releaseStorage', release.storageNamespace)
|
|
283
|
+
params.set('revision1', String(rev1))
|
|
284
|
+
params.set('revision2', String(rev2))
|
|
285
|
+
navigate({ pathname: '/helm/compare', search: params.toString() })
|
|
286
|
+
}, [navigate, release.name, release.namespace, release.storageNamespace])
|
|
163
287
|
|
|
164
288
|
const handleViewRevision = (revision: number) => {
|
|
165
289
|
setSelectedRevision(revision)
|
|
@@ -177,7 +301,7 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
177
301
|
|
|
178
302
|
try {
|
|
179
303
|
await rollbackWithProgress(
|
|
180
|
-
|
|
304
|
+
helmNamespace,
|
|
181
305
|
release.name,
|
|
182
306
|
rollbackRevision,
|
|
183
307
|
(event) => {
|
|
@@ -196,7 +320,7 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
196
320
|
}])
|
|
197
321
|
|
|
198
322
|
queryClient.invalidateQueries({ queryKey: ['helm-releases'] })
|
|
199
|
-
queryClient.invalidateQueries({ queryKey: ['helm-release',
|
|
323
|
+
queryClient.invalidateQueries({ queryKey: ['helm-release', helmNamespace, release.name] })
|
|
200
324
|
|
|
201
325
|
setTimeout(() => {
|
|
202
326
|
setRollbackRevision(null)
|
|
@@ -216,7 +340,7 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
216
340
|
|
|
217
341
|
const handleUninstallConfirm = () => {
|
|
218
342
|
uninstallMutation.mutate(
|
|
219
|
-
{ namespace:
|
|
343
|
+
{ namespace: helmNamespace, name: release.name },
|
|
220
344
|
{
|
|
221
345
|
onSuccess: () => {
|
|
222
346
|
setShowUninstallConfirm(false)
|
|
@@ -229,16 +353,99 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
229
353
|
)
|
|
230
354
|
}
|
|
231
355
|
|
|
232
|
-
const
|
|
233
|
-
|
|
356
|
+
const resetUpgradeDialog = () => {
|
|
357
|
+
setUpgradeProgress([])
|
|
358
|
+
setSelectedVersion(null)
|
|
359
|
+
setAdjustValues(false)
|
|
360
|
+
setRenderAdjustValues(false)
|
|
361
|
+
setEditedUpgradeYaml('')
|
|
362
|
+
editedUpgradeYamlRef.current = ''
|
|
363
|
+
setUpgradeYamlError(null)
|
|
364
|
+
setUpgradeValuesSeeded(false)
|
|
365
|
+
setUpgradePreview(null)
|
|
366
|
+
setUpgradePreviewRequest(null)
|
|
367
|
+
setShowUpgradePreview(false)
|
|
368
|
+
upgradePreviewMutation.reset()
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
useEffect(() => {
|
|
372
|
+
if (!adjustValues || !upgradeValues || upgradeValuesSeeded) return
|
|
373
|
+
const nextYaml = userSuppliedToYaml(upgradeValues.userSupplied)
|
|
374
|
+
editedUpgradeYamlRef.current = nextYaml
|
|
375
|
+
setEditedUpgradeYaml(nextYaml)
|
|
376
|
+
setUpgradeYamlError(null)
|
|
377
|
+
setUpgradeValuesSeeded(true)
|
|
378
|
+
}, [adjustValues, upgradeValues, upgradeValuesSeeded])
|
|
379
|
+
|
|
380
|
+
const handleToggleAdjustValues = () => {
|
|
381
|
+
if (adjustValues) {
|
|
382
|
+
setAdjustValues(false)
|
|
383
|
+
return
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
setRenderAdjustValues(true)
|
|
387
|
+
setEditedUpgradeYaml('')
|
|
388
|
+
editedUpgradeYamlRef.current = ''
|
|
389
|
+
setUpgradeYamlError(null)
|
|
390
|
+
setUpgradeValuesSeeded(false)
|
|
391
|
+
upgradePreviewMutation.reset()
|
|
392
|
+
setAdjustValues(true)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const getEditedUpgradeValues = () => {
|
|
396
|
+
if (!adjustValues) return {}
|
|
397
|
+
const parsed = parseUpgradeValuesYaml(editedUpgradeYamlRef.current)
|
|
398
|
+
setUpgradeYamlError(parsed.error)
|
|
399
|
+
return parsed.values
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const handleUpgradePreview = async () => {
|
|
403
|
+
if (!targetVersion || upgradeValuesLoading || upgradeValuesError || !upgradeValuesSeeded) return
|
|
404
|
+
const editedValues = getEditedUpgradeValues()
|
|
405
|
+
if (!editedValues) return
|
|
406
|
+
const previewYaml = editedUpgradeYamlRef.current
|
|
407
|
+
const previewRequest: UpgradePreviewRequest = {
|
|
408
|
+
targetVersion,
|
|
409
|
+
values: editedValues,
|
|
410
|
+
repositoryName: upgradeInfo?.repositoryName,
|
|
411
|
+
}
|
|
412
|
+
try {
|
|
413
|
+
const result = await upgradePreviewMutation.mutateAsync({
|
|
414
|
+
namespace: helmNamespace,
|
|
415
|
+
name: release.name,
|
|
416
|
+
values: previewRequest.values,
|
|
417
|
+
version: previewRequest.targetVersion,
|
|
418
|
+
repository: previewRequest.repositoryName,
|
|
419
|
+
})
|
|
420
|
+
if (targetVersionRef.current !== previewRequest.targetVersion || editedUpgradeYamlRef.current !== previewYaml) return
|
|
421
|
+
setUpgradePreview(result)
|
|
422
|
+
setUpgradePreviewRequest(previewRequest)
|
|
423
|
+
setShowUpgradePreview(true)
|
|
424
|
+
} catch {
|
|
425
|
+
return
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const runUpgrade = async (versionToApply: string, editedValuesOverride: Record<string, unknown> | undefined, repositoryName: string | undefined) => {
|
|
430
|
+
if (!versionToApply) return
|
|
431
|
+
|
|
432
|
+
let editedValues = editedValuesOverride
|
|
433
|
+
if (editedValues === undefined && adjustValues) {
|
|
434
|
+
if (upgradeValuesLoading || upgradeValuesError || !upgradeValuesSeeded) return
|
|
435
|
+
editedValues = getEditedUpgradeValues() ?? undefined
|
|
436
|
+
if (!editedValues) return
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
setShowUpgradePreview(false)
|
|
234
440
|
setIsUpgrading(true)
|
|
235
441
|
setUpgradeProgress([])
|
|
236
442
|
|
|
237
443
|
try {
|
|
238
444
|
await upgradeWithProgress(
|
|
239
|
-
|
|
445
|
+
helmNamespace,
|
|
240
446
|
release.name,
|
|
241
|
-
|
|
447
|
+
versionToApply,
|
|
448
|
+
repositoryName,
|
|
242
449
|
(event) => {
|
|
243
450
|
if (event.type === 'progress' && event.message) {
|
|
244
451
|
setUpgradeProgress(prev => [...prev, {
|
|
@@ -246,23 +453,25 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
246
453
|
message: event.message || '',
|
|
247
454
|
}])
|
|
248
455
|
}
|
|
249
|
-
}
|
|
456
|
+
},
|
|
457
|
+
editedValues
|
|
250
458
|
)
|
|
251
459
|
|
|
252
460
|
setUpgradeProgress(prev => [...prev, {
|
|
253
461
|
phase: 'complete',
|
|
254
|
-
message: `Successfully upgraded to ${
|
|
462
|
+
message: `Successfully upgraded to ${versionToApply}`,
|
|
255
463
|
}])
|
|
256
464
|
|
|
257
465
|
// Invalidate queries
|
|
258
466
|
queryClient.invalidateQueries({ queryKey: ['helm-releases'] })
|
|
259
|
-
queryClient.invalidateQueries({ queryKey: ['helm-release',
|
|
260
|
-
queryClient.invalidateQueries({ queryKey: ['helm-upgrade-info',
|
|
467
|
+
queryClient.invalidateQueries({ queryKey: ['helm-release', helmNamespace, release.name] })
|
|
468
|
+
queryClient.invalidateQueries({ queryKey: ['helm-upgrade-info', helmNamespace, release.name] })
|
|
469
|
+
queryClient.invalidateQueries({ queryKey: ['helm-values', helmNamespace, release.name] })
|
|
261
470
|
queryClient.invalidateQueries({ queryKey: ['helm-batch-upgrade-info'] })
|
|
262
471
|
|
|
263
472
|
setTimeout(() => {
|
|
264
473
|
setShowUpgradeConfirm(false)
|
|
265
|
-
|
|
474
|
+
resetUpgradeDialog()
|
|
266
475
|
refetch()
|
|
267
476
|
switchTab('resources')
|
|
268
477
|
}, 1500)
|
|
@@ -276,7 +485,17 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
276
485
|
}
|
|
277
486
|
}
|
|
278
487
|
|
|
488
|
+
const handleUpgradeConfirm = () => {
|
|
489
|
+
void runUpgrade(targetVersion, undefined, upgradeInfo?.repositoryName)
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const handleApplyUpgradePreview = () => {
|
|
493
|
+
if (!upgradePreviewRequest) return
|
|
494
|
+
void runUpgrade(upgradePreviewRequest.targetVersion, upgradePreviewRequest.values, upgradePreviewRequest.repositoryName)
|
|
495
|
+
}
|
|
496
|
+
|
|
279
497
|
const headerHeight = 49
|
|
498
|
+
const dockInset = useDockReservedHeight()
|
|
280
499
|
|
|
281
500
|
const tabs: { id: TabId; label: string; icon: typeof Package }[] = [
|
|
282
501
|
{ id: 'overview', label: 'Overview', icon: Package },
|
|
@@ -287,11 +506,6 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
287
506
|
{ id: 'hooks', label: 'Hooks', icon: Anchor },
|
|
288
507
|
]
|
|
289
508
|
|
|
290
|
-
// Add diff tab only when comparing
|
|
291
|
-
if (diffRevisions) {
|
|
292
|
-
tabs.push({ id: 'diff', label: 'Diff', icon: FileText })
|
|
293
|
-
}
|
|
294
|
-
|
|
295
509
|
return (
|
|
296
510
|
<div
|
|
297
511
|
className={clsx(
|
|
@@ -299,7 +513,7 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
299
513
|
TRANSITION_DRAWER,
|
|
300
514
|
isOpen ? 'translate-x-0 opacity-100' : 'translate-x-full opacity-0'
|
|
301
515
|
)}
|
|
302
|
-
style={{ width:
|
|
516
|
+
style={{ width: renderedDrawerWidth, top: headerHeight, right: rightInset, height: `calc(100vh - ${headerHeight}px - ${dockInset}px)` }}
|
|
303
517
|
>
|
|
304
518
|
{/* Resize handle */}
|
|
305
519
|
<div
|
|
@@ -328,50 +542,112 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
328
542
|
<span className="badge bg-theme-hover/50 text-theme-text-secondary animate-pulse">
|
|
329
543
|
checking...
|
|
330
544
|
</span>
|
|
545
|
+
) : upgradeError ? (
|
|
546
|
+
<Tooltip content={upgradeErrorMessage ?? ''}>
|
|
547
|
+
<span
|
|
548
|
+
className="badge bg-theme-hover/50 text-theme-text-secondary"
|
|
549
|
+
>
|
|
550
|
+
upgrade check failed
|
|
551
|
+
</span>
|
|
552
|
+
</Tooltip>
|
|
553
|
+
) : upgradeInfo?.updateAvailable && releaseDetail?.managedByFluxHelmRelease ? (
|
|
554
|
+
// Route-only for GitOps-managed releases: a direct `helm upgrade`
|
|
555
|
+
// would be reverted at the next reconcile, so surface the available
|
|
556
|
+
// version as info and point at GitOps rather than offer the upgrade.
|
|
557
|
+
<Tooltip content={`${upgradeInfo.latestVersion} available — managed by Flux, upgrade via the GitOps view (a direct upgrade would be reverted at the next reconcile).`}>
|
|
558
|
+
<span className={clsx('badge', SEVERITY_BADGE.warning, 'opacity-90')}>
|
|
559
|
+
<ArrowUpCircle className="w-3 h-3" />
|
|
560
|
+
{upgradeInfo.latestVersion}
|
|
561
|
+
</span>
|
|
562
|
+
</Tooltip>
|
|
331
563
|
) : upgradeInfo?.updateAvailable ? (
|
|
564
|
+
<Tooltip content={canHelmWrite ? `Click to upgrade: ${upgradeInfo.currentVersion} → ${upgradeInfo.latestVersion}${upgradeInfo.repositoryName ? ` (${upgradeInfo.repositoryName})` : upgradeInfo.sourceType === 'oci' ? ' (OCI)' : ''}` : helmActReason}>
|
|
332
565
|
<button
|
|
333
566
|
onClick={() => setShowUpgradeConfirm(true)}
|
|
334
567
|
disabled={!canHelmWrite}
|
|
335
568
|
className={clsx(
|
|
336
|
-
'badge transition-colors', SEVERITY_BADGE.warning,
|
|
569
|
+
'badge transition-colors disabled:pointer-events-none', SEVERITY_BADGE.warning,
|
|
337
570
|
canHelmWrite ? 'hover:bg-amber-500/30 cursor-pointer' : 'opacity-50 cursor-not-allowed'
|
|
338
571
|
)}
|
|
339
|
-
title={canHelmWrite ? `Click to upgrade: ${upgradeInfo.currentVersion} → ${upgradeInfo.latestVersion}${upgradeInfo.repositoryName ? ` (${upgradeInfo.repositoryName})` : ''}` : helmActReason}
|
|
340
572
|
>
|
|
341
573
|
<ArrowUpCircle className="w-3 h-3" />
|
|
342
574
|
{upgradeInfo.latestVersion}
|
|
343
575
|
</button>
|
|
576
|
+
</Tooltip>
|
|
344
577
|
) : upgradeInfo && !upgradeInfo.error ? (
|
|
345
|
-
<
|
|
578
|
+
<Tooltip content="Chart is up to date">
|
|
579
|
+
<span className={clsx('badge', SEVERITY_BADGE.success)}>
|
|
346
580
|
latest
|
|
347
581
|
</span>
|
|
582
|
+
</Tooltip>
|
|
583
|
+
) : upgradeInfo?.error ? (
|
|
584
|
+
releaseDetail?.managedByFluxHelmRelease ? (
|
|
585
|
+
// Managed by Flux — the "Managed by Flux" badge routes to GitOps,
|
|
586
|
+
// where the chart source lives. Don't push a Helm source here.
|
|
587
|
+
<Tooltip content="Chart source is managed by Flux — track upgrades from the GitOps view.">
|
|
588
|
+
<span className="badge bg-theme-hover/50 text-theme-text-secondary">
|
|
589
|
+
source via GitOps
|
|
590
|
+
</span>
|
|
591
|
+
</Tooltip>
|
|
592
|
+
) : upgradeSourceIssue && !isUpgradeSourceIssueActionable(upgradeSourceIssue) ? (
|
|
593
|
+
<Tooltip content={getUpgradeSourceIssueTooltip(upgradeSourceIssue, upgradeInfo.error, canHelmWrite, helmActReason)}>
|
|
594
|
+
<span className="badge bg-theme-hover/50 text-theme-text-secondary">
|
|
595
|
+
<Link2 className="w-3 h-3" />
|
|
596
|
+
{getUpgradeSourceIssueLabel(upgradeSourceIssue)}
|
|
597
|
+
</span>
|
|
598
|
+
</Tooltip>
|
|
599
|
+
) : isUpgradeSourceIssueActionable(upgradeSourceIssue) ? (
|
|
600
|
+
<Tooltip content={getUpgradeSourceIssueTooltip(upgradeSourceIssue, upgradeInfo.error, canHelmWrite, helmActReason)}>
|
|
601
|
+
<button
|
|
602
|
+
onClick={() => canHelmWrite && setShowTrackSource(true)}
|
|
603
|
+
disabled={!canHelmWrite}
|
|
604
|
+
className={clsx(
|
|
605
|
+
'badge transition-colors disabled:pointer-events-none bg-theme-hover/50 text-theme-text-secondary',
|
|
606
|
+
canHelmWrite ? 'hover:bg-theme-hover cursor-pointer' : 'opacity-50 cursor-not-allowed'
|
|
607
|
+
)}
|
|
608
|
+
>
|
|
609
|
+
<Link2 className="w-3 h-3" />
|
|
610
|
+
{getUpgradeSourceIssueLabel(upgradeSourceIssue)}
|
|
611
|
+
</button>
|
|
612
|
+
</Tooltip>
|
|
613
|
+
) : (
|
|
614
|
+
<Tooltip content={upgradeInfo.error}>
|
|
615
|
+
<span className="badge bg-theme-hover/50 text-theme-text-secondary">
|
|
616
|
+
upgrade source unresolved
|
|
617
|
+
</span>
|
|
618
|
+
</Tooltip>
|
|
619
|
+
)
|
|
348
620
|
) : null}
|
|
349
621
|
</div>
|
|
350
622
|
<div className="flex items-center gap-1">
|
|
623
|
+
<Tooltip content="Refresh">
|
|
351
624
|
<button
|
|
352
625
|
onClick={refetch}
|
|
353
626
|
disabled={isRefreshAnimating}
|
|
354
|
-
className="p-1.5 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded disabled:opacity-50"
|
|
355
|
-
title="Refresh"
|
|
627
|
+
className="p-1.5 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded disabled:opacity-50 disabled:pointer-events-none"
|
|
356
628
|
>
|
|
357
629
|
<RefreshCw className={clsx('w-4 h-4', isRefreshAnimating && 'animate-spin')} />
|
|
358
630
|
</button>
|
|
631
|
+
</Tooltip>
|
|
632
|
+
<Tooltip content={canHelmWrite ? 'Uninstall release' : helmActReason}>
|
|
359
633
|
<button
|
|
360
634
|
onClick={() => setShowUninstallConfirm(true)}
|
|
361
635
|
disabled={!canHelmWrite}
|
|
362
636
|
className={clsx(
|
|
363
|
-
'p-1.5 rounded',
|
|
637
|
+
'p-1.5 rounded disabled:pointer-events-none',
|
|
364
638
|
canHelmWrite
|
|
365
639
|
? 'text-theme-text-secondary hover:text-red-400 hover:bg-red-500/10'
|
|
366
640
|
: 'text-theme-text-disabled cursor-not-allowed'
|
|
367
641
|
)}
|
|
368
|
-
title={canHelmWrite ? 'Uninstall release' : helmActReason}
|
|
369
642
|
>
|
|
370
643
|
<Trash2 className="w-4 h-4" />
|
|
371
644
|
</button>
|
|
372
|
-
|
|
645
|
+
</Tooltip>
|
|
646
|
+
<Tooltip content="Close (Esc)">
|
|
647
|
+
<button onClick={onClose} className="p-1.5 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded">
|
|
373
648
|
<X className="w-4 h-4" />
|
|
374
649
|
</button>
|
|
650
|
+
</Tooltip>
|
|
375
651
|
</div>
|
|
376
652
|
</div>
|
|
377
653
|
|
|
@@ -380,15 +656,31 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
380
656
|
<div className="flex items-center gap-2">
|
|
381
657
|
<Package className="w-5 h-5 text-purple-400" />
|
|
382
658
|
<h2 className="text-lg font-semibold text-theme-text-primary truncate">{release.name}</h2>
|
|
659
|
+
<Tooltip content="Copy name" wrapperClassName="shrink-0">
|
|
383
660
|
<button
|
|
384
661
|
onClick={() => copyToClipboard(release.name, 'name')}
|
|
385
662
|
className="p-1 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded shrink-0"
|
|
386
|
-
title="Copy name"
|
|
387
663
|
>
|
|
388
664
|
{copied === 'name' ? <Check className="w-3.5 h-3.5 text-green-400" /> : <Copy className="w-3.5 h-3.5" />}
|
|
389
665
|
</button>
|
|
666
|
+
</Tooltip>
|
|
390
667
|
</div>
|
|
391
668
|
<p className="text-sm text-theme-text-tertiary">{release.namespace}</p>
|
|
669
|
+
{releaseDetail?.managedByFluxHelmRelease && (
|
|
670
|
+
<Tooltip content={`Installed by Flux helm-controller via HelmRelease ${releaseDetail.managedByFluxHelmRelease}. Changes here would be reverted at the next reconcile.`} wrapperClassName="mt-1">
|
|
671
|
+
<button
|
|
672
|
+
type="button"
|
|
673
|
+
onClick={() => {
|
|
674
|
+
const [ns, name] = releaseDetail.managedByFluxHelmRelease!.split('/')
|
|
675
|
+
navigate(`/gitops/detail/helmreleases/${encodeURIComponent(ns || '_')}/${encodeURIComponent(name)}`)
|
|
676
|
+
}}
|
|
677
|
+
className="inline-flex items-center gap-1 rounded border border-amber-500/40 bg-amber-500/10 px-1.5 py-0.5 text-[11px] text-amber-700 dark:text-amber-400 hover:bg-amber-500/20 transition-colors"
|
|
678
|
+
>
|
|
679
|
+
<GitBranch className="w-3 h-3" />
|
|
680
|
+
Managed by Flux · {releaseDetail.managedByFluxHelmRelease}
|
|
681
|
+
</button>
|
|
682
|
+
</Tooltip>
|
|
683
|
+
)}
|
|
392
684
|
</div>
|
|
393
685
|
|
|
394
686
|
{/* Tabs */}
|
|
@@ -413,12 +705,20 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
413
705
|
|
|
414
706
|
{/* Content */}
|
|
415
707
|
<div className="flex-1 overflow-y-auto" style={{ viewTransitionName: 'helm-drawer-content' }}>
|
|
416
|
-
{
|
|
417
|
-
<
|
|
418
|
-
) : !releaseDetail ? (
|
|
419
|
-
<div className="flex items-center justify-center h-32 text-theme-text-tertiary">Release not found</div>
|
|
708
|
+
{!releaseDetail ? (
|
|
709
|
+
<FetchResult loading={isLoading} error={releaseError} notFoundMessage="Release not found" className="h-32" />
|
|
420
710
|
) : (
|
|
421
711
|
<>
|
|
712
|
+
<HelmOperationBanner
|
|
713
|
+
operation={releaseDetail.lastOperation}
|
|
714
|
+
operationInsight={releaseDetail.operationInsight}
|
|
715
|
+
releaseNamespace={releaseDetail.namespace}
|
|
716
|
+
managedByFluxHelmRelease={releaseDetail.managedByFluxHelmRelease}
|
|
717
|
+
hookDiagnostics={releaseDetail.hookDiagnostics}
|
|
718
|
+
canCompare={canViewSensitive}
|
|
719
|
+
onCompare={handleCompareRevisions}
|
|
720
|
+
onNavigateToResource={onNavigateToResource}
|
|
721
|
+
/>
|
|
422
722
|
{activeTab === 'overview' && (
|
|
423
723
|
<OverviewTab release={releaseDetail} onCopy={copyToClipboard} copied={copied} />
|
|
424
724
|
)}
|
|
@@ -426,6 +726,7 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
426
726
|
<RevisionHistory
|
|
427
727
|
history={releaseDetail.history}
|
|
428
728
|
currentRevision={releaseDetail.revision}
|
|
729
|
+
operations={mergeHelmOperations(releaseDetail.operations, releaseDetail.lastOperation)}
|
|
429
730
|
onViewRevision={handleViewRevision}
|
|
430
731
|
onCompare={handleCompareRevisions}
|
|
431
732
|
onRollback={canHelmWrite ? handleRollbackRequest : undefined}
|
|
@@ -451,8 +752,10 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
451
752
|
onToggleAllValues={setShowAllValues}
|
|
452
753
|
onCopy={(text) => copyToClipboard(text, 'values')}
|
|
453
754
|
copied={copied === 'values'}
|
|
454
|
-
namespace={
|
|
755
|
+
namespace={helmNamespace}
|
|
455
756
|
name={release.name}
|
|
757
|
+
revision={selectedRevision}
|
|
758
|
+
currentRevision={releaseDetail.revision}
|
|
456
759
|
onApplySuccess={() => refetch()}
|
|
457
760
|
/>
|
|
458
761
|
</RoleGatedPanel>
|
|
@@ -464,21 +767,7 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
464
767
|
/>
|
|
465
768
|
)}
|
|
466
769
|
{activeTab === 'hooks' && (
|
|
467
|
-
<HooksTab hooks={releaseDetail.hooks || []} />
|
|
468
|
-
)}
|
|
469
|
-
{activeTab === 'diff' && diffRevisions && (
|
|
470
|
-
<RoleGatedPanel min="member" feature="release manifest diffs">
|
|
471
|
-
<ManifestDiffViewer
|
|
472
|
-
diff={diffData?.diff || ''}
|
|
473
|
-
isLoading={diffLoading}
|
|
474
|
-
revision1={diffRevisions.rev1}
|
|
475
|
-
revision2={diffRevisions.rev2}
|
|
476
|
-
onClose={() => {
|
|
477
|
-
setDiffRevisions(null)
|
|
478
|
-
setActiveTab('history')
|
|
479
|
-
}}
|
|
480
|
-
/>
|
|
481
|
-
</RoleGatedPanel>
|
|
770
|
+
<HooksTab hooks={releaseDetail.hooks || []} hookDiagnostics={releaseDetail.hookDiagnostics || []} />
|
|
482
771
|
)}
|
|
483
772
|
</>
|
|
484
773
|
)}
|
|
@@ -528,7 +817,7 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
528
817
|
open={showUpgradeConfirm}
|
|
529
818
|
onClose={() => {
|
|
530
819
|
setShowUpgradeConfirm(false)
|
|
531
|
-
|
|
820
|
+
resetUpgradeDialog()
|
|
532
821
|
if (isUpgrading) {
|
|
533
822
|
// Upgrade continues server-side — switch to resources tab to monitor
|
|
534
823
|
setIsUpgrading(false)
|
|
@@ -537,22 +826,203 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
537
826
|
}}
|
|
538
827
|
onConfirm={handleUpgradeConfirm}
|
|
539
828
|
title="Upgrade Release"
|
|
540
|
-
message={`Upgrade "${release.name}" to version ${
|
|
829
|
+
message={`Upgrade "${release.name}" to version ${targetVersion}?`}
|
|
541
830
|
details={upgradeProgress.length === 0
|
|
542
|
-
? `
|
|
831
|
+
? `The chart will move from version ${upgradeInfo?.currentVersion} to ${targetVersion}. ${adjustValues ? 'Your edited values will be applied.' : 'Your existing values will be preserved.'} The change is applied to your cluster immediately.`
|
|
543
832
|
: undefined
|
|
544
833
|
}
|
|
545
|
-
confirmLabel=
|
|
834
|
+
confirmLabel={isDowngrade ? 'Downgrade' : 'Upgrade'}
|
|
546
835
|
variant="warning"
|
|
547
836
|
isLoading={isUpgrading}
|
|
837
|
+
confirmDisabled={isPreviewingUpgrade || (adjustValues && (upgradeValuesLoading || !!upgradeValuesError || !upgradeValuesSeeded || !!upgradeYamlError))}
|
|
548
838
|
isClosable
|
|
839
|
+
className="max-w-2xl"
|
|
549
840
|
>
|
|
841
|
+
{upgradeProgress.length === 0 && (
|
|
842
|
+
<div className="mb-1">
|
|
843
|
+
<label
|
|
844
|
+
id="upgrade-version-label"
|
|
845
|
+
htmlFor={selectableVersions ? 'upgrade-version' : undefined}
|
|
846
|
+
className="block text-sm font-medium text-theme-text-secondary mb-1.5"
|
|
847
|
+
>
|
|
848
|
+
Target version
|
|
849
|
+
</label>
|
|
850
|
+
{availableVersionsLoading ? (
|
|
851
|
+
<div
|
|
852
|
+
aria-labelledby="upgrade-version-label"
|
|
853
|
+
aria-live="polite"
|
|
854
|
+
className="flex h-10 items-center gap-2 rounded-lg border border-theme-border-light bg-theme-elevated px-3 text-sm text-theme-text-secondary"
|
|
855
|
+
>
|
|
856
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
857
|
+
Loading available versions...
|
|
858
|
+
</div>
|
|
859
|
+
) : selectableVersions ? (
|
|
860
|
+
<select
|
|
861
|
+
id="upgrade-version"
|
|
862
|
+
value={targetVersion}
|
|
863
|
+
onChange={(e) => {
|
|
864
|
+
targetVersionRef.current = e.target.value
|
|
865
|
+
setSelectedVersion(e.target.value)
|
|
866
|
+
}}
|
|
867
|
+
disabled={isUpgrading || isPreviewingUpgrade}
|
|
868
|
+
className="w-full px-3 py-2 bg-theme-elevated border border-theme-border-light rounded-lg text-sm text-theme-text-primary focus:outline-none focus:ring-2 focus:ring-accent disabled:opacity-50"
|
|
869
|
+
>
|
|
870
|
+
{selectableVersions.map((v) => (
|
|
871
|
+
<option key={v} value={v}>
|
|
872
|
+
{v}
|
|
873
|
+
{v === upgradeInfo?.latestVersion ? ' (latest)' : ''}
|
|
874
|
+
{v === upgradeInfo?.currentVersion ? ' (current)' : ''}
|
|
875
|
+
</option>
|
|
876
|
+
))}
|
|
877
|
+
</select>
|
|
878
|
+
) : (
|
|
879
|
+
<div
|
|
880
|
+
aria-labelledby="upgrade-version-label"
|
|
881
|
+
className="flex h-10 items-center rounded-lg border border-theme-border-light bg-theme-elevated px-3 text-sm text-theme-text-primary"
|
|
882
|
+
>
|
|
883
|
+
{targetVersion}
|
|
884
|
+
</div>
|
|
885
|
+
)}
|
|
886
|
+
{isDowngrade && (
|
|
887
|
+
<p className="mt-1 text-xs text-amber-600 dark:text-amber-400">
|
|
888
|
+
This is a downgrade from {upgradeInfo?.currentVersion}.
|
|
889
|
+
</p>
|
|
890
|
+
)}
|
|
891
|
+
{availableVersionsError && (
|
|
892
|
+
<p className="mt-1 text-xs text-theme-text-tertiary">
|
|
893
|
+
Version list unavailable; using the detected target version.
|
|
894
|
+
</p>
|
|
895
|
+
)}
|
|
896
|
+
{availableVersions && availableVersions.length >= 50 && (
|
|
897
|
+
<p className="mt-1 text-xs text-theme-text-tertiary">
|
|
898
|
+
Showing the 50 newest versions.
|
|
899
|
+
</p>
|
|
900
|
+
)}
|
|
901
|
+
</div>
|
|
902
|
+
)}
|
|
903
|
+
{upgradeProgress.length === 0 && canHelmWrite && canViewSensitive && (
|
|
904
|
+
<div className="mt-3 border-t border-theme-border pt-3">
|
|
905
|
+
<Tooltip content="Edit the user-supplied Helm values that Radar will pass to this upgrade.">
|
|
906
|
+
<button
|
|
907
|
+
type="button"
|
|
908
|
+
onClick={handleToggleAdjustValues}
|
|
909
|
+
disabled={isUpgrading || isPreviewingUpgrade}
|
|
910
|
+
aria-expanded={adjustValues}
|
|
911
|
+
aria-controls="helm-upgrade-values-panel"
|
|
912
|
+
className="flex items-center gap-1.5 text-sm font-medium text-theme-text-secondary hover:text-theme-text-primary disabled:opacity-50"
|
|
913
|
+
>
|
|
914
|
+
<ChevronRight className={clsx('w-4 h-4 transition-transform duration-200', adjustValues && 'rotate-90')} />
|
|
915
|
+
<SlidersHorizontal className="w-3.5 h-3.5" />
|
|
916
|
+
Adjust your values (optional)
|
|
917
|
+
</button>
|
|
918
|
+
</Tooltip>
|
|
919
|
+
<div
|
|
920
|
+
id="helm-upgrade-values-panel"
|
|
921
|
+
className={`issue-details-motion ${adjustValues ? 'issue-details-motion-open' : ''}`}
|
|
922
|
+
onTransitionEnd={(event) => {
|
|
923
|
+
if (event.target !== event.currentTarget) return
|
|
924
|
+
if (event.propertyName !== 'grid-template-rows') return
|
|
925
|
+
if (!adjustValues) {
|
|
926
|
+
setRenderAdjustValues(false)
|
|
927
|
+
}
|
|
928
|
+
}}
|
|
929
|
+
>
|
|
930
|
+
<div className="overflow-hidden">
|
|
931
|
+
{renderAdjustValues && (
|
|
932
|
+
<div className="mt-2">
|
|
933
|
+
<p className="mb-2 text-xs text-theme-text-tertiary">
|
|
934
|
+
These are your current settings — edit them to carry into {targetVersion}. What you see here is exactly what gets applied.
|
|
935
|
+
</p>
|
|
936
|
+
{upgradeValuesError && (
|
|
937
|
+
<div className="mt-2 px-3 py-2 text-xs text-red-400 bg-red-500/10 border border-red-500/30 rounded">
|
|
938
|
+
Couldn’t load current values: {upgradeValuesError instanceof Error ? upgradeValuesError.message : 'Unknown error'}
|
|
939
|
+
</div>
|
|
940
|
+
)}
|
|
941
|
+
{(upgradeValuesLoading || !upgradeValuesSeeded) && (
|
|
942
|
+
<div className="flex min-h-[180px] items-center gap-2 rounded border border-theme-border bg-theme-elevated px-3 text-sm text-theme-text-secondary">
|
|
943
|
+
<Loader2 className="w-4 h-4 animate-spin" />
|
|
944
|
+
Loading current values...
|
|
945
|
+
</div>
|
|
946
|
+
)}
|
|
947
|
+
{!upgradeValuesLoading && !upgradeValuesError && upgradeValuesSeeded && (
|
|
948
|
+
<textarea
|
|
949
|
+
defaultValue={editedUpgradeYaml}
|
|
950
|
+
onChange={(event) => {
|
|
951
|
+
editedUpgradeYamlRef.current = event.target.value
|
|
952
|
+
if (upgradeYamlError) setUpgradeYamlError(null)
|
|
953
|
+
}}
|
|
954
|
+
onBlur={() => setUpgradeYamlError(parseUpgradeValuesYaml(editedUpgradeYamlRef.current).error)}
|
|
955
|
+
aria-label="Edited Helm values YAML"
|
|
956
|
+
readOnly={isPreviewingUpgrade || isUpgrading}
|
|
957
|
+
spellCheck={false}
|
|
958
|
+
autoCorrect="off"
|
|
959
|
+
autoCapitalize="off"
|
|
960
|
+
rows={9}
|
|
961
|
+
className="max-h-[360px] min-h-[180px] w-full resize-y rounded-lg border border-theme-border bg-theme-elevated px-3 py-2 font-mono text-xs leading-5 text-theme-text-primary placeholder:text-theme-text-tertiary focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/30 read-only:opacity-70"
|
|
962
|
+
/>
|
|
963
|
+
)}
|
|
964
|
+
{upgradeYamlError && (
|
|
965
|
+
<div className="mt-2 px-3 py-2 text-xs text-red-400 bg-red-500/10 border border-red-500/30 rounded">
|
|
966
|
+
{upgradeYamlError}
|
|
967
|
+
</div>
|
|
968
|
+
)}
|
|
969
|
+
{upgradePreviewMutation.error && (
|
|
970
|
+
<div className="mt-2 px-3 py-2 text-xs text-red-400 bg-red-500/10 border border-red-500/30 rounded">
|
|
971
|
+
Couldn’t preview against {targetVersion}: {upgradePreviewMutation.error.message}. You can still upgrade.
|
|
972
|
+
</div>
|
|
973
|
+
)}
|
|
974
|
+
<div className="mt-2 flex justify-end">
|
|
975
|
+
<Tooltip
|
|
976
|
+
content="Render this release with the selected chart version and edited values, then show the manifest diff. Nothing is applied until you confirm from the preview."
|
|
977
|
+
wrapperClassName="shrink-0"
|
|
978
|
+
>
|
|
979
|
+
<button
|
|
980
|
+
type="button"
|
|
981
|
+
onClick={handleUpgradePreview}
|
|
982
|
+
disabled={upgradeValuesLoading || !!upgradeValuesError || !upgradeValuesSeeded || !!upgradeYamlError || isPreviewingUpgrade || isUpgrading}
|
|
983
|
+
className="flex items-center gap-1 px-2.5 py-1 text-xs text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded border border-theme-border disabled:opacity-50 disabled:cursor-not-allowed"
|
|
984
|
+
>
|
|
985
|
+
{upgradePreviewMutation.isPending ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Eye className="w-3.5 h-3.5" />}
|
|
986
|
+
Preview what changes
|
|
987
|
+
</button>
|
|
988
|
+
</Tooltip>
|
|
989
|
+
</div>
|
|
990
|
+
</div>
|
|
991
|
+
)}
|
|
992
|
+
</div>
|
|
993
|
+
</div>
|
|
994
|
+
</div>
|
|
995
|
+
)}
|
|
550
996
|
{upgradeProgress.length > 0 && <ProgressLog entries={upgradeProgress} />}
|
|
551
997
|
</ConfirmDialog>
|
|
998
|
+
|
|
999
|
+
{showUpgradePreview && upgradePreview && upgradePreviewRequest && (
|
|
1000
|
+
<ValuesDiffPreview
|
|
1001
|
+
previewData={upgradePreview}
|
|
1002
|
+
onClose={() => setShowUpgradePreview(false)}
|
|
1003
|
+
onApply={handleApplyUpgradePreview}
|
|
1004
|
+
isApplying={isUpgrading}
|
|
1005
|
+
title={`Preview upgrade to ${upgradePreviewRequest.targetVersion}`}
|
|
1006
|
+
applyLabel={upgradeInfo?.currentVersion && compareVersions(upgradePreviewRequest.targetVersion, upgradeInfo.currentVersion) === -1 ? 'Downgrade' : 'Upgrade'}
|
|
1007
|
+
/>
|
|
1008
|
+
)}
|
|
1009
|
+
|
|
1010
|
+
<TrackChartSourceDialog
|
|
1011
|
+
open={showTrackSource}
|
|
1012
|
+
onClose={() => setShowTrackSource(false)}
|
|
1013
|
+
chartName={releaseDetail?.chart}
|
|
1014
|
+
sourceIssue={upgradeSourceIssue}
|
|
1015
|
+
sourceError={upgradeInfo?.error}
|
|
1016
|
+
/>
|
|
552
1017
|
</div>
|
|
553
1018
|
)
|
|
554
1019
|
}
|
|
555
1020
|
|
|
1021
|
+
function userSuppliedToYaml(userSupplied: Record<string, unknown> | undefined): string {
|
|
1022
|
+
if (!userSupplied || Object.keys(userSupplied).length === 0) return ''
|
|
1023
|
+
return yaml.stringify(userSupplied, { lineWidth: 0 })
|
|
1024
|
+
}
|
|
1025
|
+
|
|
556
1026
|
// Shared progress log for streaming Helm operations
|
|
557
1027
|
function ProgressLog({ entries }: { entries: { phase: string; message: string }[] }) {
|
|
558
1028
|
return (
|
|
@@ -580,6 +1050,321 @@ function ProgressLog({ entries }: { entries: { phase: string; message: string }[
|
|
|
580
1050
|
)
|
|
581
1051
|
}
|
|
582
1052
|
|
|
1053
|
+
function mergeHelmOperations(operations: HelmOperation[] | undefined, lastOperation: HelmOperation | undefined): HelmOperation[] {
|
|
1054
|
+
const merged: HelmOperation[] = []
|
|
1055
|
+
const seen = new Set<string>()
|
|
1056
|
+
for (const op of [...(operations || []), ...(lastOperation ? [lastOperation] : [])]) {
|
|
1057
|
+
const key = helmOperationKey(op)
|
|
1058
|
+
if (seen.has(key)) continue
|
|
1059
|
+
seen.add(key)
|
|
1060
|
+
merged.push(op)
|
|
1061
|
+
}
|
|
1062
|
+
return merged
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function helmOperationKey(operation: HelmOperation): string {
|
|
1066
|
+
return [
|
|
1067
|
+
operation.kind,
|
|
1068
|
+
operation.status,
|
|
1069
|
+
operation.revision || 0,
|
|
1070
|
+
operation.failedRevision || 0,
|
|
1071
|
+
operation.rollbackRevision || 0,
|
|
1072
|
+
operation.targetRevision || 0,
|
|
1073
|
+
].join(':')
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
function HelmOperationBanner({
|
|
1077
|
+
operation,
|
|
1078
|
+
operationInsight,
|
|
1079
|
+
releaseNamespace,
|
|
1080
|
+
managedByFluxHelmRelease,
|
|
1081
|
+
hookDiagnostics,
|
|
1082
|
+
canCompare,
|
|
1083
|
+
onCompare,
|
|
1084
|
+
onNavigateToResource,
|
|
1085
|
+
}: {
|
|
1086
|
+
operation?: HelmOperation
|
|
1087
|
+
operationInsight?: HelmOperationInsight
|
|
1088
|
+
releaseNamespace: string
|
|
1089
|
+
managedByFluxHelmRelease?: string
|
|
1090
|
+
hookDiagnostics?: HookDiagnostic[]
|
|
1091
|
+
canCompare?: boolean
|
|
1092
|
+
onCompare?: (rev1: number, rev2: number) => void
|
|
1093
|
+
onNavigateToResource?: NavigateToResource
|
|
1094
|
+
}) {
|
|
1095
|
+
const primaryHookDiagnostic = hookDiagnostics?.[0]
|
|
1096
|
+
if ((!operation || !shouldShowOperationBanner(operation)) && !primaryHookDiagnostic) {
|
|
1097
|
+
return null
|
|
1098
|
+
}
|
|
1099
|
+
if (!operation || !shouldShowOperationBanner(operation)) {
|
|
1100
|
+
const tone = hookDiagnosticTone(primaryHookDiagnostic!)
|
|
1101
|
+
return (
|
|
1102
|
+
<div className="m-4 mb-0 card-inner-lg">
|
|
1103
|
+
<div className="flex items-start gap-3">
|
|
1104
|
+
<AlertTriangle className={clsx('mt-0.5 h-5 w-5 shrink-0', SEVERITY_TEXT[tone])} />
|
|
1105
|
+
<div className="min-w-0 flex-1">
|
|
1106
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
1107
|
+
<span className="text-sm font-medium text-theme-text-primary">Helm hook needs attention</span>
|
|
1108
|
+
<span className={clsx('badge-sm', SEVERITY_BADGE[tone])}>{primaryHookDiagnostic!.phase}</span>
|
|
1109
|
+
</div>
|
|
1110
|
+
<HookSignal diagnostic={primaryHookDiagnostic!} />
|
|
1111
|
+
</div>
|
|
1112
|
+
</div>
|
|
1113
|
+
</div>
|
|
1114
|
+
)
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
const isFailure = operation.status === 'failed'
|
|
1118
|
+
const isPending = operation.status === 'stuck_pending'
|
|
1119
|
+
const tone: 'error' | 'warning' | 'info' = isFailure ? 'error' : operation.kind === 'rollback' ? 'info' : 'warning'
|
|
1120
|
+
const Icon = operation.kind === 'upgrade_rolled_back' || operation.kind === 'rollback' ? RotateCcw : isPending ? Clock : AlertTriangle
|
|
1121
|
+
const title = operationTitle(operation)
|
|
1122
|
+
const statusLabel = operation.status.replace(/_/g, ' ')
|
|
1123
|
+
const showStatusBadge = !(operation.status === 'failed' && title.toLowerCase().includes('failed'))
|
|
1124
|
+
const rawMessage = operation.rawMessage?.trim()
|
|
1125
|
+
const hasRawMessage = !!rawMessage && rawMessage !== operation.message.trim()
|
|
1126
|
+
|
|
1127
|
+
return (
|
|
1128
|
+
<div className="m-4 mb-0 card-inner-lg">
|
|
1129
|
+
<div className="flex items-start gap-3">
|
|
1130
|
+
<Icon className={clsx('mt-0.5 h-5 w-5 shrink-0', SEVERITY_TEXT[tone])} />
|
|
1131
|
+
<div className="min-w-0 flex-1">
|
|
1132
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
1133
|
+
<span className="text-sm font-medium text-theme-text-primary">{title}</span>
|
|
1134
|
+
{showStatusBadge && (
|
|
1135
|
+
<span className={clsx('badge-sm', SEVERITY_BADGE[tone])}>{statusLabel}</span>
|
|
1136
|
+
)}
|
|
1137
|
+
<OperationRevisionChips operation={operation} />
|
|
1138
|
+
</div>
|
|
1139
|
+
<p className="mt-1 text-sm text-theme-text-secondary">{operation.message}</p>
|
|
1140
|
+
{hasRawMessage && (
|
|
1141
|
+
<details className="mt-2">
|
|
1142
|
+
<summary className="cursor-pointer select-none text-xs font-medium text-theme-text-tertiary hover:text-theme-text-secondary">
|
|
1143
|
+
Show raw Helm error
|
|
1144
|
+
</summary>
|
|
1145
|
+
<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">
|
|
1146
|
+
{rawMessage}
|
|
1147
|
+
</pre>
|
|
1148
|
+
</details>
|
|
1149
|
+
)}
|
|
1150
|
+
{operation.failureDescription && !hasRawMessage && (
|
|
1151
|
+
<Tooltip content={operation.failureDescription} wrapperClassName="mt-1 flex">
|
|
1152
|
+
<span className="line-clamp-2 text-xs text-theme-text-tertiary">
|
|
1153
|
+
{operation.failureDescription}
|
|
1154
|
+
</span>
|
|
1155
|
+
</Tooltip>
|
|
1156
|
+
)}
|
|
1157
|
+
{operation.kind === 'upgrade_rolled_back' && (
|
|
1158
|
+
<p className="mt-1 text-xs text-theme-text-tertiary">
|
|
1159
|
+
Helm history does not record whether <code className="inline-code text-[11px]">--atomic</code> was set; the rollback is inferred from adjacent release revisions.
|
|
1160
|
+
</p>
|
|
1161
|
+
)}
|
|
1162
|
+
<OperationInsightSignals
|
|
1163
|
+
insight={operationInsight}
|
|
1164
|
+
releaseNamespace={releaseNamespace}
|
|
1165
|
+
canCompare={Boolean(canCompare)}
|
|
1166
|
+
onCompare={onCompare}
|
|
1167
|
+
onNavigateToResource={onNavigateToResource}
|
|
1168
|
+
/>
|
|
1169
|
+
{primaryHookDiagnostic && <HookSignal diagnostic={primaryHookDiagnostic} />}
|
|
1170
|
+
{managedByFluxHelmRelease && (
|
|
1171
|
+
<p className="mt-1 text-xs text-theme-text-tertiary">
|
|
1172
|
+
This release is managed by Flux HelmRelease {managedByFluxHelmRelease}; direct Helm changes may be reconciled back.
|
|
1173
|
+
</p>
|
|
1174
|
+
)}
|
|
1175
|
+
</div>
|
|
1176
|
+
</div>
|
|
1177
|
+
</div>
|
|
1178
|
+
)
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
function OperationInsightSignals({
|
|
1182
|
+
insight,
|
|
1183
|
+
releaseNamespace,
|
|
1184
|
+
canCompare,
|
|
1185
|
+
onCompare,
|
|
1186
|
+
onNavigateToResource,
|
|
1187
|
+
}: {
|
|
1188
|
+
insight?: HelmOperationInsight
|
|
1189
|
+
releaseNamespace: string
|
|
1190
|
+
canCompare: boolean
|
|
1191
|
+
onCompare?: (rev1: number, rev2: number) => void
|
|
1192
|
+
onNavigateToResource?: NavigateToResource
|
|
1193
|
+
}) {
|
|
1194
|
+
const compare = insight?.suggestedCompare
|
|
1195
|
+
const primaryResource = insight?.primaryResource
|
|
1196
|
+
const relatedCount = primaryResource
|
|
1197
|
+
? Math.max(0, (insight?.signalCount ?? 0) - 1)
|
|
1198
|
+
: insight?.relatedResources?.length ?? 0
|
|
1199
|
+
const showCompare = Boolean(compare && canCompare && onCompare)
|
|
1200
|
+
if (!primaryResource && !showCompare) {
|
|
1201
|
+
return null
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
return (
|
|
1205
|
+
<div className="mt-2 space-y-2">
|
|
1206
|
+
{primaryResource && (
|
|
1207
|
+
<PrimaryOperationResourceSignal
|
|
1208
|
+
resource={primaryResource}
|
|
1209
|
+
releaseNamespace={releaseNamespace}
|
|
1210
|
+
relatedCount={relatedCount}
|
|
1211
|
+
onNavigateToResource={onNavigateToResource}
|
|
1212
|
+
/>
|
|
1213
|
+
)}
|
|
1214
|
+
{showCompare && compare && (
|
|
1215
|
+
<Tooltip content={compare.reason || 'Compare the two Helm revisions'}>
|
|
1216
|
+
<button
|
|
1217
|
+
type="button"
|
|
1218
|
+
onClick={() => onCompare?.(compare.revision1, compare.revision2)}
|
|
1219
|
+
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"
|
|
1220
|
+
>
|
|
1221
|
+
<GitCompare className="h-3.5 w-3.5" />
|
|
1222
|
+
Compare rev {compare.revision1} to {compare.revision2}
|
|
1223
|
+
</button>
|
|
1224
|
+
</Tooltip>
|
|
1225
|
+
)}
|
|
1226
|
+
</div>
|
|
1227
|
+
)
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
function PrimaryOperationResourceSignal({
|
|
1231
|
+
resource,
|
|
1232
|
+
releaseNamespace,
|
|
1233
|
+
relatedCount,
|
|
1234
|
+
onNavigateToResource,
|
|
1235
|
+
}: {
|
|
1236
|
+
resource: HelmOwnedResource
|
|
1237
|
+
releaseNamespace: string
|
|
1238
|
+
relatedCount: number
|
|
1239
|
+
onNavigateToResource?: NavigateToResource
|
|
1240
|
+
}) {
|
|
1241
|
+
const canNavigate = Boolean(onNavigateToResource)
|
|
1242
|
+
const resourceLabel = `${resource.kind}/${resource.name}`
|
|
1243
|
+
const resourceTarget = resource.namespace ? `${resource.namespace}/${resource.name}` : resource.name
|
|
1244
|
+
const showNamespace = Boolean(resource.namespace && resource.namespace !== releaseNamespace)
|
|
1245
|
+
const resourceTooltip = `Open ${resource.kind} ${resourceTarget}`
|
|
1246
|
+
const showInlineReady = Boolean(resource.ready && !resource.summary)
|
|
1247
|
+
const openResource = () => {
|
|
1248
|
+
onNavigateToResource?.({
|
|
1249
|
+
kind: kindToPlural(resource.kind),
|
|
1250
|
+
namespace: resource.namespace,
|
|
1251
|
+
name: resource.name,
|
|
1252
|
+
group: apiVersionToGroup(resource.apiVersion),
|
|
1253
|
+
})
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
return (
|
|
1257
|
+
<div className="rounded-md bg-theme-base/50 p-2 text-xs">
|
|
1258
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
1259
|
+
<span className="font-medium text-theme-text-secondary">Likely resource to inspect</span>
|
|
1260
|
+
{canNavigate ? (
|
|
1261
|
+
<Tooltip content={resourceTooltip}>
|
|
1262
|
+
<button
|
|
1263
|
+
type="button"
|
|
1264
|
+
onClick={openResource}
|
|
1265
|
+
aria-label={resourceTooltip}
|
|
1266
|
+
className={clsx(
|
|
1267
|
+
'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',
|
|
1268
|
+
getKindBadgeColor(resource.kind)
|
|
1269
|
+
)}
|
|
1270
|
+
>
|
|
1271
|
+
<span className="min-w-0 truncate">{resourceLabel}</span>
|
|
1272
|
+
<ExternalLink className="h-3 w-3 shrink-0 opacity-70" />
|
|
1273
|
+
</button>
|
|
1274
|
+
</Tooltip>
|
|
1275
|
+
) : (
|
|
1276
|
+
<span className={clsx('badge-sm max-w-full min-w-0', getKindBadgeColor(resource.kind))}>
|
|
1277
|
+
<span className="min-w-0 truncate">{resourceLabel}</span>
|
|
1278
|
+
</span>
|
|
1279
|
+
)}
|
|
1280
|
+
{showNamespace && <span className="text-theme-text-tertiary">ns/{resource.namespace}</span>}
|
|
1281
|
+
{showInlineReady && <span className="font-mono text-theme-text-secondary">{resource.ready}</span>}
|
|
1282
|
+
{resource.status && (
|
|
1283
|
+
<span className={clsx('badge-sm', getResourceStatusColor(resource.status))}>{resource.status}</span>
|
|
1284
|
+
)}
|
|
1285
|
+
</div>
|
|
1286
|
+
{(resource.issue || resource.summary || resource.message || relatedCount > 0) && (
|
|
1287
|
+
<div className="mt-1 text-theme-text-tertiary">
|
|
1288
|
+
{[
|
|
1289
|
+
resource.issue || resource.summary || resource.message,
|
|
1290
|
+
relatedCount > 0 ? `${relatedCount} more affected resource${relatedCount === 1 ? '' : 's'} in Resources tab` : null,
|
|
1291
|
+
]
|
|
1292
|
+
.filter(Boolean)
|
|
1293
|
+
.join(' - ')}
|
|
1294
|
+
</div>
|
|
1295
|
+
)}
|
|
1296
|
+
</div>
|
|
1297
|
+
)
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
function hookDiagnosticTone(diagnostic: HookDiagnostic): 'error' | 'warning' {
|
|
1301
|
+
return diagnostic.phase.toLowerCase() === 'failed' ? 'error' : 'warning'
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
function HookSignal({ diagnostic }: { diagnostic: HookDiagnostic }) {
|
|
1305
|
+
return (
|
|
1306
|
+
<div className="mt-2 rounded-md bg-theme-base/50 p-2 text-xs">
|
|
1307
|
+
<div className="font-medium text-theme-text-secondary">
|
|
1308
|
+
Hook signal: {formatHookRef(diagnostic)} is {diagnostic.phase}
|
|
1309
|
+
</div>
|
|
1310
|
+
<div className="mt-1 text-theme-text-tertiary">{diagnostic.message}</div>
|
|
1311
|
+
{diagnostic.evidence?.summary && (
|
|
1312
|
+
<div className="mt-1 text-theme-text-secondary">{diagnostic.evidence.summary}</div>
|
|
1313
|
+
)}
|
|
1314
|
+
{diagnostic.evidenceUnavailableReason && (
|
|
1315
|
+
<div className="mt-1 text-theme-text-tertiary">{diagnostic.evidenceUnavailableReason}</div>
|
|
1316
|
+
)}
|
|
1317
|
+
</div>
|
|
1318
|
+
)
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
function shouldShowOperationBanner(operation: HelmOperation): boolean {
|
|
1322
|
+
return operation.kind === 'upgrade_rolled_back' || operation.kind === 'rollback' || operation.status === 'failed' || operation.status === 'stuck_pending'
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
function operationTitle(operation: HelmOperation): string {
|
|
1326
|
+
switch (operation.kind) {
|
|
1327
|
+
case 'upgrade_rolled_back':
|
|
1328
|
+
return 'Helm rolled back after failed upgrade'
|
|
1329
|
+
case 'rollback':
|
|
1330
|
+
return 'Helm rollback applied'
|
|
1331
|
+
case 'pending':
|
|
1332
|
+
return 'Helm operation may be stuck'
|
|
1333
|
+
case 'upgrade_failed':
|
|
1334
|
+
return 'Helm upgrade failed'
|
|
1335
|
+
case 'release_failed':
|
|
1336
|
+
return 'Helm release failed'
|
|
1337
|
+
default:
|
|
1338
|
+
return 'Helm operation'
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
function OperationRevisionChips({ operation }: { operation: HelmOperation }) {
|
|
1343
|
+
const chips: Array<{ key: string; label: string; className: string }> = []
|
|
1344
|
+
if (operation.failedRevision) {
|
|
1345
|
+
chips.push({ key: 'failed', label: `failed rev ${operation.failedRevision}`, className: SEVERITY_BADGE.error })
|
|
1346
|
+
}
|
|
1347
|
+
if (operation.rollbackRevision) {
|
|
1348
|
+
chips.push({ key: 'rollback', label: `rollback rev ${operation.rollbackRevision}`, className: SEVERITY_BADGE.warning })
|
|
1349
|
+
}
|
|
1350
|
+
if (operation.targetRevision) {
|
|
1351
|
+
chips.push({ key: 'target', label: `target rev ${operation.targetRevision}`, className: SEVERITY_BADGE.info })
|
|
1352
|
+
}
|
|
1353
|
+
if (!operation.failedRevision && !operation.rollbackRevision && operation.revision) {
|
|
1354
|
+
chips.push({ key: 'revision', label: `rev ${operation.revision}`, className: SEVERITY_BADGE.neutral })
|
|
1355
|
+
}
|
|
1356
|
+
if (chips.length === 0) {
|
|
1357
|
+
return null
|
|
1358
|
+
}
|
|
1359
|
+
return (
|
|
1360
|
+
<>
|
|
1361
|
+
{chips.map((chip) => (
|
|
1362
|
+
<span key={chip.key} className={clsx('badge-sm', chip.className)}>{chip.label}</span>
|
|
1363
|
+
))}
|
|
1364
|
+
</>
|
|
1365
|
+
)
|
|
1366
|
+
}
|
|
1367
|
+
|
|
583
1368
|
// Overview tab content
|
|
584
1369
|
interface OverviewTabProps {
|
|
585
1370
|
release: {
|
|
@@ -592,12 +1377,15 @@ interface OverviewTabProps {
|
|
|
592
1377
|
notes: string
|
|
593
1378
|
readme?: string
|
|
594
1379
|
dependencies?: ChartDependency[]
|
|
1380
|
+
lastOperation?: HelmOperation
|
|
595
1381
|
}
|
|
596
1382
|
onCopy: (text: string, key: string) => void
|
|
597
1383
|
copied: string | null
|
|
598
1384
|
}
|
|
599
1385
|
|
|
600
1386
|
function OverviewTab({ release, onCopy, copied }: OverviewTabProps) {
|
|
1387
|
+
const showDescription = !!release.description && !isDuplicateOperationRawDescription(release.description, release.lastOperation)
|
|
1388
|
+
|
|
601
1389
|
return (
|
|
602
1390
|
<div className="p-4 space-y-4">
|
|
603
1391
|
{/* Chart info */}
|
|
@@ -628,7 +1416,7 @@ function OverviewTab({ release, onCopy, copied }: OverviewTabProps) {
|
|
|
628
1416
|
</div>
|
|
629
1417
|
|
|
630
1418
|
{/* Description */}
|
|
631
|
-
{
|
|
1419
|
+
{showDescription && (
|
|
632
1420
|
<div className="bg-theme-elevated/30 rounded-lg p-4">
|
|
633
1421
|
<h3 className="text-sm font-medium text-theme-text-secondary mb-2">Description</h3>
|
|
634
1422
|
<p className="text-sm text-theme-text-secondary">{release.description}</p>
|
|
@@ -712,12 +1500,23 @@ function OverviewTab({ release, onCopy, copied }: OverviewTabProps) {
|
|
|
712
1500
|
)
|
|
713
1501
|
}
|
|
714
1502
|
|
|
1503
|
+
function isDuplicateOperationRawDescription(description: string, operation?: HelmOperation): boolean {
|
|
1504
|
+
const desc = normalizeComparableText(description)
|
|
1505
|
+
const raw = normalizeComparableText(operation?.rawMessage || '')
|
|
1506
|
+
return desc !== '' && raw.includes(desc)
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
function normalizeComparableText(value: string): string {
|
|
1510
|
+
return value.trim().replace(/\s+/g, ' ')
|
|
1511
|
+
}
|
|
1512
|
+
|
|
715
1513
|
// Hooks tab content
|
|
716
1514
|
interface HooksTabProps {
|
|
717
1515
|
hooks: HelmHook[]
|
|
1516
|
+
hookDiagnostics: HookDiagnostic[]
|
|
718
1517
|
}
|
|
719
1518
|
|
|
720
|
-
function HooksTab({ hooks }: HooksTabProps) {
|
|
1519
|
+
function HooksTab({ hooks, hookDiagnostics }: HooksTabProps) {
|
|
721
1520
|
if (hooks.length === 0) {
|
|
722
1521
|
return (
|
|
723
1522
|
<div className="flex flex-col items-center justify-center h-48 text-theme-text-tertiary">
|
|
@@ -749,23 +1548,31 @@ function HooksTab({ hooks }: HooksTabProps) {
|
|
|
749
1548
|
return SEVERITY_BADGE.neutral
|
|
750
1549
|
}
|
|
751
1550
|
|
|
1551
|
+
const diagnostics = new Map<string, HookDiagnostic>()
|
|
1552
|
+
for (const diagnostic of hookDiagnostics) {
|
|
1553
|
+
diagnostics.set(hookDiagnosticKey(diagnostic.namespace, diagnostic.kind, diagnostic.name), diagnostic)
|
|
1554
|
+
diagnostics.set(hookDiagnosticKey(undefined, diagnostic.kind, diagnostic.name), diagnostic)
|
|
1555
|
+
}
|
|
1556
|
+
|
|
752
1557
|
return (
|
|
753
1558
|
<div className="p-4 space-y-3">
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
<div key={hook.name} className="bg-theme-elevated/30 rounded-lg p-4">
|
|
759
|
-
<div className="flex items-start justify-between mb-2">
|
|
760
|
-
<div>
|
|
761
|
-
<div className="flex items-center gap-2">
|
|
762
|
-
<span className="text-theme-text-primary font-medium">{hook.name}</span>
|
|
763
|
-
<span className="badge-sm bg-theme-hover/50 text-theme-text-secondary">
|
|
1559
|
+
{hooks.map((hook) => {
|
|
1560
|
+
const diagnostic = diagnostics.get(hookDiagnosticKey(hook.namespace, hook.kind, hook.name))
|
|
1561
|
+
|| diagnostics.get(hookDiagnosticKey(undefined, hook.kind, hook.name))
|
|
1562
|
+
return (
|
|
1563
|
+
<div key={`${hook.namespace || '_'}:${hook.kind}:${hook.name}`} className="bg-theme-elevated/30 rounded-lg p-4">
|
|
1564
|
+
<div className="flex items-start justify-between gap-3 mb-2">
|
|
1565
|
+
<div className="min-w-0">
|
|
1566
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
1567
|
+
<span className="break-all text-theme-text-primary font-medium">{hook.name}</span>
|
|
1568
|
+
<span className="badge-sm shrink-0 bg-theme-hover/50 text-theme-text-secondary">
|
|
764
1569
|
{hook.kind}
|
|
765
1570
|
</span>
|
|
766
1571
|
</div>
|
|
767
|
-
<div className="flex
|
|
1572
|
+
<div className="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-xs text-theme-text-tertiary">
|
|
1573
|
+
{hook.namespace && <span className="break-all">Namespace: {hook.namespace}</span>}
|
|
768
1574
|
<span>Weight: {hook.weight}</span>
|
|
1575
|
+
{hook.path && <span className="break-all">Path: {hook.path}</span>}
|
|
769
1576
|
</div>
|
|
770
1577
|
</div>
|
|
771
1578
|
{hook.status && (
|
|
@@ -784,8 +1591,172 @@ function HooksTab({ hooks }: HooksTabProps) {
|
|
|
784
1591
|
</span>
|
|
785
1592
|
))}
|
|
786
1593
|
</div>
|
|
1594
|
+
{(hook.startedAt || hook.completedAt || hook.deletePolicies?.length || hook.outputLogPolicies?.length) && (
|
|
1595
|
+
<div className="mt-3 grid grid-cols-1 gap-2 text-xs text-theme-text-tertiary sm:grid-cols-2">
|
|
1596
|
+
{hook.startedAt && (
|
|
1597
|
+
<div>
|
|
1598
|
+
<span className="text-theme-text-disabled">Started: </span>
|
|
1599
|
+
<span>{formatDate(hook.startedAt)}</span>
|
|
1600
|
+
</div>
|
|
1601
|
+
)}
|
|
1602
|
+
{hook.completedAt && (
|
|
1603
|
+
<div>
|
|
1604
|
+
<span className="text-theme-text-disabled">Completed: </span>
|
|
1605
|
+
<span>{formatDate(hook.completedAt)}</span>
|
|
1606
|
+
</div>
|
|
1607
|
+
)}
|
|
1608
|
+
{hook.deletePolicies && hook.deletePolicies.length > 0 && (
|
|
1609
|
+
<div className="sm:col-span-2">
|
|
1610
|
+
<span className="text-theme-text-disabled">Delete policies: </span>
|
|
1611
|
+
<span>{hook.deletePolicies.join(', ')}</span>
|
|
1612
|
+
</div>
|
|
1613
|
+
)}
|
|
1614
|
+
{hook.outputLogPolicies && hook.outputLogPolicies.length > 0 && (
|
|
1615
|
+
<div className="sm:col-span-2">
|
|
1616
|
+
<span className="text-theme-text-disabled">Output log policies: </span>
|
|
1617
|
+
<span>{hook.outputLogPolicies.join(', ')}</span>
|
|
1618
|
+
</div>
|
|
1619
|
+
)}
|
|
1620
|
+
</div>
|
|
1621
|
+
)}
|
|
1622
|
+
{diagnostic && <HookDiagnosticEvidence diagnostic={diagnostic} />}
|
|
787
1623
|
</div>
|
|
788
|
-
|
|
1624
|
+
)
|
|
1625
|
+
})}
|
|
1626
|
+
</div>
|
|
1627
|
+
)
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
function hookDiagnosticKey(namespace: string | undefined, kind: string | undefined, name: string | undefined): string {
|
|
1631
|
+
return `${namespace || ''}/${(kind || '').toLowerCase()}/${name || ''}`
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
function formatHookRef(hook: Pick<HookDiagnostic, 'namespace' | 'kind' | 'name'>): string {
|
|
1635
|
+
return `${hook.namespace ? `${hook.namespace}/` : ''}${hook.name} (${hook.kind})`
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
function HookDiagnosticEvidence({ diagnostic }: { diagnostic: HookDiagnostic }) {
|
|
1639
|
+
const evidence = diagnostic.evidence
|
|
1640
|
+
|
|
1641
|
+
return (
|
|
1642
|
+
<div className="mt-3 border-t border-theme-border/60 pt-3 text-xs">
|
|
1643
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
1644
|
+
<span className="font-medium text-theme-text-secondary">Diagnostic evidence</span>
|
|
1645
|
+
<span className={clsx('badge-sm', diagnostic.phase.toLowerCase() === 'failed' ? SEVERITY_BADGE.error : SEVERITY_BADGE.warning)}>
|
|
1646
|
+
{diagnostic.phase}
|
|
1647
|
+
</span>
|
|
1648
|
+
{evidence?.summary && <span className="text-theme-text-tertiary">{evidence.summary}</span>}
|
|
1649
|
+
</div>
|
|
1650
|
+
<div className="mt-1 text-theme-text-tertiary">{diagnostic.message}</div>
|
|
1651
|
+
{diagnostic.evidenceUnavailableReason && (
|
|
1652
|
+
<div className="mt-1 text-theme-text-tertiary">{diagnostic.evidenceUnavailableReason}</div>
|
|
1653
|
+
)}
|
|
1654
|
+
{evidence && (
|
|
1655
|
+
<div className="mt-3 space-y-3">
|
|
1656
|
+
{evidence.jobs && evidence.jobs.length > 0 && (
|
|
1657
|
+
<div className="space-y-1">
|
|
1658
|
+
{evidence.jobs.map((job) => (
|
|
1659
|
+
<div key={`${job.namespace || ''}/${job.name}`} className="grid grid-cols-[80px_1fr] gap-2 rounded bg-theme-base/40 px-2 py-1">
|
|
1660
|
+
<span className="text-theme-text-tertiary">Job</span>
|
|
1661
|
+
<span className="min-w-0 text-theme-text-primary">
|
|
1662
|
+
<span className="break-all font-medium">{job.namespace ? `${job.namespace}/` : ''}{job.name}</span>
|
|
1663
|
+
{job.status && <span className="ml-2 text-theme-text-secondary">{job.status}</span>}
|
|
1664
|
+
<span className="ml-2 text-theme-text-tertiary">
|
|
1665
|
+
active {job.active || 0} · succeeded {job.succeeded || 0} · failed {job.failed || 0}
|
|
1666
|
+
</span>
|
|
1667
|
+
</span>
|
|
1668
|
+
{job.conditions?.map((condition) => (
|
|
1669
|
+
<span key={condition} className="col-start-2 text-theme-text-tertiary">{condition}</span>
|
|
1670
|
+
))}
|
|
1671
|
+
</div>
|
|
1672
|
+
))}
|
|
1673
|
+
</div>
|
|
1674
|
+
)}
|
|
1675
|
+
|
|
1676
|
+
{evidence.pods && evidence.pods.length > 0 && (
|
|
1677
|
+
<div className="space-y-1">
|
|
1678
|
+
{evidence.pods.map((pod) => (
|
|
1679
|
+
<div key={`${pod.namespace || ''}/${pod.name}`} className="grid grid-cols-[80px_1fr] gap-2 rounded bg-theme-base/40 px-2 py-1">
|
|
1680
|
+
<span className="text-theme-text-tertiary">Pod</span>
|
|
1681
|
+
<span className="min-w-0 text-theme-text-primary">
|
|
1682
|
+
<span className="break-all font-medium">{pod.namespace ? `${pod.namespace}/` : ''}{pod.name}</span>
|
|
1683
|
+
{pod.phase && <span className="ml-2 text-theme-text-secondary">{pod.phase}</span>}
|
|
1684
|
+
{pod.ready && <span className="ml-2 text-theme-text-tertiary">{pod.ready} ready</span>}
|
|
1685
|
+
{Boolean(pod.restartCount) && <span className="ml-2 text-theme-text-tertiary">{pod.restartCount} restarts</span>}
|
|
1686
|
+
</span>
|
|
1687
|
+
{(pod.reason || pod.message) && (
|
|
1688
|
+
<span className="col-start-2 text-theme-text-tertiary">
|
|
1689
|
+
{pod.reason && <span className="font-medium">{pod.reason}: </span>}
|
|
1690
|
+
{pod.message}
|
|
1691
|
+
</span>
|
|
1692
|
+
)}
|
|
1693
|
+
</div>
|
|
1694
|
+
))}
|
|
1695
|
+
</div>
|
|
1696
|
+
)}
|
|
1697
|
+
|
|
1698
|
+
{evidence.events && evidence.events.length > 0 && (
|
|
1699
|
+
<div className="space-y-1">
|
|
1700
|
+
{evidence.events.map((event, index) => (
|
|
1701
|
+
<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">
|
|
1702
|
+
<span className="text-theme-text-tertiary">Event</span>
|
|
1703
|
+
<span className="min-w-0 text-theme-text-primary">
|
|
1704
|
+
<span className={clsx('badge-sm mr-2', event.type === 'Warning' ? SEVERITY_BADGE.warning : SEVERITY_BADGE.neutral)}>
|
|
1705
|
+
{event.type || 'Event'}
|
|
1706
|
+
</span>
|
|
1707
|
+
<span className="font-medium">{event.reason || 'Unknown'}</span>
|
|
1708
|
+
<span className="ml-2 break-all text-theme-text-tertiary">{event.involvedKind}/{event.involvedName}</span>
|
|
1709
|
+
</span>
|
|
1710
|
+
{event.message && <span className="col-start-2 text-theme-text-tertiary">{event.message}</span>}
|
|
1711
|
+
</div>
|
|
1712
|
+
))}
|
|
1713
|
+
</div>
|
|
1714
|
+
)}
|
|
1715
|
+
|
|
1716
|
+
{evidence.logs && evidence.logs.length > 0 && (
|
|
1717
|
+
<div className="space-y-2">
|
|
1718
|
+
{evidence.logs.map((log, index) => (
|
|
1719
|
+
<HookLogEvidenceBlock key={`${log.pod}/${log.container}/${log.previous ? 'previous' : 'current'}/${index}`} log={log} />
|
|
1720
|
+
))}
|
|
1721
|
+
</div>
|
|
1722
|
+
)}
|
|
1723
|
+
|
|
1724
|
+
{evidence.errors && evidence.errors.length > 0 && (
|
|
1725
|
+
<div className="space-y-1 text-theme-text-tertiary">
|
|
1726
|
+
{evidence.errors.map((error) => (
|
|
1727
|
+
<div key={error}>Evidence read error: {error}</div>
|
|
1728
|
+
))}
|
|
1729
|
+
</div>
|
|
1730
|
+
)}
|
|
1731
|
+
</div>
|
|
1732
|
+
)}
|
|
1733
|
+
</div>
|
|
1734
|
+
)
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
function HookLogEvidenceBlock({ log }: { log: HookLogEvidence }) {
|
|
1738
|
+
const visibleLines = (log.lines || []).slice(0, 8)
|
|
1739
|
+
|
|
1740
|
+
return (
|
|
1741
|
+
<div className="rounded bg-theme-base/40 px-2 py-2">
|
|
1742
|
+
<div className="mb-1 flex flex-wrap items-center gap-2 text-theme-text-secondary">
|
|
1743
|
+
<span className="font-medium">Logs</span>
|
|
1744
|
+
<span className="break-all">{log.pod}/{log.container}</span>
|
|
1745
|
+
{log.previous && <span className="badge-sm bg-theme-hover/50 text-theme-text-secondary">previous</span>}
|
|
1746
|
+
{log.fallback && <span className="text-theme-text-tertiary">fallback tail</span>}
|
|
1747
|
+
</div>
|
|
1748
|
+
{log.error ? (
|
|
1749
|
+
<div className="text-theme-text-tertiary">{log.error}</div>
|
|
1750
|
+
) : (
|
|
1751
|
+
<>
|
|
1752
|
+
<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">
|
|
1753
|
+
{visibleLines.join('\n')}
|
|
1754
|
+
</pre>
|
|
1755
|
+
{log.lines && log.lines.length > visibleLines.length && (
|
|
1756
|
+
<div className="mt-1 text-theme-text-tertiary">+{log.lines.length - visibleLines.length} more lines in API response</div>
|
|
1757
|
+
)}
|
|
1758
|
+
</>
|
|
1759
|
+
)}
|
|
789
1760
|
</div>
|
|
790
1761
|
)
|
|
791
1762
|
}
|