@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,1060 @@
|
|
|
1
|
+
import { useState, useCallback, useEffect, useMemo, useRef } from 'react'
|
|
2
|
+
import { X, Package, ChevronRight, ChevronLeft, Play, Loader2, AlertTriangle, CheckCircle, User, BookOpen, Link as LinkIcon, Star, BadgeCheck, Shield, Globe, Building2, Plus, Minus, Terminal } from 'lucide-react'
|
|
3
|
+
import { clsx } from 'clsx'
|
|
4
|
+
import yaml from 'yaml'
|
|
5
|
+
import { createPatch } from 'diff'
|
|
6
|
+
import { useQueryClient } from '@tanstack/react-query'
|
|
7
|
+
import { useChartDetail, useNamespaces, useArtifactHubChart, installChartWithProgress, type InstallProgressEvent } from '../../api/client'
|
|
8
|
+
import { useCanHelmWrite } from '../../contexts/CapabilitiesContext'
|
|
9
|
+
import type { ChartSource, ChartDetail, ArtifactHubChartDetail } from '../../types'
|
|
10
|
+
import { YamlEditor } from '../ui/YamlEditor'
|
|
11
|
+
import { Tooltip } from '../ui/Tooltip'
|
|
12
|
+
import { Markdown } from '../ui/Markdown'
|
|
13
|
+
import { SEVERITY_BADGE, SEVERITY_TEXT } from '../../utils/badge-colors'
|
|
14
|
+
|
|
15
|
+
// Deep merge two objects — values from `overrides` take priority
|
|
16
|
+
function deepMerge(base: Record<string, unknown>, overrides: Record<string, unknown>): Record<string, unknown> {
|
|
17
|
+
const result = { ...base }
|
|
18
|
+
for (const key of Object.keys(overrides)) {
|
|
19
|
+
const baseVal = base[key]
|
|
20
|
+
const overVal = overrides[key]
|
|
21
|
+
if (baseVal && overVal && typeof baseVal === 'object' && typeof overVal === 'object' && !Array.isArray(baseVal) && !Array.isArray(overVal)) {
|
|
22
|
+
result[key] = deepMerge(baseVal as Record<string, unknown>, overVal as Record<string, unknown>)
|
|
23
|
+
} else {
|
|
24
|
+
result[key] = overVal
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return result
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface InstallWizardProps {
|
|
31
|
+
repo: string
|
|
32
|
+
chartName: string
|
|
33
|
+
version: string
|
|
34
|
+
source: ChartSource
|
|
35
|
+
repoUrl?: string // Direct repo URL (overrides ArtifactHub lookup)
|
|
36
|
+
defaultValues?: Record<string, unknown> // Pre-populated values (e.g., resource limits from traffic wizard)
|
|
37
|
+
onClose: () => void
|
|
38
|
+
onSuccess: (namespace: string, releaseName: string) => void
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type WizardStep = 'info' | 'values' | 'review' | 'installing'
|
|
42
|
+
|
|
43
|
+
// Progress log entry
|
|
44
|
+
interface ProgressEntry {
|
|
45
|
+
phase: string
|
|
46
|
+
message: string
|
|
47
|
+
detail?: string
|
|
48
|
+
timestamp: Date
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function InstallWizard({ repo, chartName, version, source, repoUrl, defaultValues, onClose, onSuccess }: InstallWizardProps) {
|
|
52
|
+
const [step, setStep] = useState<WizardStep>('info')
|
|
53
|
+
const [releaseName, setReleaseName] = useState(chartName)
|
|
54
|
+
const [namespace, setNamespace] = useState(chartName)
|
|
55
|
+
const [createNamespace, setCreateNamespace] = useState(true)
|
|
56
|
+
const [valuesYaml, setValuesYaml] = useState('')
|
|
57
|
+
const [yamlError, setYamlError] = useState<string | null>(null)
|
|
58
|
+
const [showReadme, setShowReadme] = useState(false)
|
|
59
|
+
|
|
60
|
+
// Install progress state
|
|
61
|
+
const [progressLogs, setProgressLogs] = useState<ProgressEntry[]>([])
|
|
62
|
+
const [installError, setInstallError] = useState<string | null>(null)
|
|
63
|
+
const [isInstalling, setIsInstalling] = useState(false)
|
|
64
|
+
const progressEndRef = useRef<HTMLDivElement>(null)
|
|
65
|
+
|
|
66
|
+
const queryClient = useQueryClient()
|
|
67
|
+
const canHelmWrite = useCanHelmWrite()
|
|
68
|
+
|
|
69
|
+
// Choose the right data based on source
|
|
70
|
+
const isLocal = source === 'local'
|
|
71
|
+
|
|
72
|
+
// Use appropriate hook based on source - only enable the relevant one
|
|
73
|
+
const { data: localChartDetail, isLoading: localChartLoading } = useChartDetail(
|
|
74
|
+
repo, chartName, version, isLocal
|
|
75
|
+
)
|
|
76
|
+
const { data: artifactHubDetail, isLoading: artifactHubLoading } = useArtifactHubChart(
|
|
77
|
+
repo, chartName, version, !isLocal
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
const chartLoading = isLocal ? localChartLoading : artifactHubLoading
|
|
81
|
+
const chartDetail = isLocal ? localChartDetail : artifactHubDetail
|
|
82
|
+
|
|
83
|
+
const { data: namespaces } = useNamespaces()
|
|
84
|
+
|
|
85
|
+
// Auto-scroll progress logs
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (progressEndRef.current) {
|
|
88
|
+
progressEndRef.current.scrollIntoView({ behavior: 'smooth' })
|
|
89
|
+
}
|
|
90
|
+
}, [progressLogs])
|
|
91
|
+
|
|
92
|
+
// Initialize values from chart defaults, merging any pre-populated defaultValues on top
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
let baseValues: Record<string, unknown> | undefined
|
|
95
|
+
if (isLocal && localChartDetail?.values) {
|
|
96
|
+
baseValues = localChartDetail.values as Record<string, unknown>
|
|
97
|
+
} else if (!isLocal && artifactHubDetail?.values) {
|
|
98
|
+
try {
|
|
99
|
+
baseValues = yaml.parse(artifactHubDetail.values) as Record<string, unknown>
|
|
100
|
+
} catch {
|
|
101
|
+
// If parsing fails, use the raw string as-is
|
|
102
|
+
setValuesYaml(artifactHubDetail.values)
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (baseValues && defaultValues) {
|
|
107
|
+
setValuesYaml(yaml.stringify(deepMerge(baseValues, defaultValues)))
|
|
108
|
+
} else if (defaultValues && !baseValues) {
|
|
109
|
+
setValuesYaml(yaml.stringify(defaultValues))
|
|
110
|
+
} else if (baseValues) {
|
|
111
|
+
setValuesYaml(yaml.stringify(baseValues))
|
|
112
|
+
}
|
|
113
|
+
}, [localChartDetail?.values, artifactHubDetail?.values, isLocal, defaultValues])
|
|
114
|
+
|
|
115
|
+
const handleInstall = useCallback(async () => {
|
|
116
|
+
let values: Record<string, unknown> | undefined
|
|
117
|
+
if (valuesYaml.trim()) {
|
|
118
|
+
try {
|
|
119
|
+
values = yaml.parse(valuesYaml)
|
|
120
|
+
} catch (err) {
|
|
121
|
+
setYamlError(err instanceof Error ? err.message : 'Invalid YAML')
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// For ArtifactHub charts, we need to use the repository URL
|
|
127
|
+
// If repoUrl is provided directly, use it (for non-ArtifactHub external repos)
|
|
128
|
+
const repository = repoUrl || (isLocal ? repo : (artifactHubDetail?.repository.url || repo))
|
|
129
|
+
|
|
130
|
+
// Switch to installing step
|
|
131
|
+
setStep('installing')
|
|
132
|
+
setIsInstalling(true)
|
|
133
|
+
setInstallError(null)
|
|
134
|
+
setProgressLogs([])
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const release = await installChartWithProgress(
|
|
138
|
+
{
|
|
139
|
+
releaseName,
|
|
140
|
+
namespace,
|
|
141
|
+
chartName,
|
|
142
|
+
version,
|
|
143
|
+
repository,
|
|
144
|
+
values,
|
|
145
|
+
createNamespace,
|
|
146
|
+
},
|
|
147
|
+
(event: InstallProgressEvent) => {
|
|
148
|
+
if (event.type === 'progress' && event.message) {
|
|
149
|
+
setProgressLogs(prev => [...prev, {
|
|
150
|
+
phase: event.phase || 'progress',
|
|
151
|
+
message: event.message || '',
|
|
152
|
+
detail: event.detail,
|
|
153
|
+
timestamp: new Date(),
|
|
154
|
+
}])
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
// Success - add final log entry
|
|
160
|
+
setProgressLogs(prev => [...prev, {
|
|
161
|
+
phase: 'complete',
|
|
162
|
+
message: `Successfully installed ${release.name}`,
|
|
163
|
+
timestamp: new Date(),
|
|
164
|
+
}])
|
|
165
|
+
|
|
166
|
+
// Invalidate queries
|
|
167
|
+
queryClient.invalidateQueries({ queryKey: ['helm-releases'] })
|
|
168
|
+
|
|
169
|
+
// Wait a moment to show success, then close
|
|
170
|
+
setTimeout(() => {
|
|
171
|
+
onSuccess(namespace, releaseName)
|
|
172
|
+
}, 1500)
|
|
173
|
+
} catch (err) {
|
|
174
|
+
setInstallError(err instanceof Error ? err.message : 'Install failed')
|
|
175
|
+
setProgressLogs(prev => [...prev, {
|
|
176
|
+
phase: 'error',
|
|
177
|
+
message: err instanceof Error ? err.message : 'Install failed',
|
|
178
|
+
timestamp: new Date(),
|
|
179
|
+
}])
|
|
180
|
+
} finally {
|
|
181
|
+
setIsInstalling(false)
|
|
182
|
+
}
|
|
183
|
+
}, [releaseName, namespace, chartName, version, repo, valuesYaml, createNamespace, onSuccess, isLocal, artifactHubDetail, queryClient])
|
|
184
|
+
|
|
185
|
+
const canProceedFromInfo = releaseName.trim() !== '' && namespace.trim() !== ''
|
|
186
|
+
const canInstall = canProceedFromInfo && !yamlError
|
|
187
|
+
|
|
188
|
+
const steps: { id: WizardStep; label: string }[] = [
|
|
189
|
+
{ id: 'info', label: 'Details' },
|
|
190
|
+
{ id: 'values', label: 'Values' },
|
|
191
|
+
{ id: 'review', label: 'Review' },
|
|
192
|
+
]
|
|
193
|
+
|
|
194
|
+
return (
|
|
195
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
196
|
+
{/* Backdrop */}
|
|
197
|
+
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={onClose} />
|
|
198
|
+
|
|
199
|
+
{/* Dialog */}
|
|
200
|
+
<div className="relative dialog max-w-3xl w-full mx-4 max-h-[90vh] flex flex-col">
|
|
201
|
+
{/* Header */}
|
|
202
|
+
<div className="flex items-center justify-between px-4 py-3 border-b border-theme-border shrink-0">
|
|
203
|
+
<div className="flex items-center gap-3">
|
|
204
|
+
{(isLocal ? (chartDetail as ChartDetail)?.icon : (chartDetail as ArtifactHubChartDetail)?.logoUrl) ? (
|
|
205
|
+
<img
|
|
206
|
+
src={isLocal ? (chartDetail as ChartDetail)?.icon : (chartDetail as ArtifactHubChartDetail)?.logoUrl}
|
|
207
|
+
alt=""
|
|
208
|
+
className="w-8 h-8 rounded object-contain bg-white/10 p-1"
|
|
209
|
+
/>
|
|
210
|
+
) : (
|
|
211
|
+
<Package className="w-8 h-8 text-purple-400" />
|
|
212
|
+
)}
|
|
213
|
+
<div>
|
|
214
|
+
<div className="flex items-center gap-2">
|
|
215
|
+
<h2 className="text-lg font-semibold text-theme-text-primary">Install {chartName}</h2>
|
|
216
|
+
{!isLocal && (
|
|
217
|
+
<Tooltip content="From ArtifactHub">
|
|
218
|
+
<Globe className="w-4 h-4 text-blue-400" />
|
|
219
|
+
</Tooltip>
|
|
220
|
+
)}
|
|
221
|
+
</div>
|
|
222
|
+
<div className="flex items-center gap-2 text-sm text-theme-text-tertiary">
|
|
223
|
+
<span>{repo} / {version}</span>
|
|
224
|
+
{!isLocal && (chartDetail as ArtifactHubChartDetail)?.repository?.official && (
|
|
225
|
+
<Tooltip content="Official">
|
|
226
|
+
<BadgeCheck className="w-3.5 h-3.5 text-blue-400" />
|
|
227
|
+
</Tooltip>
|
|
228
|
+
)}
|
|
229
|
+
{!isLocal && (chartDetail as ArtifactHubChartDetail)?.repository?.verifiedPublisher && (
|
|
230
|
+
<Tooltip content="Verified Publisher">
|
|
231
|
+
<Shield className="w-3.5 h-3.5 text-green-400" />
|
|
232
|
+
</Tooltip>
|
|
233
|
+
)}
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
<button
|
|
238
|
+
onClick={onClose}
|
|
239
|
+
className="p-1.5 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded"
|
|
240
|
+
>
|
|
241
|
+
<X className="w-5 h-5" />
|
|
242
|
+
</button>
|
|
243
|
+
</div>
|
|
244
|
+
|
|
245
|
+
{/* Step indicator - hidden during install */}
|
|
246
|
+
{step !== 'installing' && (
|
|
247
|
+
<div className="flex items-center justify-center gap-2 px-4 py-3 border-b border-theme-border bg-theme-surface/50">
|
|
248
|
+
{steps.map((s, i) => (
|
|
249
|
+
<div key={s.id} className="flex items-center">
|
|
250
|
+
{i > 0 && <ChevronRight className="w-4 h-4 text-theme-text-disabled mx-2" />}
|
|
251
|
+
<button
|
|
252
|
+
onClick={() => {
|
|
253
|
+
if (s.id === 'info' || (s.id === 'values' && canProceedFromInfo) || (s.id === 'review' && canInstall)) {
|
|
254
|
+
setStep(s.id)
|
|
255
|
+
}
|
|
256
|
+
}}
|
|
257
|
+
className={clsx(
|
|
258
|
+
'flex items-center gap-2 px-3 py-1.5 rounded-full text-sm transition-colors',
|
|
259
|
+
step === s.id
|
|
260
|
+
? SEVERITY_BADGE.info
|
|
261
|
+
: 'text-theme-text-secondary hover:text-theme-text-primary'
|
|
262
|
+
)}
|
|
263
|
+
>
|
|
264
|
+
<span className={clsx(
|
|
265
|
+
'w-5 h-5 rounded-full flex items-center justify-center text-xs font-medium',
|
|
266
|
+
step === s.id ? 'bg-blue-500 text-white' : 'bg-theme-elevated text-theme-text-secondary'
|
|
267
|
+
)}>
|
|
268
|
+
{i + 1}
|
|
269
|
+
</span>
|
|
270
|
+
{s.label}
|
|
271
|
+
</button>
|
|
272
|
+
</div>
|
|
273
|
+
))}
|
|
274
|
+
</div>
|
|
275
|
+
)}
|
|
276
|
+
|
|
277
|
+
{/* Content */}
|
|
278
|
+
<div className="flex-1 overflow-auto p-4">
|
|
279
|
+
{chartLoading ? (
|
|
280
|
+
<div className="flex items-center justify-center h-32 text-theme-text-tertiary">
|
|
281
|
+
<Loader2 className="w-5 h-5 animate-spin mr-2" />
|
|
282
|
+
Loading chart details...
|
|
283
|
+
</div>
|
|
284
|
+
) : (
|
|
285
|
+
<>
|
|
286
|
+
{step === 'info' && (
|
|
287
|
+
<InfoStep
|
|
288
|
+
chartDetail={chartDetail}
|
|
289
|
+
source={source}
|
|
290
|
+
releaseName={releaseName}
|
|
291
|
+
setReleaseName={setReleaseName}
|
|
292
|
+
namespace={namespace}
|
|
293
|
+
setNamespace={setNamespace}
|
|
294
|
+
namespaces={namespaces || []}
|
|
295
|
+
createNamespace={createNamespace}
|
|
296
|
+
setCreateNamespace={setCreateNamespace}
|
|
297
|
+
showReadme={showReadme}
|
|
298
|
+
setShowReadme={setShowReadme}
|
|
299
|
+
/>
|
|
300
|
+
)}
|
|
301
|
+
|
|
302
|
+
{step === 'values' && (
|
|
303
|
+
<ValuesStep
|
|
304
|
+
valuesYaml={valuesYaml}
|
|
305
|
+
setValuesYaml={setValuesYaml}
|
|
306
|
+
yamlError={yamlError}
|
|
307
|
+
setYamlError={setYamlError}
|
|
308
|
+
chartDetail={chartDetail}
|
|
309
|
+
source={source}
|
|
310
|
+
/>
|
|
311
|
+
)}
|
|
312
|
+
|
|
313
|
+
{step === 'review' && (
|
|
314
|
+
<ReviewStep
|
|
315
|
+
releaseName={releaseName}
|
|
316
|
+
namespace={namespace}
|
|
317
|
+
chartName={chartName}
|
|
318
|
+
version={version}
|
|
319
|
+
repo={repo}
|
|
320
|
+
source={source}
|
|
321
|
+
artifactHubRepoUrl={!isLocal ? (chartDetail as ArtifactHubChartDetail)?.repository?.url : undefined}
|
|
322
|
+
createNamespace={createNamespace}
|
|
323
|
+
valuesYaml={valuesYaml}
|
|
324
|
+
defaultValuesYaml={
|
|
325
|
+
isLocal
|
|
326
|
+
? (localChartDetail?.values ? yaml.stringify(localChartDetail.values) : '')
|
|
327
|
+
: (artifactHubDetail?.values || '')
|
|
328
|
+
}
|
|
329
|
+
/>
|
|
330
|
+
)}
|
|
331
|
+
|
|
332
|
+
{step === 'installing' && (
|
|
333
|
+
<InstallingStep
|
|
334
|
+
releaseName={releaseName}
|
|
335
|
+
namespace={namespace}
|
|
336
|
+
chartName={chartName}
|
|
337
|
+
progressLogs={progressLogs}
|
|
338
|
+
isInstalling={isInstalling}
|
|
339
|
+
installError={installError}
|
|
340
|
+
progressEndRef={progressEndRef}
|
|
341
|
+
/>
|
|
342
|
+
)}
|
|
343
|
+
</>
|
|
344
|
+
)}
|
|
345
|
+
</div>
|
|
346
|
+
|
|
347
|
+
{/* Footer */}
|
|
348
|
+
<div className="flex items-center justify-between px-4 py-3 border-t border-theme-border shrink-0">
|
|
349
|
+
<div>
|
|
350
|
+
{step !== 'info' && step !== 'installing' && (
|
|
351
|
+
<button
|
|
352
|
+
onClick={() => setStep(step === 'review' ? 'values' : 'info')}
|
|
353
|
+
className="flex items-center gap-1 px-4 py-2 text-sm text-theme-text-secondary hover:text-theme-text-primary transition-colors"
|
|
354
|
+
>
|
|
355
|
+
<ChevronLeft className="w-4 h-4" />
|
|
356
|
+
Back
|
|
357
|
+
</button>
|
|
358
|
+
)}
|
|
359
|
+
</div>
|
|
360
|
+
<div className="flex items-center gap-3">
|
|
361
|
+
{step === 'installing' ? (
|
|
362
|
+
// Installing step - only show close if done or error
|
|
363
|
+
<>
|
|
364
|
+
{!isInstalling && installError && (
|
|
365
|
+
<button
|
|
366
|
+
onClick={() => setStep('review')}
|
|
367
|
+
className="flex items-center gap-1 px-4 py-2 text-sm text-theme-text-secondary hover:text-theme-text-primary transition-colors"
|
|
368
|
+
>
|
|
369
|
+
<ChevronLeft className="w-4 h-4" />
|
|
370
|
+
Back to Review
|
|
371
|
+
</button>
|
|
372
|
+
)}
|
|
373
|
+
{isInstalling && (
|
|
374
|
+
<span className="text-sm text-theme-text-tertiary">
|
|
375
|
+
Installing...
|
|
376
|
+
</span>
|
|
377
|
+
)}
|
|
378
|
+
</>
|
|
379
|
+
) : (
|
|
380
|
+
<>
|
|
381
|
+
<button
|
|
382
|
+
onClick={onClose}
|
|
383
|
+
className="px-4 py-2 text-sm text-theme-text-secondary hover:text-theme-text-primary transition-colors"
|
|
384
|
+
>
|
|
385
|
+
Cancel
|
|
386
|
+
</button>
|
|
387
|
+
{step === 'review' ? (
|
|
388
|
+
<button
|
|
389
|
+
onClick={handleInstall}
|
|
390
|
+
disabled={!canInstall || isInstalling || !canHelmWrite}
|
|
391
|
+
className="flex items-center gap-2 px-4 py-2 text-sm font-medium btn-brand rounded-lg disabled:cursor-not-allowed"
|
|
392
|
+
title={!canHelmWrite ? 'Helm write permissions required (rbac.helm=true)' : undefined}
|
|
393
|
+
>
|
|
394
|
+
{isInstalling ? (
|
|
395
|
+
<Loader2 className="w-4 h-4 animate-spin" />
|
|
396
|
+
) : (
|
|
397
|
+
<Play className="w-4 h-4" />
|
|
398
|
+
)}
|
|
399
|
+
Install
|
|
400
|
+
</button>
|
|
401
|
+
) : (
|
|
402
|
+
<button
|
|
403
|
+
onClick={() => setStep(step === 'info' ? 'values' : 'review')}
|
|
404
|
+
disabled={step === 'info' && !canProceedFromInfo}
|
|
405
|
+
className="flex items-center gap-1 px-4 py-2 text-sm font-medium btn-brand rounded-lg disabled:cursor-not-allowed"
|
|
406
|
+
>
|
|
407
|
+
Next
|
|
408
|
+
<ChevronRight className="w-4 h-4" />
|
|
409
|
+
</button>
|
|
410
|
+
)}
|
|
411
|
+
</>
|
|
412
|
+
)}
|
|
413
|
+
</div>
|
|
414
|
+
</div>
|
|
415
|
+
</div>
|
|
416
|
+
</div>
|
|
417
|
+
)
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
interface InfoStepProps {
|
|
421
|
+
chartDetail: ChartDetail | ArtifactHubChartDetail | undefined
|
|
422
|
+
source: ChartSource
|
|
423
|
+
releaseName: string
|
|
424
|
+
setReleaseName: (name: string) => void
|
|
425
|
+
namespace: string
|
|
426
|
+
setNamespace: (ns: string) => void
|
|
427
|
+
namespaces: { name: string }[]
|
|
428
|
+
createNamespace: boolean
|
|
429
|
+
setCreateNamespace: (create: boolean) => void
|
|
430
|
+
showReadme: boolean
|
|
431
|
+
setShowReadme: (show: boolean) => void
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function InfoStep({
|
|
435
|
+
chartDetail,
|
|
436
|
+
source,
|
|
437
|
+
releaseName,
|
|
438
|
+
setReleaseName,
|
|
439
|
+
namespace,
|
|
440
|
+
setNamespace,
|
|
441
|
+
namespaces,
|
|
442
|
+
createNamespace,
|
|
443
|
+
setCreateNamespace,
|
|
444
|
+
showReadme,
|
|
445
|
+
setShowReadme,
|
|
446
|
+
}: InfoStepProps) {
|
|
447
|
+
const isLocal = source === 'local'
|
|
448
|
+
const localDetail = chartDetail as ChartDetail | undefined
|
|
449
|
+
const ahDetail = chartDetail as ArtifactHubChartDetail | undefined
|
|
450
|
+
|
|
451
|
+
const description = isLocal ? localDetail?.description : ahDetail?.description
|
|
452
|
+
const home = isLocal ? localDetail?.home : ahDetail?.homeUrl
|
|
453
|
+
const maintainers = isLocal ? localDetail?.maintainers : ahDetail?.maintainers
|
|
454
|
+
const readme = isLocal ? localDetail?.readme : ahDetail?.readme
|
|
455
|
+
|
|
456
|
+
return (
|
|
457
|
+
<div className="space-y-6">
|
|
458
|
+
{/* ArtifactHub specific metadata */}
|
|
459
|
+
{!isLocal && ahDetail && (
|
|
460
|
+
<div className="flex items-center gap-4 p-3 bg-theme-elevated/30 rounded-lg">
|
|
461
|
+
{ahDetail.stars > 0 && (
|
|
462
|
+
<Tooltip content={`${ahDetail.stars} stars`}>
|
|
463
|
+
<span className="flex items-center gap-1 text-sm text-amber-400">
|
|
464
|
+
<Star className="w-4 h-4 fill-current" />
|
|
465
|
+
{ahDetail.stars > 999 ? `${(ahDetail.stars / 1000).toFixed(1)}k` : ahDetail.stars}
|
|
466
|
+
</span>
|
|
467
|
+
</Tooltip>
|
|
468
|
+
)}
|
|
469
|
+
{ahDetail.productionOrgsCount && ahDetail.productionOrgsCount > 0 && (
|
|
470
|
+
<Tooltip content={`Used in ${ahDetail.productionOrgsCount} production environments`}>
|
|
471
|
+
<span className="flex items-center gap-1 text-sm text-theme-text-secondary">
|
|
472
|
+
<Building2 className="w-4 h-4" />
|
|
473
|
+
{ahDetail.productionOrgsCount} orgs
|
|
474
|
+
</span>
|
|
475
|
+
</Tooltip>
|
|
476
|
+
)}
|
|
477
|
+
{ahDetail.security && (ahDetail.security.critical || ahDetail.security.high) && (
|
|
478
|
+
<Tooltip content={`Security: ${ahDetail.security.critical || 0} critical, ${ahDetail.security.high || 0} high`}>
|
|
479
|
+
<span className={clsx(
|
|
480
|
+
'flex items-center gap-1 text-sm',
|
|
481
|
+
ahDetail.security.critical ? 'text-red-400' : 'text-orange-400'
|
|
482
|
+
)}>
|
|
483
|
+
<Shield className="w-4 h-4" />
|
|
484
|
+
{(ahDetail.security.critical || 0) + (ahDetail.security.high || 0)} issues
|
|
485
|
+
</span>
|
|
486
|
+
</Tooltip>
|
|
487
|
+
)}
|
|
488
|
+
{ahDetail.signed && (
|
|
489
|
+
<span className="flex items-center gap-1 text-sm text-green-400">
|
|
490
|
+
<BadgeCheck className="w-4 h-4" />
|
|
491
|
+
Signed
|
|
492
|
+
</span>
|
|
493
|
+
)}
|
|
494
|
+
{ahDetail.license && (
|
|
495
|
+
<span className="text-sm text-theme-text-tertiary">{ahDetail.license}</span>
|
|
496
|
+
)}
|
|
497
|
+
</div>
|
|
498
|
+
)}
|
|
499
|
+
|
|
500
|
+
{/* Chart description */}
|
|
501
|
+
{description && (
|
|
502
|
+
<div className="bg-theme-elevated/30 rounded-lg p-4">
|
|
503
|
+
<p className="text-sm text-theme-text-secondary">{description}</p>
|
|
504
|
+
{(home || maintainers?.length) && (
|
|
505
|
+
<div className="flex flex-wrap gap-4 mt-3 pt-3 border-t border-theme-border">
|
|
506
|
+
{home && (
|
|
507
|
+
<a
|
|
508
|
+
href={home}
|
|
509
|
+
target="_blank"
|
|
510
|
+
rel="noopener noreferrer"
|
|
511
|
+
className="flex items-center gap-1 text-xs text-blue-400 hover:text-blue-300"
|
|
512
|
+
>
|
|
513
|
+
<LinkIcon className="w-3.5 h-3.5" />
|
|
514
|
+
Homepage
|
|
515
|
+
</a>
|
|
516
|
+
)}
|
|
517
|
+
{maintainers && maintainers.length > 0 && (
|
|
518
|
+
<span className="flex items-center gap-1 text-xs text-theme-text-tertiary">
|
|
519
|
+
<User className="w-3.5 h-3.5" />
|
|
520
|
+
{maintainers.map((m) => m.name).join(', ')}
|
|
521
|
+
</span>
|
|
522
|
+
)}
|
|
523
|
+
</div>
|
|
524
|
+
)}
|
|
525
|
+
</div>
|
|
526
|
+
)}
|
|
527
|
+
|
|
528
|
+
{/* Release name input */}
|
|
529
|
+
<div>
|
|
530
|
+
<label className="block text-sm font-medium text-theme-text-secondary mb-2">
|
|
531
|
+
Release Name
|
|
532
|
+
</label>
|
|
533
|
+
<input
|
|
534
|
+
type="text"
|
|
535
|
+
value={releaseName}
|
|
536
|
+
onChange={(e) => setReleaseName(e.target.value)}
|
|
537
|
+
placeholder="my-release"
|
|
538
|
+
className="w-full px-3 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"
|
|
539
|
+
/>
|
|
540
|
+
<p className="mt-1 text-xs text-theme-text-tertiary">
|
|
541
|
+
A unique name for this release in the namespace
|
|
542
|
+
</p>
|
|
543
|
+
</div>
|
|
544
|
+
|
|
545
|
+
{/* Namespace selection */}
|
|
546
|
+
<div>
|
|
547
|
+
<label className="block text-sm font-medium text-theme-text-secondary mb-2">
|
|
548
|
+
Namespace
|
|
549
|
+
</label>
|
|
550
|
+
<input
|
|
551
|
+
type="text"
|
|
552
|
+
list="namespace-suggestions"
|
|
553
|
+
value={namespace}
|
|
554
|
+
onChange={(e) => setNamespace(e.target.value)}
|
|
555
|
+
placeholder="Enter namespace name"
|
|
556
|
+
className="w-full px-3 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"
|
|
557
|
+
/>
|
|
558
|
+
<datalist id="namespace-suggestions">
|
|
559
|
+
{namespaces.map(ns => (
|
|
560
|
+
<option key={ns.name} value={ns.name} />
|
|
561
|
+
))}
|
|
562
|
+
</datalist>
|
|
563
|
+
<label className="flex items-center gap-2 mt-2 text-sm text-theme-text-secondary">
|
|
564
|
+
<input
|
|
565
|
+
type="checkbox"
|
|
566
|
+
checked={createNamespace}
|
|
567
|
+
onChange={(e) => setCreateNamespace(e.target.checked)}
|
|
568
|
+
className="rounded border-theme-border text-blue-500 focus:ring-blue-500"
|
|
569
|
+
/>
|
|
570
|
+
Create namespace if it doesn't exist
|
|
571
|
+
</label>
|
|
572
|
+
</div>
|
|
573
|
+
|
|
574
|
+
{/* README toggle */}
|
|
575
|
+
{readme && (
|
|
576
|
+
<div>
|
|
577
|
+
<button
|
|
578
|
+
onClick={() => setShowReadme(!showReadme)}
|
|
579
|
+
className="flex items-center gap-2 text-sm text-blue-400 hover:text-blue-300"
|
|
580
|
+
>
|
|
581
|
+
<BookOpen className="w-4 h-4" />
|
|
582
|
+
{showReadme ? 'Hide' : 'Show'} Chart README
|
|
583
|
+
</button>
|
|
584
|
+
{showReadme && (
|
|
585
|
+
<div className="mt-3 p-4 bg-theme-base/50 rounded-lg text-xs overflow-auto max-h-96">
|
|
586
|
+
<Markdown>{readme}</Markdown>
|
|
587
|
+
</div>
|
|
588
|
+
)}
|
|
589
|
+
</div>
|
|
590
|
+
)}
|
|
591
|
+
</div>
|
|
592
|
+
)
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
interface ValuesStepProps {
|
|
596
|
+
valuesYaml: string
|
|
597
|
+
setValuesYaml: (yaml: string) => void
|
|
598
|
+
yamlError: string | null
|
|
599
|
+
setYamlError: (error: string | null) => void
|
|
600
|
+
chartDetail: ChartDetail | ArtifactHubChartDetail | undefined
|
|
601
|
+
source: ChartSource
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function ValuesStep({ valuesYaml, setValuesYaml, yamlError, setYamlError, chartDetail, source }: ValuesStepProps) {
|
|
605
|
+
const [showEditor, setShowEditor] = useState(false)
|
|
606
|
+
|
|
607
|
+
const isLocal = source === 'local'
|
|
608
|
+
const localDetail = chartDetail as ChartDetail | undefined
|
|
609
|
+
const ahDetail = chartDetail as ArtifactHubChartDetail | undefined
|
|
610
|
+
|
|
611
|
+
const defaultValues = isLocal
|
|
612
|
+
? (localDetail?.values ? yaml.stringify(localDetail.values) : '')
|
|
613
|
+
: (ahDetail?.values || '')
|
|
614
|
+
|
|
615
|
+
const hasDefaults = Boolean(defaultValues)
|
|
616
|
+
const hasValues = Boolean(valuesYaml)
|
|
617
|
+
|
|
618
|
+
// Get README/docs link
|
|
619
|
+
const homeUrl = isLocal ? localDetail?.home : ahDetail?.homeUrl
|
|
620
|
+
|
|
621
|
+
return (
|
|
622
|
+
<div className="space-y-4">
|
|
623
|
+
{/* Info banner */}
|
|
624
|
+
<div className="flex items-start gap-3 p-4 bg-blue-500/10 border border-blue-500/30 rounded-lg">
|
|
625
|
+
<CheckCircle className="w-5 h-5 text-blue-400 shrink-0 mt-0.5" />
|
|
626
|
+
<div>
|
|
627
|
+
<p className="text-sm font-medium text-theme-text-primary">Ready to install with defaults</p>
|
|
628
|
+
<p className="text-xs text-theme-text-secondary mt-1">
|
|
629
|
+
{hasValues
|
|
630
|
+
? 'Default values are shown below. Edit only the values you want to change, then proceed to install.'
|
|
631
|
+
: 'Most charts work out of the box. You can skip this step or customize values below.'}
|
|
632
|
+
</p>
|
|
633
|
+
{homeUrl && (
|
|
634
|
+
<a
|
|
635
|
+
href={homeUrl}
|
|
636
|
+
target="_blank"
|
|
637
|
+
rel="noopener noreferrer"
|
|
638
|
+
className="inline-flex items-center gap-1 text-xs text-blue-400 hover:text-blue-300 mt-2"
|
|
639
|
+
>
|
|
640
|
+
<LinkIcon className="w-3 h-3" />
|
|
641
|
+
View chart documentation
|
|
642
|
+
</a>
|
|
643
|
+
)}
|
|
644
|
+
</div>
|
|
645
|
+
</div>
|
|
646
|
+
|
|
647
|
+
{/* Collapsible editor section */}
|
|
648
|
+
<div className="border border-theme-border rounded-lg overflow-hidden">
|
|
649
|
+
<button
|
|
650
|
+
onClick={() => setShowEditor(!showEditor)}
|
|
651
|
+
className="w-full flex items-center justify-between px-4 py-3 bg-theme-elevated/50 hover:bg-theme-elevated transition-colors"
|
|
652
|
+
>
|
|
653
|
+
<div className="flex items-center gap-2">
|
|
654
|
+
<ChevronRight className={clsx('w-4 h-4 text-theme-text-tertiary transition-transform', showEditor && 'rotate-90')} />
|
|
655
|
+
<span className="text-sm font-medium text-theme-text-primary">
|
|
656
|
+
{hasValues ? (showEditor ? 'Hide' : 'Show') : 'Add'} configuration values
|
|
657
|
+
</span>
|
|
658
|
+
<span className="text-xs text-theme-text-tertiary">(optional)</span>
|
|
659
|
+
{hasValues && !showEditor && (
|
|
660
|
+
<span className="text-xs text-green-400 ml-2">Default values loaded</span>
|
|
661
|
+
)}
|
|
662
|
+
</div>
|
|
663
|
+
</button>
|
|
664
|
+
|
|
665
|
+
{showEditor && (
|
|
666
|
+
<div className="p-4 border-t border-theme-border">
|
|
667
|
+
{/* Action buttons */}
|
|
668
|
+
<div className="flex items-center gap-3 mb-4">
|
|
669
|
+
{hasDefaults && !hasValues && (
|
|
670
|
+
<button
|
|
671
|
+
onClick={() => setValuesYaml(defaultValues)}
|
|
672
|
+
className={clsx('text-xs px-3 py-1.5 rounded hover:bg-sky-500/30 transition-colors', SEVERITY_BADGE.info)}
|
|
673
|
+
>
|
|
674
|
+
Load default values
|
|
675
|
+
</button>
|
|
676
|
+
)}
|
|
677
|
+
{hasValues && hasDefaults && (
|
|
678
|
+
<button
|
|
679
|
+
onClick={() => setValuesYaml(defaultValues)}
|
|
680
|
+
className="text-xs px-3 py-1.5 bg-theme-elevated border border-theme-border-light rounded hover:bg-theme-hover transition-colors text-theme-text-secondary"
|
|
681
|
+
>
|
|
682
|
+
Reset to defaults
|
|
683
|
+
</button>
|
|
684
|
+
)}
|
|
685
|
+
{hasValues && (
|
|
686
|
+
<button
|
|
687
|
+
onClick={() => setValuesYaml('')}
|
|
688
|
+
className="text-xs text-theme-text-tertiary hover:text-theme-text-secondary"
|
|
689
|
+
>
|
|
690
|
+
Clear all
|
|
691
|
+
</button>
|
|
692
|
+
)}
|
|
693
|
+
</div>
|
|
694
|
+
|
|
695
|
+
{yamlError && (
|
|
696
|
+
<div className="flex items-center gap-2 px-3 py-2 mb-4 text-xs text-red-400 bg-red-500/10 border border-red-500/30 rounded">
|
|
697
|
+
<AlertTriangle className="w-4 h-4 shrink-0" />
|
|
698
|
+
{yamlError}
|
|
699
|
+
</div>
|
|
700
|
+
)}
|
|
701
|
+
|
|
702
|
+
{hasValues ? (
|
|
703
|
+
<YamlEditor
|
|
704
|
+
value={valuesYaml}
|
|
705
|
+
onChange={setValuesYaml}
|
|
706
|
+
height="300px"
|
|
707
|
+
onValidate={(isValid, errors) => {
|
|
708
|
+
setYamlError(isValid ? null : errors[0] || 'Invalid YAML')
|
|
709
|
+
}}
|
|
710
|
+
/>
|
|
711
|
+
) : (
|
|
712
|
+
<div className="flex flex-col items-center justify-center py-8 text-theme-text-tertiary bg-theme-base/30 rounded-lg">
|
|
713
|
+
<p className="text-sm">No default values available for this chart</p>
|
|
714
|
+
<p className="text-xs mt-1 text-theme-text-disabled">
|
|
715
|
+
{homeUrl ? 'Check the documentation for available options' : 'You can add custom values manually'}
|
|
716
|
+
</p>
|
|
717
|
+
<button
|
|
718
|
+
onClick={() => setValuesYaml('# Custom values\n')}
|
|
719
|
+
className="mt-3 text-xs px-3 py-1.5 bg-theme-elevated border border-theme-border-light rounded hover:bg-theme-hover transition-colors text-theme-text-secondary"
|
|
720
|
+
>
|
|
721
|
+
Add custom values
|
|
722
|
+
</button>
|
|
723
|
+
</div>
|
|
724
|
+
)}
|
|
725
|
+
</div>
|
|
726
|
+
)}
|
|
727
|
+
</div>
|
|
728
|
+
</div>
|
|
729
|
+
)
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
interface ReviewStepProps {
|
|
733
|
+
releaseName: string
|
|
734
|
+
namespace: string
|
|
735
|
+
chartName: string
|
|
736
|
+
version: string
|
|
737
|
+
repo: string
|
|
738
|
+
source: ChartSource
|
|
739
|
+
artifactHubRepoUrl?: string
|
|
740
|
+
createNamespace: boolean
|
|
741
|
+
valuesYaml: string
|
|
742
|
+
defaultValuesYaml: string
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function ReviewStep({
|
|
746
|
+
releaseName,
|
|
747
|
+
namespace,
|
|
748
|
+
chartName,
|
|
749
|
+
version,
|
|
750
|
+
repo,
|
|
751
|
+
source,
|
|
752
|
+
artifactHubRepoUrl,
|
|
753
|
+
createNamespace,
|
|
754
|
+
valuesYaml,
|
|
755
|
+
defaultValuesYaml,
|
|
756
|
+
}: ReviewStepProps) {
|
|
757
|
+
const hasCustomValues = valuesYaml.trim() !== ''
|
|
758
|
+
const hasDefaults = defaultValuesYaml.trim() !== ''
|
|
759
|
+
|
|
760
|
+
// Compute the diff between defaults and current values
|
|
761
|
+
const diffLines = useMemo(() => {
|
|
762
|
+
if (!hasCustomValues) return null
|
|
763
|
+
|
|
764
|
+
// If no defaults, show all custom values as additions
|
|
765
|
+
if (!hasDefaults) {
|
|
766
|
+
return valuesYaml.split('\n').map((line, i) => ({
|
|
767
|
+
type: 'add' as const,
|
|
768
|
+
content: line,
|
|
769
|
+
lineNumber: i + 1,
|
|
770
|
+
}))
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// If values are the same as defaults, no diff
|
|
774
|
+
if (valuesYaml.trim() === defaultValuesYaml.trim()) return null
|
|
775
|
+
|
|
776
|
+
// Generate unified diff
|
|
777
|
+
const patch = createPatch('values.yaml', defaultValuesYaml, valuesYaml, '', '', { context: 2 })
|
|
778
|
+
const lines = patch.split('\n')
|
|
779
|
+
|
|
780
|
+
// Parse the diff, skipping the header lines
|
|
781
|
+
const result: { type: 'context' | 'add' | 'remove' | 'header'; content: string; lineNumber?: number }[] = []
|
|
782
|
+
let newLineNum = 0
|
|
783
|
+
let seenHeader = false
|
|
784
|
+
|
|
785
|
+
for (const line of lines) {
|
|
786
|
+
// Skip diff metadata lines
|
|
787
|
+
if (line.startsWith('Index:') || line.startsWith('===') ||
|
|
788
|
+
line.startsWith('---') || line.startsWith('+++')) {
|
|
789
|
+
continue
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// Hunk header
|
|
793
|
+
if (line.startsWith('@@')) {
|
|
794
|
+
seenHeader = true
|
|
795
|
+
// Parse line numbers from @@ -1,3 +1,4 @@
|
|
796
|
+
const match = line.match(/@@ -\d+,?\d* \+(\d+),?\d* @@/)
|
|
797
|
+
if (match) {
|
|
798
|
+
newLineNum = parseInt(match[1], 10)
|
|
799
|
+
}
|
|
800
|
+
result.push({ type: 'header', content: line })
|
|
801
|
+
continue
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
if (!seenHeader) continue
|
|
805
|
+
|
|
806
|
+
if (line.startsWith('+')) {
|
|
807
|
+
result.push({ type: 'add', content: line.slice(1), lineNumber: newLineNum })
|
|
808
|
+
newLineNum++
|
|
809
|
+
} else if (line.startsWith('-')) {
|
|
810
|
+
result.push({ type: 'remove', content: line.slice(1) })
|
|
811
|
+
} else if (line.startsWith(' ')) {
|
|
812
|
+
result.push({ type: 'context', content: line.slice(1), lineNumber: newLineNum })
|
|
813
|
+
newLineNum++
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
return result.length > 0 ? result : null
|
|
818
|
+
}, [valuesYaml, defaultValuesYaml, hasCustomValues, hasDefaults])
|
|
819
|
+
|
|
820
|
+
const hasChanges = diffLines && diffLines.some(l => l.type === 'add' || l.type === 'remove')
|
|
821
|
+
|
|
822
|
+
return (
|
|
823
|
+
<div className="space-y-6">
|
|
824
|
+
<div className="flex items-center gap-3 p-4 bg-blue-500/10 border border-blue-500/30 rounded-lg">
|
|
825
|
+
<CheckCircle className="w-6 h-6 text-blue-400 shrink-0" />
|
|
826
|
+
<div>
|
|
827
|
+
<p className="text-sm font-medium text-theme-text-primary">Ready to install</p>
|
|
828
|
+
<p className="text-xs text-theme-text-secondary mt-0.5">
|
|
829
|
+
Review the configuration below and click Install to proceed
|
|
830
|
+
</p>
|
|
831
|
+
</div>
|
|
832
|
+
</div>
|
|
833
|
+
|
|
834
|
+
{/* ArtifactHub notice */}
|
|
835
|
+
{source === 'artifacthub' && (
|
|
836
|
+
<div className="flex items-start gap-3 p-4 bg-amber-500/10 border border-amber-500/30 rounded-lg">
|
|
837
|
+
<Globe className="w-5 h-5 text-amber-400 shrink-0 mt-0.5" />
|
|
838
|
+
<div>
|
|
839
|
+
<p className="text-sm font-medium text-amber-400">Installing from ArtifactHub</p>
|
|
840
|
+
<p className="text-xs text-theme-text-secondary mt-1">
|
|
841
|
+
This chart will be installed from: <code className="bg-theme-elevated px-1 rounded">{artifactHubRepoUrl || repo}</code>
|
|
842
|
+
</p>
|
|
843
|
+
</div>
|
|
844
|
+
</div>
|
|
845
|
+
)}
|
|
846
|
+
|
|
847
|
+
<div className="bg-theme-elevated/30 rounded-lg p-4 space-y-3">
|
|
848
|
+
<h3 className="text-sm font-medium text-theme-text-secondary">Installation Summary</h3>
|
|
849
|
+
<div className="grid grid-cols-2 gap-3 text-sm">
|
|
850
|
+
<div>
|
|
851
|
+
<dt className="text-theme-text-tertiary">Release Name</dt>
|
|
852
|
+
<dd className="text-theme-text-primary font-medium">{releaseName}</dd>
|
|
853
|
+
</div>
|
|
854
|
+
<div>
|
|
855
|
+
<dt className="text-theme-text-tertiary">Namespace</dt>
|
|
856
|
+
<dd className="text-theme-text-primary font-medium">
|
|
857
|
+
{namespace}
|
|
858
|
+
{createNamespace && (
|
|
859
|
+
<span className="ml-2 text-xs text-amber-400">(will be created)</span>
|
|
860
|
+
)}
|
|
861
|
+
</dd>
|
|
862
|
+
</div>
|
|
863
|
+
<div>
|
|
864
|
+
<dt className="text-theme-text-tertiary">Chart</dt>
|
|
865
|
+
<dd className="text-theme-text-primary">{chartName}</dd>
|
|
866
|
+
</div>
|
|
867
|
+
<div>
|
|
868
|
+
<dt className="text-theme-text-tertiary">Version</dt>
|
|
869
|
+
<dd className="text-theme-text-primary">{version}</dd>
|
|
870
|
+
</div>
|
|
871
|
+
<div>
|
|
872
|
+
<dt className="text-theme-text-tertiary">Source</dt>
|
|
873
|
+
<dd className="text-theme-text-primary flex items-center gap-1">
|
|
874
|
+
{source === 'local' ? (
|
|
875
|
+
<>Local: {repo}</>
|
|
876
|
+
) : (
|
|
877
|
+
<>
|
|
878
|
+
<Globe className="w-3.5 h-3.5 text-blue-400" />
|
|
879
|
+
ArtifactHub: {repo}
|
|
880
|
+
</>
|
|
881
|
+
)}
|
|
882
|
+
</dd>
|
|
883
|
+
</div>
|
|
884
|
+
<div>
|
|
885
|
+
<dt className="text-theme-text-tertiary">Custom Values</dt>
|
|
886
|
+
<dd className="text-theme-text-primary">
|
|
887
|
+
{hasChanges ? 'Modified' : hasCustomValues ? 'Using defaults' : 'No (using defaults)'}
|
|
888
|
+
</dd>
|
|
889
|
+
</div>
|
|
890
|
+
</div>
|
|
891
|
+
</div>
|
|
892
|
+
|
|
893
|
+
{/* Values diff view */}
|
|
894
|
+
{hasChanges && diffLines && (
|
|
895
|
+
<div className="bg-theme-elevated/30 rounded-lg p-4">
|
|
896
|
+
<h3 className="text-sm font-medium text-theme-text-secondary mb-3">Values Changes</h3>
|
|
897
|
+
<div className="bg-theme-base/50 rounded overflow-auto max-h-64 font-mono text-xs">
|
|
898
|
+
{diffLines.map((line, i) => {
|
|
899
|
+
if (line.type === 'header') {
|
|
900
|
+
return (
|
|
901
|
+
<div key={i} className="px-3 py-1 bg-blue-500/10 text-blue-400 border-y border-theme-border">
|
|
902
|
+
{line.content}
|
|
903
|
+
</div>
|
|
904
|
+
)
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
return (
|
|
908
|
+
<div
|
|
909
|
+
key={i}
|
|
910
|
+
className={clsx(
|
|
911
|
+
'flex items-start px-3 py-0.5',
|
|
912
|
+
line.type === 'add' && 'bg-green-500/10',
|
|
913
|
+
line.type === 'remove' && 'bg-red-500/10'
|
|
914
|
+
)}
|
|
915
|
+
>
|
|
916
|
+
<span className={clsx(
|
|
917
|
+
'w-4 shrink-0',
|
|
918
|
+
line.type === 'add' && 'text-green-400',
|
|
919
|
+
line.type === 'remove' && 'text-red-400',
|
|
920
|
+
line.type === 'context' && 'text-theme-text-disabled'
|
|
921
|
+
)}>
|
|
922
|
+
{line.type === 'add' && <Plus className="w-3 h-3 inline" />}
|
|
923
|
+
{line.type === 'remove' && <Minus className="w-3 h-3 inline" />}
|
|
924
|
+
</span>
|
|
925
|
+
<span className={clsx(
|
|
926
|
+
'flex-1 whitespace-pre',
|
|
927
|
+
line.type === 'add' && 'text-green-400',
|
|
928
|
+
line.type === 'remove' && 'text-red-400',
|
|
929
|
+
line.type === 'context' && 'text-theme-text-secondary'
|
|
930
|
+
)}>
|
|
931
|
+
{line.content || ' '}
|
|
932
|
+
</span>
|
|
933
|
+
</div>
|
|
934
|
+
)
|
|
935
|
+
})}
|
|
936
|
+
</div>
|
|
937
|
+
</div>
|
|
938
|
+
)}
|
|
939
|
+
</div>
|
|
940
|
+
)
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// Installing step - shows progress logs during installation
|
|
944
|
+
interface InstallingStepProps {
|
|
945
|
+
releaseName: string
|
|
946
|
+
namespace: string
|
|
947
|
+
chartName: string
|
|
948
|
+
progressLogs: ProgressEntry[]
|
|
949
|
+
isInstalling: boolean
|
|
950
|
+
installError: string | null
|
|
951
|
+
progressEndRef: React.RefObject<HTMLDivElement | null>
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function InstallingStep({
|
|
955
|
+
releaseName,
|
|
956
|
+
namespace,
|
|
957
|
+
chartName,
|
|
958
|
+
progressLogs,
|
|
959
|
+
isInstalling,
|
|
960
|
+
installError,
|
|
961
|
+
progressEndRef,
|
|
962
|
+
}: InstallingStepProps) {
|
|
963
|
+
const isComplete = progressLogs.some(l => l.phase === 'complete')
|
|
964
|
+
|
|
965
|
+
return (
|
|
966
|
+
<div className="space-y-6">
|
|
967
|
+
{/* Header */}
|
|
968
|
+
<div className={clsx(
|
|
969
|
+
'flex items-center gap-3 p-4 rounded-lg border',
|
|
970
|
+
isComplete ? 'bg-green-500/10 border-green-500/30' :
|
|
971
|
+
installError ? 'bg-red-500/10 border-red-500/30' :
|
|
972
|
+
'bg-blue-500/10 border-blue-500/30'
|
|
973
|
+
)}>
|
|
974
|
+
{isComplete ? (
|
|
975
|
+
<CheckCircle className="w-6 h-6 text-green-400 shrink-0" />
|
|
976
|
+
) : installError ? (
|
|
977
|
+
<AlertTriangle className="w-6 h-6 text-red-400 shrink-0" />
|
|
978
|
+
) : (
|
|
979
|
+
<Loader2 className="w-6 h-6 text-blue-400 shrink-0 animate-spin" />
|
|
980
|
+
)}
|
|
981
|
+
<div>
|
|
982
|
+
<p className={clsx(
|
|
983
|
+
'text-sm font-medium',
|
|
984
|
+
isComplete ? 'text-green-400' :
|
|
985
|
+
installError ? 'text-red-400' :
|
|
986
|
+
'text-theme-text-primary'
|
|
987
|
+
)}>
|
|
988
|
+
{isComplete ? 'Installation Complete' :
|
|
989
|
+
installError ? 'Installation Failed' :
|
|
990
|
+
`Installing ${chartName}...`}
|
|
991
|
+
</p>
|
|
992
|
+
<p className="text-xs text-theme-text-secondary mt-0.5">
|
|
993
|
+
{isComplete ? `${releaseName} is now running in ${namespace}` :
|
|
994
|
+
installError ? 'See logs below for details' :
|
|
995
|
+
`Deploying to namespace ${namespace}`}
|
|
996
|
+
</p>
|
|
997
|
+
</div>
|
|
998
|
+
</div>
|
|
999
|
+
|
|
1000
|
+
{/* Progress logs */}
|
|
1001
|
+
<div className="bg-theme-base rounded-lg border border-theme-border overflow-hidden">
|
|
1002
|
+
<div className="flex items-center gap-2 px-4 py-2 border-b border-theme-border bg-theme-elevated/50">
|
|
1003
|
+
<Terminal className="w-4 h-4 text-theme-text-tertiary" />
|
|
1004
|
+
<span className="text-xs font-medium text-theme-text-secondary">Install Progress</span>
|
|
1005
|
+
</div>
|
|
1006
|
+
<div className="p-3 max-h-64 overflow-auto font-mono text-xs">
|
|
1007
|
+
{progressLogs.length === 0 && isInstalling && (
|
|
1008
|
+
<div className="text-theme-text-tertiary">Starting installation...</div>
|
|
1009
|
+
)}
|
|
1010
|
+
{progressLogs.map((log, i) => (
|
|
1011
|
+
<div
|
|
1012
|
+
key={i}
|
|
1013
|
+
className={clsx(
|
|
1014
|
+
'flex items-start gap-2 py-1',
|
|
1015
|
+
log.phase === 'error' && 'text-red-400',
|
|
1016
|
+
log.phase === 'complete' && 'text-green-400'
|
|
1017
|
+
)}
|
|
1018
|
+
>
|
|
1019
|
+
<span className="text-theme-text-disabled shrink-0 w-16">
|
|
1020
|
+
{log.timestamp.toLocaleTimeString()}
|
|
1021
|
+
</span>
|
|
1022
|
+
<span className={clsx(
|
|
1023
|
+
'shrink-0 badge-sm uppercase',
|
|
1024
|
+
log.phase === 'error' ? SEVERITY_BADGE.error :
|
|
1025
|
+
log.phase === 'complete' ? SEVERITY_BADGE.success :
|
|
1026
|
+
'bg-theme-elevated text-theme-text-tertiary'
|
|
1027
|
+
)}>
|
|
1028
|
+
{log.phase}
|
|
1029
|
+
</span>
|
|
1030
|
+
<span className={clsx(
|
|
1031
|
+
log.phase === 'error' ? SEVERITY_TEXT.error :
|
|
1032
|
+
log.phase === 'complete' ? SEVERITY_TEXT.success :
|
|
1033
|
+
'text-theme-text-secondary'
|
|
1034
|
+
)}>
|
|
1035
|
+
{log.message}
|
|
1036
|
+
</span>
|
|
1037
|
+
</div>
|
|
1038
|
+
))}
|
|
1039
|
+
{isInstalling && (
|
|
1040
|
+
<div className="flex items-center gap-2 py-1 text-theme-text-tertiary">
|
|
1041
|
+
<Loader2 className="w-3 h-3 animate-spin" />
|
|
1042
|
+
<span>Waiting for resources...</span>
|
|
1043
|
+
</div>
|
|
1044
|
+
)}
|
|
1045
|
+
<div ref={progressEndRef} />
|
|
1046
|
+
</div>
|
|
1047
|
+
</div>
|
|
1048
|
+
|
|
1049
|
+
{/* Error details if failed */}
|
|
1050
|
+
{installError && (
|
|
1051
|
+
<div className="bg-red-500/10 border border-red-500/30 rounded-lg p-4">
|
|
1052
|
+
<p className="text-sm font-medium text-red-400 mb-2">Error Details</p>
|
|
1053
|
+
<pre className="text-xs text-red-300 whitespace-pre-wrap font-mono">
|
|
1054
|
+
{installError}
|
|
1055
|
+
</pre>
|
|
1056
|
+
</div>
|
|
1057
|
+
)}
|
|
1058
|
+
</div>
|
|
1059
|
+
)
|
|
1060
|
+
}
|