@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,301 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react'
|
|
2
|
+
import { X } from 'lucide-react'
|
|
3
|
+
import { clsx } from 'clsx'
|
|
4
|
+
import { TRANSITION_BACKDROP, TRANSITION_PANEL } from '../../utils/animation'
|
|
5
|
+
import { useActiveShortcuts, type ShortcutCategory } from '../../hooks/useKeyboardShortcuts'
|
|
6
|
+
|
|
7
|
+
interface ShortcutHelpOverlayProps {
|
|
8
|
+
onClose: () => void
|
|
9
|
+
currentView?: string
|
|
10
|
+
/** Controls fade-in/out animation (driven by useAnimatedUnmount) */
|
|
11
|
+
isOpen?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Categories that always appear at the top
|
|
15
|
+
const GLOBAL_CATEGORIES: ShortcutCategory[] = ['Navigation', 'General']
|
|
16
|
+
|
|
17
|
+
// Categories tied to contextual UI (not a specific view)
|
|
18
|
+
const CONTEXT_CATEGORIES: ShortcutCategory[] = ['Drawer', 'Dock']
|
|
19
|
+
|
|
20
|
+
// Preferred ordering within the view section
|
|
21
|
+
const VIEW_CATEGORY_ORDER: ShortcutCategory[] = [
|
|
22
|
+
'Search', 'Table', 'Resource Actions', 'Topology', 'Timeline', 'Helm',
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
const VIEW_LABELS: Record<string, string> = {
|
|
26
|
+
home: 'Home',
|
|
27
|
+
topology: 'Topology',
|
|
28
|
+
resources: 'Resources',
|
|
29
|
+
timeline: 'Timeline',
|
|
30
|
+
helm: 'Helm',
|
|
31
|
+
traffic: 'Traffic',
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type ShortcutEntry = { description: string; keys: string[] }
|
|
35
|
+
|
|
36
|
+
function KbdKey({ children }: { children: string }) {
|
|
37
|
+
return (
|
|
38
|
+
<kbd className="inline-flex items-center justify-center min-w-[1.5rem] h-6 px-1.5 text-xs font-mono font-medium bg-theme-elevated border border-theme-border-light rounded text-theme-text-primary shadow-sm">
|
|
39
|
+
{children}
|
|
40
|
+
</kbd>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function ShortcutKeys({ keys }: { keys: string }) {
|
|
45
|
+
// Handle multi-key sequences like "g g"
|
|
46
|
+
if (keys.includes(' ') && !keys.includes('+')) {
|
|
47
|
+
const parts = keys.split(' ')
|
|
48
|
+
return (
|
|
49
|
+
<span className="flex items-center gap-1">
|
|
50
|
+
{parts.map((part, i) => (
|
|
51
|
+
<span key={i} className="flex items-center gap-0.5">
|
|
52
|
+
{i > 0 && <span className="text-theme-text-tertiary text-xs mx-0.5"></span>}
|
|
53
|
+
<KbdKey>{part}</KbdKey>
|
|
54
|
+
</span>
|
|
55
|
+
))}
|
|
56
|
+
</span>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Handle modifier combos like "Cmd+K", "Ctrl+D", "Shift+N"
|
|
61
|
+
// But not the literal "+" key itself
|
|
62
|
+
if (keys.includes('+') && keys !== '+') {
|
|
63
|
+
const parts = keys.split('+')
|
|
64
|
+
return (
|
|
65
|
+
<span className="flex items-center gap-0.5">
|
|
66
|
+
{parts.map((part, i) => (
|
|
67
|
+
<span key={i} className="flex items-center gap-0.5">
|
|
68
|
+
{i > 0 && <span className="text-theme-text-tertiary text-[10px]">+</span>}
|
|
69
|
+
<KbdKey>{formatKeyLabel(part)}</KbdKey>
|
|
70
|
+
</span>
|
|
71
|
+
))}
|
|
72
|
+
</span>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Single key
|
|
77
|
+
return <KbdKey>{formatKeyLabel(keys)}</KbdKey>
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function formatKeyLabel(key: string): string {
|
|
81
|
+
const isMac = typeof navigator !== 'undefined' && navigator.platform.includes('Mac')
|
|
82
|
+
switch (key.toLowerCase()) {
|
|
83
|
+
case 'cmd':
|
|
84
|
+
case 'meta': return isMac ? '⌘' : 'Ctrl'
|
|
85
|
+
case 'ctrl': return 'Ctrl'
|
|
86
|
+
case 'shift': return isMac ? '⇧' : 'Shift'
|
|
87
|
+
case 'alt': return isMac ? '⌥' : 'Alt'
|
|
88
|
+
case 'escape': return 'Esc'
|
|
89
|
+
case 'enter': return '↵'
|
|
90
|
+
case 'arrowup': return '↑'
|
|
91
|
+
case 'arrowdown': return '↓'
|
|
92
|
+
case 'arrowleft': return '←'
|
|
93
|
+
case 'arrowright': return '→'
|
|
94
|
+
case '`': return '`'
|
|
95
|
+
default: return key
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function CategoryBlock({ category, entries }: { category: string; entries: ShortcutEntry[] }) {
|
|
100
|
+
return (
|
|
101
|
+
<div>
|
|
102
|
+
<h3 className="text-xs font-semibold text-theme-text-tertiary uppercase tracking-wider mb-2.5">
|
|
103
|
+
{category}
|
|
104
|
+
</h3>
|
|
105
|
+
<div className="space-y-1.5">
|
|
106
|
+
{entries.map(entry => (
|
|
107
|
+
<div key={entry.description} className="flex items-center justify-between py-1">
|
|
108
|
+
<span className="text-sm text-theme-text-secondary">{entry.description}</span>
|
|
109
|
+
<span className="flex items-center gap-1.5 ml-4 shrink-0">
|
|
110
|
+
{entry.keys.map((k, i) => (
|
|
111
|
+
<span key={k} className="flex items-center gap-1.5">
|
|
112
|
+
{i > 0 && <span className="text-theme-text-tertiary text-[10px]">/</span>}
|
|
113
|
+
<ShortcutKeys keys={k} />
|
|
114
|
+
</span>
|
|
115
|
+
))}
|
|
116
|
+
</span>
|
|
117
|
+
</div>
|
|
118
|
+
))}
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Balance categories into two columns by item count (descending-greedy for best packing)
|
|
125
|
+
function balanceColumns(categories: ShortcutCategory[], grouped: Map<ShortcutCategory, ShortcutEntry[]>): [ShortcutCategory[], ShortcutCategory[]] {
|
|
126
|
+
const sorted = [...categories].sort((a, b) =>
|
|
127
|
+
(grouped.get(b)?.length || 0) - (grouped.get(a)?.length || 0)
|
|
128
|
+
)
|
|
129
|
+
const leftSet = new Set<ShortcutCategory>()
|
|
130
|
+
const rightSet = new Set<ShortcutCategory>()
|
|
131
|
+
let leftCount = 0, rightCount = 0
|
|
132
|
+
|
|
133
|
+
for (const cat of sorted) {
|
|
134
|
+
const count = grouped.get(cat)!.length
|
|
135
|
+
if (leftCount <= rightCount) {
|
|
136
|
+
leftSet.add(cat)
|
|
137
|
+
leftCount += count
|
|
138
|
+
} else {
|
|
139
|
+
rightSet.add(cat)
|
|
140
|
+
rightCount += count
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Preserve original ordering within each column
|
|
145
|
+
const left = categories.filter(c => leftSet.has(c))
|
|
146
|
+
const right = categories.filter(c => rightSet.has(c))
|
|
147
|
+
return [left, right]
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function TwoColumnSection({ categories, grouped }: { categories: ShortcutCategory[]; grouped: Map<ShortcutCategory, ShortcutEntry[]> }) {
|
|
151
|
+
if (categories.length === 0) return null
|
|
152
|
+
|
|
153
|
+
if (categories.length <= 2) {
|
|
154
|
+
// Sort larger category to the left for visual balance
|
|
155
|
+
const sorted = [...categories].sort((a, b) =>
|
|
156
|
+
(grouped.get(b)?.length || 0) - (grouped.get(a)?.length || 0)
|
|
157
|
+
)
|
|
158
|
+
return (
|
|
159
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-4">
|
|
160
|
+
{sorted.map(cat => (
|
|
161
|
+
<CategoryBlock key={cat} category={cat} entries={grouped.get(cat)!} />
|
|
162
|
+
))}
|
|
163
|
+
</div>
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const [left, right] = balanceColumns(categories, grouped)
|
|
168
|
+
return (
|
|
169
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-8">
|
|
170
|
+
<div className="space-y-4">
|
|
171
|
+
{left.map(cat => <CategoryBlock key={cat} category={cat} entries={grouped.get(cat)!} />)}
|
|
172
|
+
</div>
|
|
173
|
+
<div className="space-y-4">
|
|
174
|
+
{right.map(cat => <CategoryBlock key={cat} category={cat} entries={grouped.get(cat)!} />)}
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function ShortcutHelpOverlay({ onClose, currentView, isOpen = true }: ShortcutHelpOverlayProps) {
|
|
181
|
+
const shortcuts = useActiveShortcuts()
|
|
182
|
+
const overlayRef = useRef<HTMLDivElement>(null)
|
|
183
|
+
|
|
184
|
+
// Close on Escape
|
|
185
|
+
useEffect(() => {
|
|
186
|
+
const handler = (e: KeyboardEvent) => {
|
|
187
|
+
if (e.key === 'Escape') {
|
|
188
|
+
e.preventDefault()
|
|
189
|
+
e.stopPropagation()
|
|
190
|
+
onClose()
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Use capture to intercept before the shortcut system
|
|
194
|
+
document.addEventListener('keydown', handler, true)
|
|
195
|
+
return () => document.removeEventListener('keydown', handler, true)
|
|
196
|
+
}, [onClose])
|
|
197
|
+
|
|
198
|
+
// Group shortcuts by category, merging duplicates with same description
|
|
199
|
+
// (e.g., "Zoom in" registered for both + and = shows as one row)
|
|
200
|
+
const grouped = new Map<ShortcutCategory, ShortcutEntry[]>()
|
|
201
|
+
for (const s of shortcuts) {
|
|
202
|
+
if (s.id === 'help-toggle') continue
|
|
203
|
+
const list = grouped.get(s.category) || []
|
|
204
|
+
const existing = list.find(item => item.description === s.description)
|
|
205
|
+
if (existing) {
|
|
206
|
+
existing.keys.push(s.keys)
|
|
207
|
+
} else {
|
|
208
|
+
list.push({ description: s.description, keys: [s.keys] })
|
|
209
|
+
}
|
|
210
|
+
grouped.set(s.category, list)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Split categories into sections
|
|
214
|
+
const globalCategories = GLOBAL_CATEGORIES.filter(c => grouped.has(c))
|
|
215
|
+
const viewCategories = VIEW_CATEGORY_ORDER.filter(
|
|
216
|
+
c => grouped.has(c) && !GLOBAL_CATEGORIES.includes(c) && !CONTEXT_CATEGORIES.includes(c)
|
|
217
|
+
)
|
|
218
|
+
const contextCategories = CONTEXT_CATEGORIES.filter(c => grouped.has(c))
|
|
219
|
+
|
|
220
|
+
const viewLabel = currentView ? VIEW_LABELS[currentView] : null
|
|
221
|
+
const hasViewSection = viewCategories.length > 0
|
|
222
|
+
const hasContextSection = contextCategories.length > 0
|
|
223
|
+
const isEmpty = globalCategories.length === 0 && !hasViewSection && !hasContextSection
|
|
224
|
+
|
|
225
|
+
return (
|
|
226
|
+
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
|
|
227
|
+
{/* Backdrop */}
|
|
228
|
+
<div
|
|
229
|
+
className={clsx(
|
|
230
|
+
'absolute inset-0 bg-theme-base/70 backdrop-blur-sm',
|
|
231
|
+
TRANSITION_BACKDROP,
|
|
232
|
+
isOpen ? 'opacity-100' : 'opacity-0'
|
|
233
|
+
)}
|
|
234
|
+
onClick={onClose}
|
|
235
|
+
/>
|
|
236
|
+
|
|
237
|
+
{/* Panel */}
|
|
238
|
+
<div
|
|
239
|
+
ref={overlayRef}
|
|
240
|
+
className={clsx(
|
|
241
|
+
'relative w-full max-w-2xl max-h-[80vh] dialog overflow-hidden flex flex-col',
|
|
242
|
+
TRANSITION_PANEL,
|
|
243
|
+
isOpen ? 'opacity-100 scale-100' : 'opacity-0 scale-[0.97]'
|
|
244
|
+
)}
|
|
245
|
+
>
|
|
246
|
+
{/* Header */}
|
|
247
|
+
<div className="flex items-center justify-between px-5 py-3.5 border-b border-theme-border">
|
|
248
|
+
<h2 className="text-base font-semibold text-theme-text-primary">Keyboard Shortcuts</h2>
|
|
249
|
+
<button
|
|
250
|
+
onClick={onClose}
|
|
251
|
+
className="p-1.5 rounded-md text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-hover transition-colors"
|
|
252
|
+
>
|
|
253
|
+
<X className="w-4 h-4" />
|
|
254
|
+
</button>
|
|
255
|
+
</div>
|
|
256
|
+
|
|
257
|
+
{/* Content */}
|
|
258
|
+
<div className="overflow-y-auto px-5 py-4 space-y-5">
|
|
259
|
+
{/* Global section — Navigation + General */}
|
|
260
|
+
<TwoColumnSection categories={globalCategories} grouped={grouped} />
|
|
261
|
+
|
|
262
|
+
{/* View-specific section */}
|
|
263
|
+
{hasViewSection && (
|
|
264
|
+
<>
|
|
265
|
+
{viewLabel && (
|
|
266
|
+
<div className="flex items-center gap-3">
|
|
267
|
+
<div className="h-px flex-1 bg-theme-border-light" />
|
|
268
|
+
<span className="text-[10px] font-semibold text-theme-text-tertiary uppercase tracking-wider">
|
|
269
|
+
{viewLabel}
|
|
270
|
+
</span>
|
|
271
|
+
<div className="h-px flex-1 bg-theme-border-light" />
|
|
272
|
+
</div>
|
|
273
|
+
)}
|
|
274
|
+
<TwoColumnSection categories={viewCategories} grouped={grouped} />
|
|
275
|
+
</>
|
|
276
|
+
)}
|
|
277
|
+
|
|
278
|
+
{/* Contextual section (Drawer, Dock) */}
|
|
279
|
+
{hasContextSection && (
|
|
280
|
+
<TwoColumnSection categories={contextCategories} grouped={grouped} />
|
|
281
|
+
)}
|
|
282
|
+
|
|
283
|
+
{/* Empty state */}
|
|
284
|
+
{isEmpty && (
|
|
285
|
+
<p className="text-sm text-theme-text-tertiary text-center py-8">
|
|
286
|
+
No keyboard shortcuts registered.
|
|
287
|
+
</p>
|
|
288
|
+
)}
|
|
289
|
+
</div>
|
|
290
|
+
|
|
291
|
+
{/* Footer */}
|
|
292
|
+
<div className="px-5 py-2.5 border-t border-theme-border bg-theme-surface/50">
|
|
293
|
+
<div className="flex items-center justify-between text-xs text-theme-text-tertiary">
|
|
294
|
+
<span>Press <KbdKey>?</KbdKey> to toggle this overlay</span>
|
|
295
|
+
<span>Press <KbdKey>Esc</KbdKey> to close</span>
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
)
|
|
301
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ToastProvider, useToast, showApiError, showApiSuccess } from '@skyhook-io/k8s-ui'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Tooltip, WithTooltip } from '@skyhook-io/k8s-ui/components/ui/Tooltip'
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
import { Download, X, Copy, Check, RotateCw, ArrowDownToLine, Loader2 } from 'lucide-react'
|
|
3
|
+
import { useQueryClient } from '@tanstack/react-query'
|
|
4
|
+
import {
|
|
5
|
+
useVersionCheck,
|
|
6
|
+
useStartDesktopUpdate,
|
|
7
|
+
useDesktopUpdateStatus,
|
|
8
|
+
useApplyDesktopUpdate,
|
|
9
|
+
} from '../../api/client'
|
|
10
|
+
import type { DesktopUpdateState } from '../../api/client'
|
|
11
|
+
|
|
12
|
+
const DISMISSED_KEY = 'radar-update-dismissed'
|
|
13
|
+
|
|
14
|
+
export function UpdateNotification() {
|
|
15
|
+
const queryClient = useQueryClient()
|
|
16
|
+
const { data: versionInfo } = useVersionCheck()
|
|
17
|
+
const [dismissed, setDismissed] = useState(false)
|
|
18
|
+
const [copied, setCopied] = useState(false)
|
|
19
|
+
const [copyFailed, setCopyFailed] = useState(false)
|
|
20
|
+
|
|
21
|
+
// Desktop update state
|
|
22
|
+
const [desktopUpdating, setDesktopUpdating] = useState(false)
|
|
23
|
+
const startUpdate = useStartDesktopUpdate()
|
|
24
|
+
const applyUpdate = useApplyDesktopUpdate()
|
|
25
|
+
const { data: updateStatus } = useDesktopUpdateStatus(desktopUpdating)
|
|
26
|
+
|
|
27
|
+
const isDesktop = versionInfo?.installMethod === 'desktop'
|
|
28
|
+
|
|
29
|
+
// Listen for "Check for Updates" menu item in desktop app (Wails runtime event).
|
|
30
|
+
// Un-dismisses the notification and invalidates the version check cache.
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
const wailsRuntime = (window as unknown as Record<string, unknown>).runtime as
|
|
33
|
+
| { EventsOn?: (event: string, callback: () => void) => () => void }
|
|
34
|
+
| undefined
|
|
35
|
+
if (!wailsRuntime?.EventsOn) return
|
|
36
|
+
|
|
37
|
+
const cleanup = wailsRuntime.EventsOn('check-for-updates', () => {
|
|
38
|
+
setDismissed(false)
|
|
39
|
+
try { localStorage.removeItem(DISMISSED_KEY) } catch { /* ignore */ }
|
|
40
|
+
queryClient.invalidateQueries({ queryKey: ['version-check'] })
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
return cleanup
|
|
44
|
+
}, [queryClient])
|
|
45
|
+
|
|
46
|
+
// Log version check errors for debugging
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (versionInfo?.error) {
|
|
49
|
+
console.debug('[radar] Version check failed:', versionInfo.error)
|
|
50
|
+
}
|
|
51
|
+
}, [versionInfo?.error])
|
|
52
|
+
|
|
53
|
+
// Check if this version was already dismissed
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (versionInfo?.latestVersion) {
|
|
56
|
+
try {
|
|
57
|
+
const dismissedVersion = localStorage.getItem(DISMISSED_KEY)
|
|
58
|
+
if (dismissedVersion === versionInfo.latestVersion) {
|
|
59
|
+
setDismissed(true)
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
// localStorage unavailable (e.g. Safari private mode)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}, [versionInfo?.latestVersion])
|
|
66
|
+
|
|
67
|
+
// Stop polling when update reaches a terminal state
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (updateStatus?.state === 'error' || updateStatus?.state === 'idle') {
|
|
70
|
+
setDesktopUpdating(false)
|
|
71
|
+
}
|
|
72
|
+
}, [updateStatus?.state])
|
|
73
|
+
|
|
74
|
+
const handleDismiss = () => {
|
|
75
|
+
try {
|
|
76
|
+
if (versionInfo?.latestVersion) {
|
|
77
|
+
localStorage.setItem(DISMISSED_KEY, versionInfo.latestVersion)
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
// localStorage unavailable — dismiss in-memory only
|
|
81
|
+
}
|
|
82
|
+
setDismissed(true)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const handleCopyCommand = async () => {
|
|
86
|
+
if (versionInfo?.updateCommand) {
|
|
87
|
+
try {
|
|
88
|
+
await navigator.clipboard.writeText(versionInfo.updateCommand)
|
|
89
|
+
setCopied(true)
|
|
90
|
+
setTimeout(() => setCopied(false), 2000)
|
|
91
|
+
} catch (err) {
|
|
92
|
+
console.debug('[radar] Clipboard write failed:', err)
|
|
93
|
+
setCopyFailed(true)
|
|
94
|
+
setTimeout(() => setCopyFailed(false), 2000)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const handleStartDesktopUpdate = () => {
|
|
100
|
+
startUpdate.mutate(undefined, {
|
|
101
|
+
onSuccess: () => setDesktopUpdating(true),
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Don't show if no update available, dismissed, or error
|
|
106
|
+
if (!versionInfo?.updateAvailable || dismissed) {
|
|
107
|
+
return null
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Determine what the current effective state is
|
|
111
|
+
const effectiveState: DesktopUpdateState = updateStatus?.state ?? 'idle'
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<div className="fixed bottom-4 right-4 z-50 max-w-sm bg-theme-surface border border-blue-500/50 rounded-lg shadow-xl p-4 animate-in slide-in-from-right">
|
|
115
|
+
<div className="flex items-start gap-3">
|
|
116
|
+
<div className="flex items-center justify-center w-8 h-8 bg-blue-500/20 rounded-full shrink-0">
|
|
117
|
+
<UpdateIcon state={effectiveState} />
|
|
118
|
+
</div>
|
|
119
|
+
<div className="flex-1 min-w-0">
|
|
120
|
+
<h4 className="text-sm font-medium text-theme-text-primary">
|
|
121
|
+
<UpdateTitle state={effectiveState} />
|
|
122
|
+
</h4>
|
|
123
|
+
<p className="text-xs text-theme-text-secondary mt-1">
|
|
124
|
+
Radar {versionInfo.latestVersion} is available.{' '}
|
|
125
|
+
You're on {versionInfo.currentVersion}.
|
|
126
|
+
</p>
|
|
127
|
+
|
|
128
|
+
{/* Desktop: in-app update flow */}
|
|
129
|
+
{isDesktop && (
|
|
130
|
+
<DesktopUpdateControls
|
|
131
|
+
state={effectiveState}
|
|
132
|
+
progress={updateStatus?.progress}
|
|
133
|
+
error={updateStatus?.error}
|
|
134
|
+
starting={startUpdate.isPending}
|
|
135
|
+
onStart={handleStartDesktopUpdate}
|
|
136
|
+
onApply={() => applyUpdate.mutate()}
|
|
137
|
+
onRetry={handleStartDesktopUpdate}
|
|
138
|
+
/>
|
|
139
|
+
)}
|
|
140
|
+
|
|
141
|
+
{/* CLI: show update command with copy button for package managers */}
|
|
142
|
+
{!isDesktop && versionInfo.updateCommand ? (
|
|
143
|
+
<button
|
|
144
|
+
onClick={handleCopyCommand}
|
|
145
|
+
className="flex items-center gap-2 mt-2 px-2 py-1.5 bg-theme-elevated rounded text-xs font-mono text-theme-text-primary hover:bg-theme-surface-hover transition-colors w-full"
|
|
146
|
+
>
|
|
147
|
+
<code className="flex-1 text-left truncate">{versionInfo.updateCommand}</code>
|
|
148
|
+
<CopyIcon copied={copied} failed={copyFailed} />
|
|
149
|
+
</button>
|
|
150
|
+
) : (
|
|
151
|
+
/* Direct download - show release link */
|
|
152
|
+
!isDesktop && versionInfo.releaseUrl && (
|
|
153
|
+
<a
|
|
154
|
+
href={versionInfo.releaseUrl}
|
|
155
|
+
target="_blank"
|
|
156
|
+
rel="noopener noreferrer"
|
|
157
|
+
className="inline-flex items-center gap-1 mt-2 text-xs font-medium text-blue-400 hover:text-blue-300"
|
|
158
|
+
>
|
|
159
|
+
Download from GitHub →
|
|
160
|
+
</a>
|
|
161
|
+
)
|
|
162
|
+
)}
|
|
163
|
+
</div>
|
|
164
|
+
{/* Don't show dismiss during active update */}
|
|
165
|
+
{effectiveState !== 'downloading' && effectiveState !== 'applying' && (
|
|
166
|
+
<button
|
|
167
|
+
onClick={handleDismiss}
|
|
168
|
+
className="p-1 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded shrink-0"
|
|
169
|
+
aria-label="Dismiss"
|
|
170
|
+
>
|
|
171
|
+
<X className="w-4 h-4" />
|
|
172
|
+
</button>
|
|
173
|
+
)}
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function CopyIcon({ copied, failed }: { copied: boolean; failed: boolean }) {
|
|
180
|
+
if (copied) return <Check className="w-3.5 h-3.5 text-green-400 shrink-0" />
|
|
181
|
+
if (failed) return <X className="w-3.5 h-3.5 text-red-400 shrink-0" />
|
|
182
|
+
return <Copy className="w-3.5 h-3.5 text-theme-text-tertiary shrink-0" />
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function UpdateIcon({ state }: { state: DesktopUpdateState }) {
|
|
186
|
+
switch (state) {
|
|
187
|
+
case 'downloading':
|
|
188
|
+
case 'applying':
|
|
189
|
+
return <Loader2 className="w-4 h-4 text-blue-400 animate-spin" />
|
|
190
|
+
case 'ready':
|
|
191
|
+
return <ArrowDownToLine className="w-4 h-4 text-green-400" />
|
|
192
|
+
default:
|
|
193
|
+
return <Download className="w-4 h-4 text-blue-400" />
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function UpdateTitle({ state }: { state: DesktopUpdateState }) {
|
|
198
|
+
switch (state) {
|
|
199
|
+
case 'ready':
|
|
200
|
+
return <>Update Ready</>
|
|
201
|
+
case 'applying':
|
|
202
|
+
return <>Applying Update...</>
|
|
203
|
+
default:
|
|
204
|
+
return <>Update Available</>
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// DesktopUpdateControls renders the update action area for desktop installs.
|
|
209
|
+
function DesktopUpdateControls({
|
|
210
|
+
state,
|
|
211
|
+
progress,
|
|
212
|
+
error,
|
|
213
|
+
starting,
|
|
214
|
+
onStart,
|
|
215
|
+
onApply,
|
|
216
|
+
onRetry,
|
|
217
|
+
}: {
|
|
218
|
+
state: DesktopUpdateState
|
|
219
|
+
progress?: number
|
|
220
|
+
error?: string
|
|
221
|
+
starting?: boolean
|
|
222
|
+
onStart: () => void
|
|
223
|
+
onApply: () => void
|
|
224
|
+
onRetry: () => void
|
|
225
|
+
}) {
|
|
226
|
+
switch (state) {
|
|
227
|
+
case 'idle':
|
|
228
|
+
return (
|
|
229
|
+
<button
|
|
230
|
+
onClick={onStart}
|
|
231
|
+
disabled={starting}
|
|
232
|
+
className="mt-2 px-3 py-1.5 btn-brand text-xs font-medium rounded"
|
|
233
|
+
>
|
|
234
|
+
{starting ? (
|
|
235
|
+
<span className="inline-flex items-center gap-1.5">
|
|
236
|
+
<Loader2 className="w-3 h-3 animate-spin" />
|
|
237
|
+
Starting...
|
|
238
|
+
</span>
|
|
239
|
+
) : (
|
|
240
|
+
'Update Now'
|
|
241
|
+
)}
|
|
242
|
+
</button>
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
case 'downloading':
|
|
246
|
+
return (
|
|
247
|
+
<div className="mt-2 space-y-1">
|
|
248
|
+
<div className="w-full bg-theme-elevated rounded-full h-1.5 overflow-hidden">
|
|
249
|
+
<div
|
|
250
|
+
className="bg-blue-500 h-full rounded-full transition-all duration-300"
|
|
251
|
+
style={{ width: `${Math.round((progress ?? 0) * 100)}%` }}
|
|
252
|
+
/>
|
|
253
|
+
</div>
|
|
254
|
+
<p className="text-xs text-theme-text-tertiary">
|
|
255
|
+
Downloading... {Math.round((progress ?? 0) * 100)}%
|
|
256
|
+
</p>
|
|
257
|
+
</div>
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
case 'ready':
|
|
261
|
+
return (
|
|
262
|
+
<div className="mt-2 flex gap-2">
|
|
263
|
+
<button
|
|
264
|
+
onClick={onApply}
|
|
265
|
+
className="px-3 py-1.5 bg-green-600 hover:bg-green-500 text-white text-xs font-medium rounded transition-colors"
|
|
266
|
+
>
|
|
267
|
+
Restart Now
|
|
268
|
+
</button>
|
|
269
|
+
</div>
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
case 'applying':
|
|
273
|
+
return (
|
|
274
|
+
<div className="mt-2 flex items-center gap-2">
|
|
275
|
+
<Loader2 className="w-3.5 h-3.5 text-blue-400 animate-spin" />
|
|
276
|
+
<p className="text-xs text-theme-text-secondary">Applying update...</p>
|
|
277
|
+
</div>
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
case 'error':
|
|
281
|
+
return (
|
|
282
|
+
<div className="mt-2 space-y-1.5">
|
|
283
|
+
{!starting && <p className="text-xs text-red-400">{error || 'Update failed'}</p>}
|
|
284
|
+
<button
|
|
285
|
+
onClick={onRetry}
|
|
286
|
+
disabled={starting}
|
|
287
|
+
className="inline-flex items-center gap-1 px-3 py-1.5 bg-theme-elevated hover:bg-theme-surface-hover text-xs font-medium text-theme-text-primary rounded transition-colors disabled:opacity-50"
|
|
288
|
+
>
|
|
289
|
+
{starting ? (
|
|
290
|
+
<Loader2 className="w-3 h-3 animate-spin" />
|
|
291
|
+
) : (
|
|
292
|
+
<RotateCw className="w-3 h-3" />
|
|
293
|
+
)}
|
|
294
|
+
{starting ? 'Starting...' : 'Retry'}
|
|
295
|
+
</button>
|
|
296
|
+
</div>
|
|
297
|
+
)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { YamlEditor, YamlDiffEditor } from '@skyhook-io/k8s-ui'
|