@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,13 +1,13 @@
|
|
|
1
1
|
import { useState, useMemo, useRef, useEffect, useCallback, forwardRef } from 'react'
|
|
2
|
-
import { useRefreshAnimation } from '../../hooks/useRefreshAnimation'
|
|
3
2
|
import { useRegisterShortcuts } from '../../hooks/useKeyboardShortcuts'
|
|
4
|
-
import { Package, Search,
|
|
5
|
-
import { PaneLoader } from '@skyhook-io/k8s-ui'
|
|
3
|
+
import { Package, Search, ArrowUpCircle, LayoutGrid, List, Shield, GitBranch, ChevronRight, RotateCcw, Clock } from 'lucide-react'
|
|
4
|
+
import { PaneLoader, PageHeader, SortableTh, FreshnessControl, type SortDir, Input } from '@skyhook-io/k8s-ui'
|
|
5
|
+
import { useConnection } from '../../context/ConnectionContext'
|
|
6
6
|
import { clsx } from 'clsx'
|
|
7
7
|
import { useHelmReleases, useHelmBatchUpgradeInfo, isForbiddenError } from '../../api/client'
|
|
8
|
-
import type { HelmRelease, SelectedHelmRelease, UpgradeInfo, ChartSource } from '../../types'
|
|
9
|
-
import { getStatusColor, formatAge,
|
|
10
|
-
import { SEVERITY_BADGE } from '../../utils/badge-colors'
|
|
8
|
+
import type { HelmOperation, HelmRelease, SelectedHelmRelease, UpgradeInfo, ChartSource } from '../../types'
|
|
9
|
+
import { getStatusColor, formatAge, isHelmReleaseActionable } from './helm-utils'
|
|
10
|
+
import { SEVERITY_BADGE, SEVERITY_DOT, SEVERITY_TEXT } from '../../utils/badge-colors'
|
|
11
11
|
import { Tooltip } from '../ui/Tooltip'
|
|
12
12
|
import { ChartBrowser } from './ChartBrowser'
|
|
13
13
|
import { InstallWizard } from './InstallWizard'
|
|
@@ -15,43 +15,59 @@ import { InstallWizard } from './InstallWizard'
|
|
|
15
15
|
type ViewTab = 'releases' | 'charts'
|
|
16
16
|
|
|
17
17
|
interface HelmViewProps {
|
|
18
|
-
|
|
18
|
+
namespaces: string[]
|
|
19
19
|
selectedRelease?: SelectedHelmRelease | null
|
|
20
|
-
onReleaseClick?: (namespace: string, name: string) => void
|
|
20
|
+
onReleaseClick?: (namespace: string, name: string, storageNamespace?: string) => void
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
export function HelmView({
|
|
23
|
+
export function HelmView({ namespaces, selectedRelease, onReleaseClick }: HelmViewProps) {
|
|
24
24
|
const [activeTab, setActiveTab] = useState<ViewTab>('releases')
|
|
25
25
|
const [searchTerm, setSearchTerm] = useState('')
|
|
26
26
|
const [selectedChart, setSelectedChart] = useState<{ repo: string; chart: string; version: string; source: ChartSource } | null>(null)
|
|
27
27
|
|
|
28
|
-
const { data: releases, isLoading, error: releasesError, refetch: refetchReleases } = useHelmReleases(
|
|
28
|
+
const { data: releases, isLoading, error: releasesError, dataUpdatedAt: releasesUpdatedAt, isFetching: releasesFetching, refetch: refetchReleases } = useHelmReleases(namespaces)
|
|
29
29
|
const isForbidden = isForbiddenError(releasesError)
|
|
30
|
+
const releasesErrorMessage = releasesError instanceof Error ? releasesError.message : 'Failed to load Helm releases'
|
|
30
31
|
|
|
31
32
|
// Lazy load upgrade info after releases are loaded
|
|
32
|
-
const { data: upgradeInfo, isLoading: upgradeLoading, refetch: refetchUpgradeInfo } = useHelmBatchUpgradeInfo(
|
|
33
|
-
|
|
33
|
+
const { data: upgradeInfo, isLoading: upgradeLoading, error: upgradeError, refetch: refetchUpgradeInfo } = useHelmBatchUpgradeInfo(
|
|
34
|
+
namespaces,
|
|
34
35
|
Boolean(releases && releases.length > 0)
|
|
35
36
|
)
|
|
37
|
+
const upgradeErrorMessage = upgradeError instanceof Error ? upgradeError.message : 'Upgrade checks failed'
|
|
36
38
|
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
const isFullyLoaded = !isLoading && !upgradeLoading
|
|
39
|
+
const { connection } = useConnection()
|
|
40
|
+
const refetchAll = () => Promise.all([refetchReleases(), refetchUpgradeInfo()])
|
|
42
41
|
|
|
43
42
|
// Filter releases by search term
|
|
43
|
+
// Resources-table cycle: asc → desc → off (null restores the server's order).
|
|
44
|
+
const [sort, setSort] = useState<{ key: HelmSortKey; dir: SortDir } | null>(null)
|
|
45
|
+
const onSort = useCallback(
|
|
46
|
+
(key: HelmSortKey) =>
|
|
47
|
+
setSort((prev) => {
|
|
48
|
+
if (!prev || prev.key !== key) return { key, dir: 'asc' }
|
|
49
|
+
if (prev.dir === 'asc') return { key, dir: 'desc' }
|
|
50
|
+
return null
|
|
51
|
+
}),
|
|
52
|
+
[],
|
|
53
|
+
)
|
|
54
|
+
|
|
44
55
|
const filteredReleases = useMemo(() => {
|
|
45
56
|
if (!releases) return []
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
57
|
+
const term = searchTerm.trim().toLowerCase()
|
|
58
|
+
const filtered = term
|
|
59
|
+
? releases.filter(
|
|
60
|
+
(r) =>
|
|
61
|
+
r.name.toLowerCase().includes(term) ||
|
|
62
|
+
r.namespace.toLowerCase().includes(term) ||
|
|
63
|
+
r.chart.toLowerCase().includes(term)
|
|
64
|
+
)
|
|
65
|
+
: releases
|
|
66
|
+
|
|
67
|
+
if (!sort) return filtered
|
|
68
|
+
const factor = sort.dir === 'asc' ? 1 : -1
|
|
69
|
+
return [...filtered].sort((a, b) => compareReleases(a, b, sort.key) * factor)
|
|
70
|
+
}, [releases, searchTerm, sort])
|
|
55
71
|
|
|
56
72
|
// Keyboard navigation state
|
|
57
73
|
const searchInputRef = useRef<HTMLInputElement>(null)
|
|
@@ -60,8 +76,9 @@ export function HelmView({ namespace, selectedRelease, onReleaseClick }: HelmVie
|
|
|
60
76
|
const filteredReleasesCountRef = useRef(0)
|
|
61
77
|
filteredReleasesCountRef.current = filteredReleases.length
|
|
62
78
|
|
|
63
|
-
// Reset highlight when search
|
|
64
|
-
|
|
79
|
+
// Reset highlight when the visible order changes (search or sort) — otherwise
|
|
80
|
+
// the index would point at whatever release shifted into that row.
|
|
81
|
+
useEffect(() => { setHighlightedIndex(-1) }, [searchTerm, sort])
|
|
65
82
|
|
|
66
83
|
// Scroll highlighted row into view
|
|
67
84
|
useEffect(() => {
|
|
@@ -132,7 +149,7 @@ export function HelmView({ namespace, selectedRelease, onReleaseClick }: HelmVie
|
|
|
132
149
|
scope: 'helm',
|
|
133
150
|
handler: () => {
|
|
134
151
|
const release = getHighlightedRelease()
|
|
135
|
-
if (release) onReleaseClick?.(release.namespace, release.name)
|
|
152
|
+
if (release) onReleaseClick?.(release.namespace, release.name, release.storageNamespace)
|
|
136
153
|
},
|
|
137
154
|
enabled: highlightedIndex >= 0,
|
|
138
155
|
},
|
|
@@ -165,6 +182,25 @@ export function HelmView({ namespace, selectedRelease, onReleaseClick }: HelmVie
|
|
|
165
182
|
<div className="flex h-full w-full">
|
|
166
183
|
{/* Main Content */}
|
|
167
184
|
<div className="flex-1 flex flex-col overflow-hidden min-w-0 w-full">
|
|
185
|
+
{/* Page header — consistent across views; the tabs + search sit below. */}
|
|
186
|
+
<div className="px-4 pt-4 pb-1">
|
|
187
|
+
<PageHeader
|
|
188
|
+
icon={Package}
|
|
189
|
+
title="Helm"
|
|
190
|
+
description="Installed Helm releases and the chart catalog for this cluster."
|
|
191
|
+
actions={
|
|
192
|
+
activeTab === 'releases' ? (
|
|
193
|
+
<FreshnessControl
|
|
194
|
+
mode="snapshot"
|
|
195
|
+
dataUpdatedAt={releasesUpdatedAt}
|
|
196
|
+
isFetching={releasesFetching || upgradeLoading}
|
|
197
|
+
onRefresh={refetchAll}
|
|
198
|
+
connectionState={connection.state}
|
|
199
|
+
/>
|
|
200
|
+
) : undefined
|
|
201
|
+
}
|
|
202
|
+
/>
|
|
203
|
+
</div>
|
|
168
204
|
{/* Tab bar */}
|
|
169
205
|
<div className="flex items-center gap-1 px-4 pt-3 border-b border-theme-border bg-theme-surface/50">
|
|
170
206
|
<button
|
|
@@ -202,36 +238,25 @@ export function HelmView({ namespace, selectedRelease, onReleaseClick }: HelmVie
|
|
|
202
238
|
<>
|
|
203
239
|
{/* Releases Toolbar */}
|
|
204
240
|
<div className="flex items-center gap-4 px-4 py-3 border-b border-theme-border bg-theme-surface/50 shrink-0">
|
|
205
|
-
<div className="flex items-center gap-2 text-theme-text-secondary">
|
|
206
|
-
<Package className="w-5 h-5" />
|
|
207
|
-
<span className="font-medium">Helm Releases</span>
|
|
208
|
-
{!isFullyLoaded && (
|
|
209
|
-
<RefreshCw className="w-3.5 h-3.5 animate-spin text-theme-text-tertiary" />
|
|
210
|
-
)}
|
|
211
|
-
</div>
|
|
212
241
|
<div className="flex-1 relative">
|
|
213
242
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-theme-text-tertiary" />
|
|
214
|
-
<
|
|
243
|
+
<Input
|
|
215
244
|
ref={searchInputRef}
|
|
216
|
-
type="text"
|
|
217
245
|
placeholder="Search releases..."
|
|
218
246
|
value={searchTerm}
|
|
219
247
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
220
248
|
className="w-full max-w-md pl-10 pr-4 py-2 bg-theme-elevated border border-theme-border-light rounded-lg text-sm text-theme-text-primary placeholder-theme-text-disabled focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
221
249
|
/>
|
|
222
250
|
</div>
|
|
223
|
-
<button
|
|
224
|
-
onClick={handleRefresh}
|
|
225
|
-
disabled={isRefreshAnimating}
|
|
226
|
-
className="p-2 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded-lg disabled:opacity-50"
|
|
227
|
-
title="Refresh"
|
|
228
|
-
>
|
|
229
|
-
<RefreshCw className={clsx('w-4 h-4', isRefreshAnimating && 'animate-spin')} />
|
|
230
|
-
</button>
|
|
231
251
|
</div>
|
|
232
252
|
|
|
233
253
|
{/* Releases Table */}
|
|
234
254
|
<div className="flex-1 overflow-auto">
|
|
255
|
+
{upgradeError && (
|
|
256
|
+
<div className="m-4 rounded-lg border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-sm text-amber-300">
|
|
257
|
+
Upgrade checks failed: {upgradeErrorMessage}
|
|
258
|
+
</div>
|
|
259
|
+
)}
|
|
235
260
|
{isLoading ? (
|
|
236
261
|
<PaneLoader className="h-full" />
|
|
237
262
|
) : isForbidden ? (
|
|
@@ -240,6 +265,20 @@ export function HelmView({ namespace, selectedRelease, onReleaseClick }: HelmVie
|
|
|
240
265
|
<p className="text-theme-text-secondary font-medium">Access Restricted</p>
|
|
241
266
|
<p className="text-sm mt-1">Insufficient permissions to list Helm releases</p>
|
|
242
267
|
</div>
|
|
268
|
+
) : releasesError ? (
|
|
269
|
+
<div className="flex flex-col items-center justify-center h-full text-theme-text-tertiary gap-3 px-6 text-center">
|
|
270
|
+
<Package className="w-10 h-10 text-amber-400" />
|
|
271
|
+
<div>
|
|
272
|
+
<p className="text-theme-text-secondary font-medium">Failed to load Helm releases</p>
|
|
273
|
+
<p className="text-sm mt-1 break-all">{releasesErrorMessage}</p>
|
|
274
|
+
</div>
|
|
275
|
+
<button
|
|
276
|
+
onClick={() => refetchReleases()}
|
|
277
|
+
className="px-3 py-1.5 text-sm text-theme-text-primary border border-theme-border rounded-lg hover:bg-theme-elevated transition-colors"
|
|
278
|
+
>
|
|
279
|
+
Retry
|
|
280
|
+
</button>
|
|
281
|
+
</div>
|
|
243
282
|
) : filteredReleases.length === 0 ? (
|
|
244
283
|
<div className="flex flex-col items-center justify-center h-full text-theme-text-tertiary gap-2">
|
|
245
284
|
<Package className="w-12 h-12 text-theme-text-disabled" />
|
|
@@ -263,44 +302,31 @@ export function HelmView({ namespace, selectedRelease, onReleaseClick }: HelmVie
|
|
|
263
302
|
</div>
|
|
264
303
|
) : (
|
|
265
304
|
<table className="w-full table-fixed">
|
|
266
|
-
<thead className="bg-theme-
|
|
305
|
+
<thead className="bg-theme-base sticky top-0 z-10">
|
|
267
306
|
<tr>
|
|
268
|
-
<
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
<
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
<
|
|
275
|
-
Chart
|
|
276
|
-
</th>
|
|
277
|
-
<th className="text-left px-4 py-3 text-xs font-medium text-theme-text-secondary uppercase tracking-wide w-24 hidden xl:table-cell">
|
|
278
|
-
App Version
|
|
279
|
-
</th>
|
|
280
|
-
<th className="text-left px-4 py-3 text-xs font-medium text-theme-text-secondary uppercase tracking-wide w-28">
|
|
281
|
-
Status
|
|
282
|
-
</th>
|
|
283
|
-
<th className="text-left px-4 py-3 text-xs font-medium text-theme-text-secondary uppercase tracking-wide w-20">
|
|
284
|
-
Rev
|
|
285
|
-
</th>
|
|
286
|
-
<th className="text-left px-4 py-3 text-xs font-medium text-theme-text-secondary uppercase tracking-wide w-24">
|
|
287
|
-
Updated
|
|
288
|
-
</th>
|
|
307
|
+
<SortableTh label="Name" sortKey="name" activeKey={sort?.key ?? null} direction={sort?.dir ?? 'asc'} onSort={onSort} className="w-[28%]" />
|
|
308
|
+
<SortableTh label="Namespace" sortKey="namespace" activeKey={sort?.key ?? null} direction={sort?.dir ?? 'asc'} onSort={onSort} className="w-[18%]" />
|
|
309
|
+
<SortableTh label="Chart" sortKey="chart" activeKey={sort?.key ?? null} direction={sort?.dir ?? 'asc'} onSort={onSort} className="w-[22%]" />
|
|
310
|
+
<SortableTh label="App Version" sortKey="appVersion" activeKey={sort?.key ?? null} direction={sort?.dir ?? 'asc'} onSort={onSort} className="w-24 hidden xl:table-cell" />
|
|
311
|
+
<SortableTh label="Status" sortKey="status" activeKey={sort?.key ?? null} direction={sort?.dir ?? 'asc'} onSort={onSort} className="w-40" />
|
|
312
|
+
<SortableTh label="Rev" sortKey="revision" activeKey={sort?.key ?? null} direction={sort?.dir ?? 'asc'} onSort={onSort} className="w-16" />
|
|
313
|
+
<SortableTh label="Updated" sortKey="updated" activeKey={sort?.key ?? null} direction={sort?.dir ?? 'asc'} onSort={onSort} className="w-24" />
|
|
289
314
|
</tr>
|
|
290
315
|
</thead>
|
|
291
316
|
<tbody className="table-divide-subtle">
|
|
292
317
|
{filteredReleases.map((release, index) => (
|
|
293
318
|
<ReleaseRow
|
|
294
|
-
key={
|
|
319
|
+
key={releaseIdentityKey(release)}
|
|
295
320
|
ref={index === highlightedIndex ? highlightedRowRef : null}
|
|
296
321
|
release={release}
|
|
297
|
-
upgradeInfo={upgradeInfo?.releases[
|
|
322
|
+
upgradeInfo={upgradeInfo?.releases[releaseIdentityKey(release)]}
|
|
298
323
|
isSelected={
|
|
299
324
|
selectedRelease?.namespace === release.namespace &&
|
|
300
|
-
selectedRelease?.name === release.name
|
|
325
|
+
selectedRelease?.name === release.name &&
|
|
326
|
+
(selectedRelease?.storageNamespace || selectedRelease?.namespace) === (release.storageNamespace || release.namespace)
|
|
301
327
|
}
|
|
302
328
|
isHighlighted={index === highlightedIndex}
|
|
303
|
-
onClick={() => onReleaseClick?.(release.namespace, release.name)}
|
|
329
|
+
onClick={() => onReleaseClick?.(release.namespace, release.name, release.storageNamespace)}
|
|
304
330
|
onMouseEnter={() => setHighlightedIndex(-1)}
|
|
305
331
|
/>
|
|
306
332
|
))}
|
|
@@ -329,6 +355,27 @@ export function HelmView({ namespace, selectedRelease, onReleaseClick }: HelmVie
|
|
|
329
355
|
)
|
|
330
356
|
}
|
|
331
357
|
|
|
358
|
+
function releaseIdentityKey(release: Pick<HelmRelease, 'namespace' | 'name' | 'storageNamespace'>): string {
|
|
359
|
+
return `${release.storageNamespace || release.namespace}/${release.name}`
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
type HelmSortKey = 'name' | 'namespace' | 'chart' | 'appVersion' | 'status' | 'revision' | 'updated'
|
|
363
|
+
|
|
364
|
+
function compareReleases(a: HelmRelease, b: HelmRelease, key: HelmSortKey): number {
|
|
365
|
+
let cmp: number
|
|
366
|
+
switch (key) {
|
|
367
|
+
case 'revision':
|
|
368
|
+
cmp = a.revision - b.revision
|
|
369
|
+
break
|
|
370
|
+
case 'updated':
|
|
371
|
+
cmp = (Date.parse(a.updated) || 0) - (Date.parse(b.updated) || 0)
|
|
372
|
+
break
|
|
373
|
+
default:
|
|
374
|
+
cmp = String(a[key] ?? '').localeCompare(String(b[key] ?? ''))
|
|
375
|
+
}
|
|
376
|
+
return cmp || a.name.localeCompare(b.name)
|
|
377
|
+
}
|
|
378
|
+
|
|
332
379
|
interface ReleaseRowProps {
|
|
333
380
|
release: HelmRelease
|
|
334
381
|
upgradeInfo?: UpgradeInfo
|
|
@@ -361,7 +408,7 @@ function getActionableTooltip(issue: string | undefined, summary: string | undef
|
|
|
361
408
|
<div className="max-w-xs">
|
|
362
409
|
<div className={clsx(
|
|
363
410
|
'font-medium',
|
|
364
|
-
health === 'unhealthy' ?
|
|
411
|
+
health === 'unhealthy' ? SEVERITY_TEXT.error : SEVERITY_TEXT.warning
|
|
365
412
|
)}>
|
|
366
413
|
{summary || issue || health}
|
|
367
414
|
</div>
|
|
@@ -380,16 +427,82 @@ function getActionableTooltip(issue: string | undefined, summary: string | undef
|
|
|
380
427
|
)
|
|
381
428
|
}
|
|
382
429
|
|
|
430
|
+
function getListOperation(release: HelmRelease): HelmOperation | undefined {
|
|
431
|
+
if (release.lastOperation) {
|
|
432
|
+
const releaseStatus = release.status.toLowerCase()
|
|
433
|
+
if (release.lastOperation.status === 'failed' && releaseStatus === 'failed') {
|
|
434
|
+
return undefined
|
|
435
|
+
}
|
|
436
|
+
if (isListOperation(release.lastOperation)) {
|
|
437
|
+
return release.lastOperation
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return release.operations?.find(isListOperation)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function isListOperation(operation: HelmOperation): boolean {
|
|
444
|
+
return operation.kind === 'upgrade_rolled_back' || operation.kind === 'rollback' || operation.status === 'stuck_pending'
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function HelmOperationChip({ operation }: { operation: HelmOperation }) {
|
|
448
|
+
const isPending = operation.status === 'stuck_pending'
|
|
449
|
+
const isRollback = operation.kind === 'upgrade_rolled_back' || operation.kind === 'rollback'
|
|
450
|
+
const Icon = isRollback ? RotateCcw : Clock
|
|
451
|
+
const tone: keyof typeof SEVERITY_BADGE = operation.kind === 'rollback' ? 'info' : isPending ? 'warning' : 'alert'
|
|
452
|
+
const label = operation.kind === 'upgrade_rolled_back'
|
|
453
|
+
? 'rolled back'
|
|
454
|
+
: operation.kind === 'rollback'
|
|
455
|
+
? 'rollback'
|
|
456
|
+
: 'stuck'
|
|
457
|
+
|
|
458
|
+
return (
|
|
459
|
+
<Tooltip content={
|
|
460
|
+
<div className="max-w-xs">
|
|
461
|
+
<div className="font-medium text-theme-text-primary">{operationSummary(operation)}</div>
|
|
462
|
+
<div className="mt-1 text-[10px] text-theme-text-secondary">{operation.message}</div>
|
|
463
|
+
{operation.failureDescription && (
|
|
464
|
+
<div className="mt-1.5 border-t border-theme-border pt-1.5 text-[10px] text-theme-text-tertiary">
|
|
465
|
+
{operation.failureDescription}
|
|
466
|
+
</div>
|
|
467
|
+
)}
|
|
468
|
+
</div>
|
|
469
|
+
}>
|
|
470
|
+
<span className={clsx('badge-sm shrink-0', SEVERITY_BADGE[tone])} aria-label={operationSummary(operation)}>
|
|
471
|
+
<Icon className="h-3 w-3" />
|
|
472
|
+
<span className="sr-only">{label}</span>
|
|
473
|
+
<span className="hidden 2xl:inline" aria-hidden="true">{label}</span>
|
|
474
|
+
</span>
|
|
475
|
+
</Tooltip>
|
|
476
|
+
)
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function operationSummary(operation: HelmOperation): string {
|
|
480
|
+
switch (operation.kind) {
|
|
481
|
+
case 'upgrade_rolled_back':
|
|
482
|
+
return 'Helm rolled back after failed upgrade'
|
|
483
|
+
case 'rollback':
|
|
484
|
+
return 'Helm rollback applied'
|
|
485
|
+
case 'pending':
|
|
486
|
+
return 'Helm operation may be stuck'
|
|
487
|
+
case 'upgrade_failed':
|
|
488
|
+
return 'Helm upgrade failed'
|
|
489
|
+
default:
|
|
490
|
+
return 'Helm release failed'
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
383
494
|
const ReleaseRow = forwardRef<HTMLTableRowElement, ReleaseRowProps>(
|
|
384
495
|
function ReleaseRow({ release, upgradeInfo, isSelected, isHighlighted, onClick, onMouseEnter }, ref) {
|
|
496
|
+
const listOperation = getListOperation(release)
|
|
497
|
+
|
|
385
498
|
// Health badge styling
|
|
386
499
|
const getHealthBadge = () => {
|
|
387
500
|
if (!release.resourceHealth || release.resourceHealth === 'unknown') return null
|
|
388
501
|
|
|
389
|
-
const healthStyles: Record<string, {
|
|
390
|
-
healthy: {
|
|
391
|
-
degraded: {
|
|
392
|
-
unhealthy: {
|
|
502
|
+
const healthStyles: Record<string, { badge: string; dot: string }> = {
|
|
503
|
+
healthy: { badge: SEVERITY_BADGE.success, dot: SEVERITY_DOT.success },
|
|
504
|
+
degraded: { badge: SEVERITY_BADGE.warning, dot: SEVERITY_DOT.warning },
|
|
505
|
+
unhealthy: { badge: SEVERITY_BADGE.error, dot: SEVERITY_DOT.error },
|
|
393
506
|
}
|
|
394
507
|
|
|
395
508
|
const style = healthStyles[release.resourceHealth] || healthStyles.healthy
|
|
@@ -398,8 +511,8 @@ const ReleaseRow = forwardRef<HTMLTableRowElement, ReleaseRowProps>(
|
|
|
398
511
|
return (
|
|
399
512
|
<Tooltip content={tooltipContent}>
|
|
400
513
|
<span className={clsx(
|
|
401
|
-
'
|
|
402
|
-
style.
|
|
514
|
+
'badge-sm shrink-0',
|
|
515
|
+
style.badge
|
|
403
516
|
)}>
|
|
404
517
|
<span className={clsx('w-1.5 h-1.5 rounded-full', style.dot)} />
|
|
405
518
|
{release.healthIssue || (release.resourceHealth !== 'healthy' ? release.healthSummary : null)}
|
|
@@ -423,10 +536,19 @@ const ReleaseRow = forwardRef<HTMLTableRowElement, ReleaseRowProps>(
|
|
|
423
536
|
)}
|
|
424
537
|
>
|
|
425
538
|
<td className="px-4 py-3">
|
|
426
|
-
<div className="flex items-center gap-2">
|
|
539
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
427
540
|
<Package className="w-4 h-4 text-theme-text-tertiary shrink-0" />
|
|
428
|
-
<span className="text-sm text-theme-text-primary
|
|
541
|
+
<span className="min-w-0 truncate text-sm font-medium text-theme-text-primary">{release.name}</span>
|
|
429
542
|
{getHealthBadge()}
|
|
543
|
+
{listOperation && <HelmOperationChip operation={listOperation} />}
|
|
544
|
+
{release.managedByFluxHelmRelease && (
|
|
545
|
+
<Tooltip content={`Installed by Flux helm-controller via HelmRelease ${release.managedByFluxHelmRelease}. Changes here will be reverted at the next reconcile — manage via the GitOps tab.`}>
|
|
546
|
+
<span className="badge-sm shrink-0 border border-theme-border bg-theme-elevated text-theme-text-secondary">
|
|
547
|
+
<GitBranch className="w-3 h-3" />
|
|
548
|
+
Flux
|
|
549
|
+
</span>
|
|
550
|
+
</Tooltip>
|
|
551
|
+
)}
|
|
430
552
|
{upgradeInfo?.updateAvailable && (
|
|
431
553
|
<Tooltip content={`Upgrade available: ${release.chartVersion} → ${upgradeInfo.latestVersion}`}>
|
|
432
554
|
<span className={clsx('badge-sm shrink-0', SEVERITY_BADGE.warning)}>
|
|
@@ -436,30 +558,49 @@ const ReleaseRow = forwardRef<HTMLTableRowElement, ReleaseRowProps>(
|
|
|
436
558
|
)}
|
|
437
559
|
</div>
|
|
438
560
|
</td>
|
|
439
|
-
<td className="px-4 py-3 w-
|
|
440
|
-
<
|
|
561
|
+
<td className="px-4 py-3 w-[18%]">
|
|
562
|
+
<Tooltip content={release.namespace}>
|
|
563
|
+
<span className="block truncate text-sm text-theme-text-secondary">{release.namespace}</span>
|
|
564
|
+
</Tooltip>
|
|
441
565
|
</td>
|
|
442
|
-
<td className="px-4 py-3 w-
|
|
566
|
+
<td className="px-4 py-3 w-[22%]">
|
|
443
567
|
<Tooltip content={`${release.chart}-${release.chartVersion}`}>
|
|
444
568
|
<span className="text-sm text-theme-text-secondary truncate block">
|
|
445
|
-
{
|
|
569
|
+
{release.chart}-{release.chartVersion}
|
|
446
570
|
</span>
|
|
447
571
|
</Tooltip>
|
|
448
572
|
</td>
|
|
449
573
|
<td className="px-4 py-3 w-24 hidden xl:table-cell">
|
|
450
|
-
|
|
574
|
+
{release.appVersion ? (
|
|
575
|
+
<span className="text-sm text-theme-text-secondary">{release.appVersion}</span>
|
|
576
|
+
) : (
|
|
577
|
+
<Tooltip content="This chart did not declare an appVersion in Chart.yaml.">
|
|
578
|
+
<span className="text-sm text-theme-text-disabled cursor-help">—</span>
|
|
579
|
+
</Tooltip>
|
|
580
|
+
)}
|
|
451
581
|
</td>
|
|
452
|
-
<td className="px-4 py-3 w-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
582
|
+
<td className="px-4 py-3 w-40">
|
|
583
|
+
{isHelmReleaseActionable(release.status) ? (
|
|
584
|
+
<Tooltip content="Click row to view rollback / history / logs and recover">
|
|
585
|
+
<span
|
|
586
|
+
className={clsx('badge inline-flex items-center gap-1', getStatusColor(release.status))}
|
|
587
|
+
>
|
|
588
|
+
{release.status}
|
|
589
|
+
<ChevronRight className="w-3 h-3 opacity-70" />
|
|
590
|
+
</span>
|
|
591
|
+
</Tooltip>
|
|
592
|
+
) : (
|
|
593
|
+
<span
|
|
594
|
+
className={clsx(
|
|
595
|
+
'badge',
|
|
596
|
+
getStatusColor(release.status)
|
|
597
|
+
)}
|
|
598
|
+
>
|
|
599
|
+
{release.status}
|
|
600
|
+
</span>
|
|
601
|
+
)}
|
|
461
602
|
</td>
|
|
462
|
-
<td className="px-4 py-3 w-
|
|
603
|
+
<td className="px-4 py-3 w-16">
|
|
463
604
|
<span className="text-sm text-theme-text-secondary">{release.revision}</span>
|
|
464
605
|
</td>
|
|
465
606
|
<td className="px-4 py-3 w-24">
|