@skyhook-io/radar-app 0.1.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 +67 -0
- package/package.json +80 -0
- package/src/App.tsx +1538 -0
- package/src/RadarApp.tsx +145 -0
- package/src/api/apiResources.ts +28 -0
- package/src/api/client.ts +2583 -0
- package/src/api/config.ts +116 -0
- package/src/api/traffic.ts +139 -0
- package/src/components/ConnectionErrorView.tsx +272 -0
- package/src/components/ContextSwitcher.tsx +481 -0
- package/src/components/DebugOverlay.tsx +94 -0
- package/src/components/UserMenu.tsx +87 -0
- package/src/components/audit/AuditSettingsDialog.tsx +162 -0
- package/src/components/audit/AuditView.tsx +123 -0
- package/src/components/cost/CostTrendChart.tsx +388 -0
- package/src/components/cost/CostView.tsx +545 -0
- package/src/components/dock/BottomDock.tsx +96 -0
- package/src/components/dock/DockContext.tsx +11 -0
- package/src/components/dock/LocalTerminalTab.tsx +22 -0
- package/src/components/dock/LogsTab.tsx +26 -0
- package/src/components/dock/NodeTerminalTab.tsx +50 -0
- package/src/components/dock/TerminalTab.tsx +42 -0
- package/src/components/dock/TrafficFlowListTab.tsx +18 -0
- package/src/components/dock/WorkloadLogsTab.tsx +23 -0
- package/src/components/dock/index.ts +2 -0
- package/src/components/gitops/GitOpsActions.tsx +1 -0
- package/src/components/gitops/GitOpsStatusBadge.tsx +1 -0
- package/src/components/gitops/ManagedResourcesList.tsx +1 -0
- package/src/components/gitops/SyncCountdown.tsx +1 -0
- package/src/components/gitops/index.ts +4 -0
- package/src/components/helm/ChartBrowser.tsx +580 -0
- package/src/components/helm/HelmReleaseDrawer.tsx +774 -0
- package/src/components/helm/HelmView.tsx +475 -0
- package/src/components/helm/InstallWizard.tsx +1060 -0
- package/src/components/helm/ManifestDiffViewer.tsx +91 -0
- package/src/components/helm/ManifestViewer.tsx +61 -0
- package/src/components/helm/OwnedResources.tsx +465 -0
- package/src/components/helm/RevisionHistory.tsx +167 -0
- package/src/components/helm/ValuesDiffPreview.tsx +190 -0
- package/src/components/helm/ValuesViewer.tsx +365 -0
- package/src/components/helm/helm-utils.ts +37 -0
- package/src/components/home/ActivitySummary.tsx +262 -0
- package/src/components/home/CertificateHealthCard.tsx +105 -0
- package/src/components/home/ClusterHealthCard.tsx +483 -0
- package/src/components/home/CostCard.tsx +112 -0
- package/src/components/home/HealthRing.tsx +1 -0
- package/src/components/home/HelmSummary.tsx +129 -0
- package/src/components/home/HomeView.tsx +224 -0
- package/src/components/home/MCPSetupDialog.tsx +417 -0
- package/src/components/home/NetworkPolicyCoverageCard.tsx +109 -0
- package/src/components/home/TopologyPreview.tsx +219 -0
- package/src/components/home/TrafficSummary.tsx +154 -0
- package/src/components/logs/JsonLogLine.tsx +1 -0
- package/src/components/logs/LogCore.tsx +2 -0
- package/src/components/logs/LogsViewer.tsx +44 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +40 -0
- package/src/components/logs/useLogBuffer.ts +2 -0
- package/src/components/logs/useLogSearch.ts +1 -0
- package/src/components/portforward/PortForwardButton.tsx +375 -0
- package/src/components/portforward/PortForwardManager.tsx +871 -0
- package/src/components/resource/PrometheusCharts.tsx +687 -0
- package/src/components/resource-drawer/ResourceDrawer.tsx +214 -0
- package/src/components/resources/ImageFilesystemModal.tsx +745 -0
- package/src/components/resources/PodFilesystemModal.tsx +407 -0
- package/src/components/resources/ResourceDetailDrawer.tsx +43 -0
- package/src/components/resources/ResourcesView.tsx +190 -0
- package/src/components/resources/drawer-components.tsx +1 -0
- package/src/components/resources/file-browser-utils.ts +35 -0
- package/src/components/resources/renderers/AlertRenderer.tsx +1 -0
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +17 -0
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +1 -0
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +1 -0
- package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +1 -0
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +1 -0
- package/src/components/resources/renderers/CertificateRenderer.tsx +1 -0
- package/src/components/resources/renderers/CertificateRequestRenderer.tsx +1 -0
- package/src/components/resources/renderers/ChallengeRenderer.tsx +1 -0
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +1 -0
- package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +1 -0
- package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +1 -0
- package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +1 -0
- package/src/components/resources/renderers/ConfigMapRenderer.tsx +1 -0
- package/src/components/resources/renderers/CronJobRenderer.tsx +1 -0
- package/src/components/resources/renderers/EventRenderer.tsx +1 -0
- package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +1 -0
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +1 -0
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +1 -0
- package/src/components/resources/renderers/GRPCRouteRenderer.tsx +1 -0
- package/src/components/resources/renderers/GatewayClassRenderer.tsx +1 -0
- package/src/components/resources/renderers/GatewayRenderer.tsx +1 -0
- package/src/components/resources/renderers/GenericRenderer.tsx +1 -0
- package/src/components/resources/renderers/GitRepositoryRenderer.tsx +1 -0
- package/src/components/resources/renderers/HPARenderer.tsx +1 -0
- package/src/components/resources/renderers/HTTPRouteRenderer.tsx +1 -0
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +1 -0
- package/src/components/resources/renderers/IngressClassRenderer.tsx +1 -0
- package/src/components/resources/renderers/IngressRenderer.tsx +1 -0
- package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +1 -0
- package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +1 -0
- package/src/components/resources/renderers/IstioGatewayRenderer.tsx +1 -0
- package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +1 -0
- package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +1 -0
- package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +1 -0
- package/src/components/resources/renderers/JobRenderer.tsx +1 -0
- package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +1 -0
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +1 -0
- package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +1 -0
- package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +1 -0
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +1 -0
- package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +1 -0
- package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +1 -0
- package/src/components/resources/renderers/KnativeEventingRenderer.tsx +1 -0
- package/src/components/resources/renderers/KnativeFlowRenderer.tsx +1 -0
- package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +1 -0
- package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +1 -0
- package/src/components/resources/renderers/KnativeRouteRenderer.tsx +1 -0
- package/src/components/resources/renderers/KnativeServiceRenderer.tsx +1 -0
- package/src/components/resources/renderers/KnativeSourceRenderer.tsx +1 -0
- package/src/components/resources/renderers/KustomizationRenderer.tsx +1 -0
- package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +1 -0
- package/src/components/resources/renderers/LeaseRenderer.tsx +1 -0
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +1 -0
- package/src/components/resources/renderers/NodeRenderer.tsx +44 -0
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +1 -0
- package/src/components/resources/renderers/OrderRenderer.tsx +1 -0
- package/src/components/resources/renderers/PVCRenderer.tsx +1 -0
- package/src/components/resources/renderers/PersistentVolumeRenderer.tsx +1 -0
- package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +1 -0
- package/src/components/resources/renderers/PodMonitorRenderer.tsx +1 -0
- package/src/components/resources/renderers/PodRenderer.tsx +94 -0
- package/src/components/resources/renderers/PriorityClassRenderer.tsx +1 -0
- package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +1 -0
- package/src/components/resources/renderers/ReplicaSetRenderer.tsx +1 -0
- package/src/components/resources/renderers/RoleBindingRenderer.tsx +1 -0
- package/src/components/resources/renderers/RoleRenderer.tsx +1 -0
- package/src/components/resources/renderers/RolloutRenderer.tsx +1 -0
- package/src/components/resources/renderers/RuntimeClassRenderer.tsx +1 -0
- package/src/components/resources/renderers/SbomReportRenderer.tsx +1 -0
- package/src/components/resources/renderers/SealedSecretRenderer.tsx +1 -0
- package/src/components/resources/renderers/SecretRenderer.tsx +1 -0
- package/src/components/resources/renderers/SecretStoreRenderer.tsx +1 -0
- package/src/components/resources/renderers/ServiceAccountRenderer.tsx +1 -0
- package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +1 -0
- package/src/components/resources/renderers/ServiceRenderer.tsx +26 -0
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +1 -0
- package/src/components/resources/renderers/StorageClassRenderer.tsx +1 -0
- package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +1 -0
- package/src/components/resources/renderers/VPARenderer.tsx +1 -0
- package/src/components/resources/renderers/VeleroBSLRenderer.tsx +1 -0
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +1 -0
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +1 -0
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +1 -0
- package/src/components/resources/renderers/VeleroVSLRenderer.tsx +1 -0
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +1 -0
- package/src/components/resources/renderers/WebhookConfigRenderer.tsx +1 -0
- package/src/components/resources/renderers/WorkflowRenderer.tsx +1 -0
- package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +1 -0
- package/src/components/resources/renderers/WorkloadRenderer.tsx +52 -0
- package/src/components/resources/renderers/argo-cells.tsx +1 -0
- package/src/components/resources/renderers/certmanager-cells.tsx +1 -0
- package/src/components/resources/renderers/cnpg-cells.tsx +1 -0
- package/src/components/resources/renderers/eso-cells.tsx +1 -0
- package/src/components/resources/renderers/flux-cells.tsx +1 -0
- package/src/components/resources/renderers/index.ts +91 -0
- package/src/components/resources/renderers/istio-cells.tsx +1 -0
- package/src/components/resources/renderers/karpenter-cells.tsx +1 -0
- package/src/components/resources/renderers/keda-cells.tsx +1 -0
- package/src/components/resources/renderers/knative-cells.tsx +1 -0
- package/src/components/resources/renderers/kyverno-cells.tsx +1 -0
- package/src/components/resources/renderers/prometheus-cells.tsx +1 -0
- package/src/components/resources/renderers/traefik-cells.tsx +1 -0
- package/src/components/resources/renderers/trivy-cells.tsx +1 -0
- package/src/components/resources/renderers/trivy-shared.tsx +1 -0
- package/src/components/resources/renderers/velero-cells.tsx +1 -0
- package/src/components/resources/resource-utils-argo.ts +2 -0
- package/src/components/resources/resource-utils-certmanager.ts +2 -0
- package/src/components/resources/resource-utils-cnpg.ts +2 -0
- package/src/components/resources/resource-utils-eso.ts +2 -0
- package/src/components/resources/resource-utils-flux.ts +2 -0
- package/src/components/resources/resource-utils-istio.ts +2 -0
- package/src/components/resources/resource-utils-karpenter.ts +2 -0
- package/src/components/resources/resource-utils-keda.ts +2 -0
- package/src/components/resources/resource-utils-knative.ts +2 -0
- package/src/components/resources/resource-utils-kyverno.ts +2 -0
- package/src/components/resources/resource-utils-prometheus.ts +2 -0
- package/src/components/resources/resource-utils-traefik.ts +1 -0
- package/src/components/resources/resource-utils-trivy.ts +2 -0
- package/src/components/resources/resource-utils-velero.ts +2 -0
- package/src/components/resources/resource-utils.ts +5 -0
- package/src/components/settings/SettingsDialog.tsx +537 -0
- package/src/components/shared/CreateResourceDialog.tsx +17 -0
- package/src/components/shared/EditableYamlView.tsx +24 -0
- package/src/components/shared/LargeClusterNamespacePicker.tsx +70 -0
- package/src/components/shared/ResourceRendererDispatch.tsx +31 -0
- package/src/components/timeline/DiffViewer.tsx +1 -0
- package/src/components/timeline/TimelineList.tsx +69 -0
- package/src/components/timeline/TimelineSwimlanes.tsx +1308 -0
- package/src/components/timeline/TimelineView.tsx +157 -0
- package/src/components/timeline/shared.tsx +1 -0
- package/src/components/traffic/TrafficFilterSidebar.tsx +571 -0
- package/src/components/traffic/TrafficFlowList.tsx +415 -0
- package/src/components/traffic/TrafficFlowListContext.tsx +68 -0
- package/src/components/traffic/TrafficGraph.tsx +1546 -0
- package/src/components/traffic/TrafficView.tsx +1213 -0
- package/src/components/traffic/TrafficWizard.tsx +386 -0
- package/src/components/traffic/index.ts +3 -0
- package/src/components/ui/CodeViewer.tsx +8 -0
- package/src/components/ui/CommandPalette.tsx +460 -0
- package/src/components/ui/ConfirmDialog.tsx +1 -0
- package/src/components/ui/DiagnosticsOverlay.tsx +619 -0
- package/src/components/ui/ErrorBoundary.tsx +46 -0
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +1 -0
- package/src/components/ui/Markdown.tsx +108 -0
- package/src/components/ui/MetricsChart.tsx +1 -0
- package/src/components/ui/NamespaceSelector.tsx +436 -0
- package/src/components/ui/ResourceBar.tsx +1 -0
- package/src/components/ui/ShortcutHelpOverlay.tsx +301 -0
- package/src/components/ui/Toast.tsx +1 -0
- package/src/components/ui/Tooltip.tsx +1 -0
- package/src/components/ui/UpdateNotification.tsx +299 -0
- package/src/components/ui/YamlEditor.tsx +1 -0
- package/src/components/workload/WorkloadView.tsx +532 -0
- package/src/context/ConnectionContext.tsx +173 -0
- package/src/context/ContextSwitchContext.tsx +56 -0
- package/src/context/NavCustomization.tsx +62 -0
- package/src/context/ThemeContext.tsx +97 -0
- package/src/contexts/CapabilitiesContext.tsx +130 -0
- package/src/hooks/useAnimatedUnmount.ts +1 -0
- package/src/hooks/useDesktopDownload.ts +41 -0
- package/src/hooks/useEventSource.ts +262 -0
- package/src/hooks/useFavorites.ts +69 -0
- package/src/hooks/useKeyboardShortcuts.tsx +7 -0
- package/src/hooks/useRefreshAnimation.ts +1 -0
- package/src/index.css +243 -0
- package/src/index.ts +17 -0
- package/src/main.tsx +158 -0
- package/src/types/gitops.ts +2 -0
- package/src/types.ts +3 -0
- package/src/utils/animation.ts +2 -0
- package/src/utils/badge-colors.ts +2 -0
- package/src/utils/context-name.ts +2 -0
- package/src/utils/desktop-download.ts +66 -0
- package/src/utils/desktop-open-folder.ts +21 -0
- package/src/utils/format.ts +2 -0
- package/src/utils/log-format.ts +12 -0
- package/src/utils/navigation.ts +23 -0
- package/src/utils/resource-hierarchy.ts +2 -0
- package/src/utils/resource-icons.ts +2 -0
- package/src/utils/skeleton-yaml.ts +2 -0
- package/src/utils/traffic-colors.ts +54 -0
- package/src/vite-env.d.ts +1 -0
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import { useState, useMemo, useRef, useEffect, useCallback, forwardRef } from 'react'
|
|
2
|
+
import { useRefreshAnimation } from '../../hooks/useRefreshAnimation'
|
|
3
|
+
import { useRegisterShortcuts } from '../../hooks/useKeyboardShortcuts'
|
|
4
|
+
import { Package, Search, RefreshCw, ArrowUpCircle, LayoutGrid, List, Shield } from 'lucide-react'
|
|
5
|
+
import { clsx } from 'clsx'
|
|
6
|
+
import { useHelmReleases, useHelmBatchUpgradeInfo, isForbiddenError } from '../../api/client'
|
|
7
|
+
import type { HelmRelease, SelectedHelmRelease, UpgradeInfo, ChartSource } from '../../types'
|
|
8
|
+
import { getStatusColor, formatAge, truncate } from './helm-utils'
|
|
9
|
+
import { SEVERITY_BADGE } from '../../utils/badge-colors'
|
|
10
|
+
import { Tooltip } from '../ui/Tooltip'
|
|
11
|
+
import { ChartBrowser } from './ChartBrowser'
|
|
12
|
+
import { InstallWizard } from './InstallWizard'
|
|
13
|
+
|
|
14
|
+
type ViewTab = 'releases' | 'charts'
|
|
15
|
+
|
|
16
|
+
interface HelmViewProps {
|
|
17
|
+
namespace: string
|
|
18
|
+
selectedRelease?: SelectedHelmRelease | null
|
|
19
|
+
onReleaseClick?: (namespace: string, name: string) => void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function HelmView({ namespace, selectedRelease, onReleaseClick }: HelmViewProps) {
|
|
23
|
+
const [activeTab, setActiveTab] = useState<ViewTab>('releases')
|
|
24
|
+
const [searchTerm, setSearchTerm] = useState('')
|
|
25
|
+
const [selectedChart, setSelectedChart] = useState<{ repo: string; chart: string; version: string; source: ChartSource } | null>(null)
|
|
26
|
+
|
|
27
|
+
const { data: releases, isLoading, error: releasesError, refetch: refetchReleases } = useHelmReleases(namespace || undefined)
|
|
28
|
+
const isForbidden = isForbiddenError(releasesError)
|
|
29
|
+
|
|
30
|
+
// Lazy load upgrade info after releases are loaded
|
|
31
|
+
const { data: upgradeInfo, isLoading: upgradeLoading, refetch: refetchUpgradeInfo } = useHelmBatchUpgradeInfo(
|
|
32
|
+
namespace || undefined,
|
|
33
|
+
Boolean(releases && releases.length > 0)
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
const [handleRefresh, isRefreshAnimating] = useRefreshAnimation(async () => {
|
|
37
|
+
await Promise.all([refetchReleases(), refetchUpgradeInfo()])
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const isFullyLoaded = !isLoading && !upgradeLoading
|
|
41
|
+
|
|
42
|
+
// Filter releases by search term
|
|
43
|
+
const filteredReleases = useMemo(() => {
|
|
44
|
+
if (!releases) return []
|
|
45
|
+
if (!searchTerm) return releases
|
|
46
|
+
const term = searchTerm.toLowerCase()
|
|
47
|
+
return releases.filter(
|
|
48
|
+
(r) =>
|
|
49
|
+
r.name.toLowerCase().includes(term) ||
|
|
50
|
+
r.namespace.toLowerCase().includes(term) ||
|
|
51
|
+
r.chart.toLowerCase().includes(term)
|
|
52
|
+
)
|
|
53
|
+
}, [releases, searchTerm])
|
|
54
|
+
|
|
55
|
+
// Keyboard navigation state
|
|
56
|
+
const searchInputRef = useRef<HTMLInputElement>(null)
|
|
57
|
+
const highlightedRowRef = useRef<HTMLTableRowElement>(null)
|
|
58
|
+
const [highlightedIndex, setHighlightedIndex] = useState(-1)
|
|
59
|
+
const filteredReleasesCountRef = useRef(0)
|
|
60
|
+
filteredReleasesCountRef.current = filteredReleases.length
|
|
61
|
+
|
|
62
|
+
// Reset highlight when search changes
|
|
63
|
+
useEffect(() => { setHighlightedIndex(-1) }, [searchTerm])
|
|
64
|
+
|
|
65
|
+
// Scroll highlighted row into view
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (highlightedIndex >= 0 && highlightedRowRef.current) {
|
|
68
|
+
highlightedRowRef.current.scrollIntoView({ block: 'nearest' })
|
|
69
|
+
}
|
|
70
|
+
}, [highlightedIndex])
|
|
71
|
+
|
|
72
|
+
// Helper: get release at highlighted index
|
|
73
|
+
const getHighlightedRelease = useCallback(() => {
|
|
74
|
+
if (highlightedIndex < 0 || highlightedIndex >= filteredReleases.length) return null
|
|
75
|
+
return filteredReleases[highlightedIndex]
|
|
76
|
+
}, [highlightedIndex, filteredReleases])
|
|
77
|
+
|
|
78
|
+
// Register Helm keyboard shortcuts
|
|
79
|
+
useRegisterShortcuts([
|
|
80
|
+
{
|
|
81
|
+
id: 'helm-search',
|
|
82
|
+
keys: '/',
|
|
83
|
+
description: 'Focus search',
|
|
84
|
+
category: 'Search',
|
|
85
|
+
scope: 'helm',
|
|
86
|
+
handler: () => searchInputRef.current?.focus(),
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: 'helm-nav-down',
|
|
90
|
+
keys: 'j',
|
|
91
|
+
description: 'Next row',
|
|
92
|
+
category: 'Helm',
|
|
93
|
+
scope: 'helm',
|
|
94
|
+
handler: () => setHighlightedIndex(i => {
|
|
95
|
+
const max = filteredReleasesCountRef.current - 1
|
|
96
|
+
return i < max ? i + 1 : i
|
|
97
|
+
}),
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: 'helm-nav-down-arrow',
|
|
101
|
+
keys: 'ArrowDown',
|
|
102
|
+
description: 'Next row',
|
|
103
|
+
category: 'Helm',
|
|
104
|
+
scope: 'helm',
|
|
105
|
+
handler: () => setHighlightedIndex(i => {
|
|
106
|
+
const max = filteredReleasesCountRef.current - 1
|
|
107
|
+
return i < max ? i + 1 : i
|
|
108
|
+
}),
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
id: 'helm-nav-up',
|
|
112
|
+
keys: 'k',
|
|
113
|
+
description: 'Previous row',
|
|
114
|
+
category: 'Helm',
|
|
115
|
+
scope: 'helm',
|
|
116
|
+
handler: () => setHighlightedIndex(i => i > 0 ? i - 1 : 0),
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
id: 'helm-nav-up-arrow',
|
|
120
|
+
keys: 'ArrowUp',
|
|
121
|
+
description: 'Previous row',
|
|
122
|
+
category: 'Helm',
|
|
123
|
+
scope: 'helm',
|
|
124
|
+
handler: () => setHighlightedIndex(i => i > 0 ? i - 1 : 0),
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
id: 'helm-open',
|
|
128
|
+
keys: 'Enter',
|
|
129
|
+
description: 'Open release detail',
|
|
130
|
+
category: 'Helm',
|
|
131
|
+
scope: 'helm',
|
|
132
|
+
handler: () => {
|
|
133
|
+
const release = getHighlightedRelease()
|
|
134
|
+
if (release) onReleaseClick?.(release.namespace, release.name)
|
|
135
|
+
},
|
|
136
|
+
enabled: highlightedIndex >= 0,
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: 'helm-clear-highlight',
|
|
140
|
+
keys: 'Escape',
|
|
141
|
+
description: 'Clear highlight / blur search',
|
|
142
|
+
category: 'Helm',
|
|
143
|
+
scope: 'helm',
|
|
144
|
+
handler: () => {
|
|
145
|
+
if (highlightedIndex >= 0) setHighlightedIndex(-1)
|
|
146
|
+
else searchInputRef.current?.blur()
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
])
|
|
150
|
+
|
|
151
|
+
const handleChartSelect = (repo: string, chart: string, version: string, source: ChartSource) => {
|
|
152
|
+
setSelectedChart({ repo, chart, version, source })
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const handleInstallSuccess = (releaseNamespace: string, releaseName: string) => {
|
|
156
|
+
setSelectedChart(null)
|
|
157
|
+
setActiveTab('releases')
|
|
158
|
+
refetchReleases()
|
|
159
|
+
// Navigate to the new release
|
|
160
|
+
onReleaseClick?.(releaseNamespace, releaseName)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<div className="flex h-full w-full">
|
|
165
|
+
{/* Main Content */}
|
|
166
|
+
<div className="flex-1 flex flex-col overflow-hidden min-w-0 w-full">
|
|
167
|
+
{/* Tab bar */}
|
|
168
|
+
<div className="flex items-center gap-1 px-4 pt-3 border-b border-theme-border bg-theme-surface/50">
|
|
169
|
+
<button
|
|
170
|
+
onClick={() => setActiveTab('releases')}
|
|
171
|
+
className={clsx(
|
|
172
|
+
'flex items-center gap-2 px-4 py-2.5 text-sm font-medium border-b-2 -mb-px transition-colors',
|
|
173
|
+
activeTab === 'releases'
|
|
174
|
+
? 'text-theme-text-primary border-blue-500'
|
|
175
|
+
: 'text-theme-text-secondary border-transparent hover:text-theme-text-primary hover:border-theme-border'
|
|
176
|
+
)}
|
|
177
|
+
>
|
|
178
|
+
<List className="w-4 h-4" />
|
|
179
|
+
Installed
|
|
180
|
+
{releases && (
|
|
181
|
+
<span className="badge-sm bg-theme-elevated">
|
|
182
|
+
{releases.length}
|
|
183
|
+
</span>
|
|
184
|
+
)}
|
|
185
|
+
</button>
|
|
186
|
+
<button
|
|
187
|
+
onClick={() => setActiveTab('charts')}
|
|
188
|
+
className={clsx(
|
|
189
|
+
'flex items-center gap-2 px-4 py-2.5 text-sm font-medium border-b-2 -mb-px transition-colors',
|
|
190
|
+
activeTab === 'charts'
|
|
191
|
+
? 'text-theme-text-primary border-blue-500'
|
|
192
|
+
: 'text-theme-text-secondary border-transparent hover:text-theme-text-primary hover:border-theme-border'
|
|
193
|
+
)}
|
|
194
|
+
>
|
|
195
|
+
<LayoutGrid className="w-4 h-4" />
|
|
196
|
+
Catalog
|
|
197
|
+
</button>
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
{activeTab === 'releases' ? (
|
|
201
|
+
<>
|
|
202
|
+
{/* Releases Toolbar */}
|
|
203
|
+
<div className="flex items-center gap-4 px-4 py-3 border-b border-theme-border bg-theme-surface/50 shrink-0">
|
|
204
|
+
<div className="flex items-center gap-2 text-theme-text-secondary">
|
|
205
|
+
<Package className="w-5 h-5" />
|
|
206
|
+
<span className="font-medium">Helm Releases</span>
|
|
207
|
+
{!isFullyLoaded && (
|
|
208
|
+
<RefreshCw className="w-3.5 h-3.5 animate-spin text-theme-text-tertiary" />
|
|
209
|
+
)}
|
|
210
|
+
</div>
|
|
211
|
+
<div className="flex-1 relative">
|
|
212
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-theme-text-tertiary" />
|
|
213
|
+
<input
|
|
214
|
+
ref={searchInputRef}
|
|
215
|
+
type="text"
|
|
216
|
+
placeholder="Search releases..."
|
|
217
|
+
value={searchTerm}
|
|
218
|
+
onChange={(e) => setSearchTerm(e.target.value)}
|
|
219
|
+
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"
|
|
220
|
+
/>
|
|
221
|
+
</div>
|
|
222
|
+
<button
|
|
223
|
+
onClick={handleRefresh}
|
|
224
|
+
disabled={isRefreshAnimating}
|
|
225
|
+
className="p-2 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded-lg disabled:opacity-50"
|
|
226
|
+
title="Refresh"
|
|
227
|
+
>
|
|
228
|
+
<RefreshCw className={clsx('w-4 h-4', isRefreshAnimating && 'animate-spin')} />
|
|
229
|
+
</button>
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
{/* Releases Table */}
|
|
233
|
+
<div className="flex-1 overflow-auto">
|
|
234
|
+
{isLoading ? (
|
|
235
|
+
<div className="flex items-center justify-center h-full text-theme-text-tertiary">
|
|
236
|
+
Loading...
|
|
237
|
+
</div>
|
|
238
|
+
) : isForbidden ? (
|
|
239
|
+
<div className="flex flex-col items-center justify-center h-full text-theme-text-tertiary">
|
|
240
|
+
<Shield className="w-8 h-8 text-amber-400 mb-2" />
|
|
241
|
+
<p className="text-theme-text-secondary font-medium">Access Restricted</p>
|
|
242
|
+
<p className="text-sm mt-1">Insufficient permissions to list Helm releases</p>
|
|
243
|
+
</div>
|
|
244
|
+
) : filteredReleases.length === 0 ? (
|
|
245
|
+
<div className="flex flex-col items-center justify-center h-full text-theme-text-tertiary gap-2">
|
|
246
|
+
<Package className="w-12 h-12 text-theme-text-disabled" />
|
|
247
|
+
<span>No Helm releases found</span>
|
|
248
|
+
{searchTerm && (
|
|
249
|
+
<button
|
|
250
|
+
onClick={() => setSearchTerm('')}
|
|
251
|
+
className="text-blue-400 hover:text-blue-300 text-sm"
|
|
252
|
+
>
|
|
253
|
+
Clear search
|
|
254
|
+
</button>
|
|
255
|
+
)}
|
|
256
|
+
{!searchTerm && (
|
|
257
|
+
<button
|
|
258
|
+
onClick={() => setActiveTab('charts')}
|
|
259
|
+
className="mt-2 px-4 py-2 text-sm text-skyhook-400 hover:text-skyhook-300 border border-skyhook-500/30 rounded-lg hover:bg-skyhook-500/10 transition-colors"
|
|
260
|
+
>
|
|
261
|
+
Browse charts to install
|
|
262
|
+
</button>
|
|
263
|
+
)}
|
|
264
|
+
</div>
|
|
265
|
+
) : (
|
|
266
|
+
<table className="w-full table-fixed">
|
|
267
|
+
<thead className="bg-theme-surface sticky top-0 z-10">
|
|
268
|
+
<tr>
|
|
269
|
+
<th className="text-left px-4 py-3 text-xs font-medium text-theme-text-secondary uppercase tracking-wide">
|
|
270
|
+
Name
|
|
271
|
+
</th>
|
|
272
|
+
<th className="text-left px-4 py-3 text-xs font-medium text-theme-text-secondary uppercase tracking-wide w-32">
|
|
273
|
+
Namespace
|
|
274
|
+
</th>
|
|
275
|
+
<th className="text-left px-4 py-3 text-xs font-medium text-theme-text-secondary uppercase tracking-wide w-48">
|
|
276
|
+
Chart
|
|
277
|
+
</th>
|
|
278
|
+
<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">
|
|
279
|
+
App Version
|
|
280
|
+
</th>
|
|
281
|
+
<th className="text-left px-4 py-3 text-xs font-medium text-theme-text-secondary uppercase tracking-wide w-28">
|
|
282
|
+
Status
|
|
283
|
+
</th>
|
|
284
|
+
<th className="text-left px-4 py-3 text-xs font-medium text-theme-text-secondary uppercase tracking-wide w-20">
|
|
285
|
+
Rev
|
|
286
|
+
</th>
|
|
287
|
+
<th className="text-left px-4 py-3 text-xs font-medium text-theme-text-secondary uppercase tracking-wide w-24">
|
|
288
|
+
Updated
|
|
289
|
+
</th>
|
|
290
|
+
</tr>
|
|
291
|
+
</thead>
|
|
292
|
+
<tbody className="table-divide-subtle">
|
|
293
|
+
{filteredReleases.map((release, index) => (
|
|
294
|
+
<ReleaseRow
|
|
295
|
+
key={`${release.namespace}-${release.name}`}
|
|
296
|
+
ref={index === highlightedIndex ? highlightedRowRef : null}
|
|
297
|
+
release={release}
|
|
298
|
+
upgradeInfo={upgradeInfo?.releases[`${release.namespace}/${release.name}`]}
|
|
299
|
+
isSelected={
|
|
300
|
+
selectedRelease?.namespace === release.namespace &&
|
|
301
|
+
selectedRelease?.name === release.name
|
|
302
|
+
}
|
|
303
|
+
isHighlighted={index === highlightedIndex}
|
|
304
|
+
onClick={() => onReleaseClick?.(release.namespace, release.name)}
|
|
305
|
+
onMouseEnter={() => setHighlightedIndex(-1)}
|
|
306
|
+
/>
|
|
307
|
+
))}
|
|
308
|
+
</tbody>
|
|
309
|
+
</table>
|
|
310
|
+
)}
|
|
311
|
+
</div>
|
|
312
|
+
</>
|
|
313
|
+
) : (
|
|
314
|
+
<ChartBrowser onChartSelect={handleChartSelect} />
|
|
315
|
+
)}
|
|
316
|
+
</div>
|
|
317
|
+
|
|
318
|
+
{/* Install wizard modal */}
|
|
319
|
+
{selectedChart && (
|
|
320
|
+
<InstallWizard
|
|
321
|
+
repo={selectedChart.repo}
|
|
322
|
+
chartName={selectedChart.chart}
|
|
323
|
+
version={selectedChart.version}
|
|
324
|
+
source={selectedChart.source}
|
|
325
|
+
onClose={() => setSelectedChart(null)}
|
|
326
|
+
onSuccess={handleInstallSuccess}
|
|
327
|
+
/>
|
|
328
|
+
)}
|
|
329
|
+
</div>
|
|
330
|
+
)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
interface ReleaseRowProps {
|
|
334
|
+
release: HelmRelease
|
|
335
|
+
upgradeInfo?: UpgradeInfo
|
|
336
|
+
isSelected: boolean
|
|
337
|
+
isHighlighted?: boolean
|
|
338
|
+
onClick: () => void
|
|
339
|
+
onMouseEnter?: () => void
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Get actionable tooltip content for health issues
|
|
343
|
+
function getActionableTooltip(issue: string | undefined, summary: string | undefined, health: string): React.ReactNode {
|
|
344
|
+
const issueDetails: Record<string, { description: string; action: string }> = {
|
|
345
|
+
OOMKilled: {
|
|
346
|
+
description: 'Container exceeded its memory limit and was killed.',
|
|
347
|
+
action: 'Increase memory limits in Helm values or optimize app memory usage.',
|
|
348
|
+
},
|
|
349
|
+
CrashLoopBackOff: {
|
|
350
|
+
description: 'Container is repeatedly crashing.',
|
|
351
|
+
action: 'Check pod logs for crash reason.',
|
|
352
|
+
},
|
|
353
|
+
ImagePullBackOff: {
|
|
354
|
+
description: 'Cannot pull container image.',
|
|
355
|
+
action: 'Verify image name in Helm values and registry credentials.',
|
|
356
|
+
},
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const details = issue ? issueDetails[issue] : null
|
|
360
|
+
|
|
361
|
+
return (
|
|
362
|
+
<div className="max-w-xs">
|
|
363
|
+
<div className={clsx(
|
|
364
|
+
'font-medium',
|
|
365
|
+
health === 'unhealthy' ? 'text-red-400' : 'text-yellow-400'
|
|
366
|
+
)}>
|
|
367
|
+
{summary || issue || health}
|
|
368
|
+
</div>
|
|
369
|
+
{details && (
|
|
370
|
+
<>
|
|
371
|
+
<div className="text-theme-text-secondary text-[10px] mt-1">{details.description}</div>
|
|
372
|
+
<div className="text-blue-400 text-[10px] mt-1.5 border-t border-theme-border pt-1.5">
|
|
373
|
+
💡 {details.action}
|
|
374
|
+
</div>
|
|
375
|
+
</>
|
|
376
|
+
)}
|
|
377
|
+
{!details && issue && (
|
|
378
|
+
<div className="text-blue-400 text-[10px] mt-1.5">Click release for details</div>
|
|
379
|
+
)}
|
|
380
|
+
</div>
|
|
381
|
+
)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const ReleaseRow = forwardRef<HTMLTableRowElement, ReleaseRowProps>(
|
|
385
|
+
function ReleaseRow({ release, upgradeInfo, isSelected, isHighlighted, onClick, onMouseEnter }, ref) {
|
|
386
|
+
// Health badge styling
|
|
387
|
+
const getHealthBadge = () => {
|
|
388
|
+
if (!release.resourceHealth || release.resourceHealth === 'unknown') return null
|
|
389
|
+
|
|
390
|
+
const healthStyles: Record<string, { bg: string; text: string; dot: string }> = {
|
|
391
|
+
healthy: { bg: 'bg-green-500/10', text: 'text-green-400', dot: 'bg-green-500' },
|
|
392
|
+
degraded: { bg: 'bg-yellow-500/10', text: 'text-yellow-400', dot: 'bg-yellow-500' },
|
|
393
|
+
unhealthy: { bg: 'bg-red-500/10', text: 'text-red-400', dot: 'bg-red-500' },
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const style = healthStyles[release.resourceHealth] || healthStyles.healthy
|
|
397
|
+
const tooltipContent = getActionableTooltip(release.healthIssue, release.healthSummary, release.resourceHealth)
|
|
398
|
+
|
|
399
|
+
return (
|
|
400
|
+
<Tooltip content={tooltipContent}>
|
|
401
|
+
<span className={clsx(
|
|
402
|
+
'flex items-center gap-1 px-1.5 py-0.5 text-xs font-medium rounded shrink-0',
|
|
403
|
+
style.bg, style.text
|
|
404
|
+
)}>
|
|
405
|
+
<span className={clsx('w-1.5 h-1.5 rounded-full', style.dot)} />
|
|
406
|
+
{release.healthIssue || (release.resourceHealth !== 'healthy' ? release.healthSummary : null)}
|
|
407
|
+
</span>
|
|
408
|
+
</Tooltip>
|
|
409
|
+
)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return (
|
|
413
|
+
<tr
|
|
414
|
+
ref={ref}
|
|
415
|
+
onClick={onClick}
|
|
416
|
+
onMouseEnter={onMouseEnter}
|
|
417
|
+
className={clsx(
|
|
418
|
+
'cursor-pointer transition-colors',
|
|
419
|
+
isSelected
|
|
420
|
+
? 'selection-strong hover:bg-skyhook-500/30'
|
|
421
|
+
: isHighlighted
|
|
422
|
+
? 'selection selection-ring'
|
|
423
|
+
: 'hover:bg-theme-surface/50'
|
|
424
|
+
)}
|
|
425
|
+
>
|
|
426
|
+
<td className="px-4 py-3">
|
|
427
|
+
<div className="flex items-center gap-2">
|
|
428
|
+
<Package className="w-4 h-4 text-theme-text-tertiary shrink-0" />
|
|
429
|
+
<span className="text-sm text-theme-text-primary font-medium truncate">{release.name}</span>
|
|
430
|
+
{getHealthBadge()}
|
|
431
|
+
{upgradeInfo?.updateAvailable && (
|
|
432
|
+
<Tooltip content={`Upgrade available: ${release.chartVersion} → ${upgradeInfo.latestVersion}`}>
|
|
433
|
+
<span className={clsx('badge-sm shrink-0', SEVERITY_BADGE.warning)}>
|
|
434
|
+
<ArrowUpCircle className="w-3 h-3" />
|
|
435
|
+
</span>
|
|
436
|
+
</Tooltip>
|
|
437
|
+
)}
|
|
438
|
+
</div>
|
|
439
|
+
</td>
|
|
440
|
+
<td className="px-4 py-3 w-32">
|
|
441
|
+
<span className="text-sm text-theme-text-secondary">{release.namespace}</span>
|
|
442
|
+
</td>
|
|
443
|
+
<td className="px-4 py-3 w-48">
|
|
444
|
+
<Tooltip content={`${release.chart}-${release.chartVersion}`}>
|
|
445
|
+
<span className="text-sm text-theme-text-secondary truncate block">
|
|
446
|
+
{truncate(`${release.chart}-${release.chartVersion}`, 35)}
|
|
447
|
+
</span>
|
|
448
|
+
</Tooltip>
|
|
449
|
+
</td>
|
|
450
|
+
<td className="px-4 py-3 w-24 hidden xl:table-cell">
|
|
451
|
+
<span className="text-sm text-theme-text-secondary">{release.appVersion || '-'}</span>
|
|
452
|
+
</td>
|
|
453
|
+
<td className="px-4 py-3 w-28">
|
|
454
|
+
<span
|
|
455
|
+
className={clsx(
|
|
456
|
+
'badge',
|
|
457
|
+
getStatusColor(release.status)
|
|
458
|
+
)}
|
|
459
|
+
>
|
|
460
|
+
{release.status}
|
|
461
|
+
</span>
|
|
462
|
+
</td>
|
|
463
|
+
<td className="px-4 py-3 w-20">
|
|
464
|
+
<span className="text-sm text-theme-text-secondary">{release.revision}</span>
|
|
465
|
+
</td>
|
|
466
|
+
<td className="px-4 py-3 w-24">
|
|
467
|
+
<Tooltip content={release.updated}>
|
|
468
|
+
<span className="text-sm text-theme-text-secondary">
|
|
469
|
+
{formatAge(release.updated)}
|
|
470
|
+
</span>
|
|
471
|
+
</Tooltip>
|
|
472
|
+
</td>
|
|
473
|
+
</tr>
|
|
474
|
+
)
|
|
475
|
+
})
|