@skyhook-io/radar-app 0.2.2 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1
- package/package.json +33 -25
- package/src/App.tsx +1449 -382
- package/src/RadarApp.tsx +132 -19
- package/src/api/apiResources.ts +1 -1
- package/src/api/client.argoResourceSync.test.ts +69 -0
- package/src/api/client.delta.test.ts +89 -0
- package/src/api/client.deltaSync.test.ts +216 -0
- package/src/api/client.metrics.test.ts +106 -0
- package/src/api/client.rightsizing.test.ts +32 -0
- package/src/api/client.ts +2730 -271
- package/src/api/client.yaml.test.ts +45 -0
- package/src/api/diagnose.ts +289 -0
- package/src/api/quotas.ts +16 -0
- package/src/api/rbac.ts +57 -0
- package/src/api/timelineSource.test.ts +217 -0
- package/src/api/timelineSource.ts +582 -0
- package/src/components/ConnectionErrorView.tsx +186 -70
- package/src/components/ContextSwitcher.tsx +63 -18
- package/src/components/DebugOverlay.tsx +5 -3
- package/src/components/NamespaceSwitcher.tsx +41 -0
- package/src/components/UserMenu.tsx +69 -21
- package/src/components/applications/ApplicationsView.tsx +936 -0
- package/src/components/audit/AuditSettingsDialog.tsx +79 -17
- package/src/components/audit/AuditView.tsx +65 -62
- package/src/components/compare/CompareViewRoute.tsx +124 -0
- package/src/components/compare/useCompareCandidates.ts +27 -0
- package/src/components/compare/useCompareLauncher.tsx +79 -0
- package/src/components/cost/ApplicationCostTab.test.ts +204 -0
- package/src/components/cost/ApplicationCostTab.tsx +571 -0
- package/src/components/cost/CostTrendChart.tsx +106 -75
- package/src/components/cost/CostView.test.ts +12 -0
- package/src/components/cost/CostView.tsx +507 -223
- package/src/components/cost/CostViewTabs.test.tsx +21 -0
- package/src/components/cost/CostViewTabs.tsx +40 -0
- package/src/components/cost/CurrentAllocationUse.test.ts +21 -0
- package/src/components/cost/CurrentAllocationUse.tsx +126 -0
- package/src/components/cost/WorkloadCostTab.test.ts +153 -0
- package/src/components/cost/WorkloadCostTab.tsx +372 -0
- package/src/components/cost/cloud-console.test.ts +39 -0
- package/src/components/cost/cloud-console.ts +81 -0
- package/src/components/cost/errors.ts +8 -0
- package/src/components/cost/format.test.ts +27 -0
- package/src/components/cost/format.ts +46 -0
- package/src/components/cost/kinds.ts +5 -0
- package/src/components/curl/ServiceCurlButton.tsx +445 -0
- package/src/components/diagnose/AISettings.tsx +147 -0
- package/src/components/diagnose/DiagnoseContext.tsx +495 -0
- package/src/components/diagnose/DiagnoseSurface.tsx +394 -0
- package/src/components/diagnose/Home.tsx +163 -0
- package/src/components/diagnose/InvestigationView.tsx +622 -0
- package/src/components/diagnose/LocalDiagnoseAction.tsx +162 -0
- package/src/components/diagnose/launch.ts +65 -0
- package/src/components/diagnose/parts.tsx +1756 -0
- package/src/components/dock/BottomDock.tsx +2 -3
- package/src/components/dock/DockContext.tsx +1 -0
- package/src/components/dock/TerminalTab.tsx +1 -1
- package/src/components/dock/WorkloadLogsTab.tsx +21 -5
- package/src/components/dock/index.ts +1 -1
- package/src/components/execution/BatchExecutionView.test.ts +170 -0
- package/src/components/execution/BatchExecutionView.tsx +1329 -0
- package/src/components/execution/batch-run-actions.test.ts +48 -0
- package/src/components/execution/batch-run-actions.ts +24 -0
- package/src/components/execution/batch-timeline.test.ts +57 -0
- package/src/components/execution/batch-timeline.ts +46 -0
- package/src/components/execution/execution-definition.test.ts +208 -0
- package/src/components/execution/execution-definition.ts +245 -0
- package/src/components/gitops/ArgoResourceDiffLoader.tsx +23 -0
- package/src/components/gitops/GitOpsView.tsx +1042 -0
- package/src/components/gitops/RevisionMetaChip.tsx +63 -0
- package/src/components/helm/ChartBrowser.tsx +87 -31
- package/src/components/helm/HelmCompareRoute.tsx +1341 -0
- package/src/components/helm/HelmReleaseDrawer.test.ts +17 -0
- package/src/components/helm/HelmReleaseDrawer.tsx +1073 -102
- package/src/components/helm/HelmView.tsx +237 -96
- package/src/components/helm/InstallWizard.tsx +94 -38
- package/src/components/helm/ManifestDiffViewer.tsx +8 -27
- package/src/components/helm/OwnedResources.tsx +34 -59
- package/src/components/helm/RevisionHistory.tsx +52 -3
- package/src/components/helm/RoleGatedPanel.tsx +3 -3
- package/src/components/helm/TrackChartSourceDialog.tsx +185 -0
- package/src/components/helm/ValuesDiffPreview.tsx +17 -7
- package/src/components/helm/ValuesViewer.tsx +49 -53
- package/src/components/helm/helm-utils.ts +4 -0
- package/src/components/home/ActivitySummary.tsx +4 -1
- package/src/components/home/ClusterHealthCard.tsx +56 -42
- package/src/components/home/CostCard.tsx +21 -36
- package/src/components/home/GitOpsControllersCard.tsx +110 -0
- package/src/components/home/HelmSummary.tsx +3 -1
- package/src/components/home/HomeView.tsx +339 -105
- package/src/components/home/MCPSetupDialog.tsx +29 -87
- package/src/components/home/TrafficSummary.tsx +2 -2
- package/src/components/home/mcpToolCatalog.ts +333 -0
- package/src/components/issues/IssuesPane.tsx +151 -0
- package/src/components/logs/LogsViewer.tsx +4 -1
- package/src/components/logs/ScheduledWorkloadLogsViewer.tsx +135 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +4 -1
- package/src/components/nav/PrimaryNavRail.tsx +285 -0
- package/src/components/portforward/PortForwardButton.tsx +118 -47
- package/src/components/portforward/PortForwardManager.tsx +253 -131
- package/src/components/resource/HPACharts.tsx +237 -0
- package/src/components/resource/PVCUsageBar.tsx +59 -0
- package/src/components/resource/PrometheusCharts.tsx +160 -584
- package/src/components/resource/PrometheusChartsGrid.tsx +270 -0
- package/src/components/resource/RestartChart.tsx +133 -0
- package/src/components/resource/RightsizingStrip.test.ts +109 -0
- package/src/components/resource/RightsizingStrip.tsx +363 -0
- package/src/components/resource-drawer/ResourceDrawer.tsx +3 -1
- package/src/components/resources/CompositeRenderer.tsx +101 -0
- package/src/components/resources/ImageFilesystemModal.tsx +19 -12
- package/src/components/resources/PodFilesystemModal.tsx +6 -5
- package/src/components/resources/ResourceDetailDrawer.tsx +13 -3
- package/src/components/resources/ResourcesView.tsx +194 -17
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +1 -0
- package/src/components/resources/renderers/HPARenderer.tsx +20 -1
- package/src/components/resources/renderers/NamespaceRenderer.tsx +31 -0
- package/src/components/resources/renderers/NodeRenderer.tsx +10 -4
- package/src/components/resources/renderers/PVCRenderer.tsx +19 -1
- package/src/components/resources/renderers/PodRenderer.tsx +30 -6
- package/src/components/resources/renderers/RoleBindingRenderer.tsx +45 -1
- package/src/components/resources/renderers/RoleRenderer.tsx +27 -1
- package/src/components/resources/renderers/ServiceAccountRenderer.tsx +28 -1
- package/src/components/resources/renderers/ServiceRenderer.tsx +81 -8
- package/src/components/resources/renderers/WorkloadRenderer.tsx +51 -4
- package/src/components/resources/renderers/index.ts +2 -0
- package/src/components/resources/resource-utils.ts +2 -1
- package/src/components/rightsizing/RightsizingScanView.tsx +938 -0
- package/src/components/rightsizing/copy.test.ts +56 -0
- package/src/components/rightsizing/model.test.ts +227 -0
- package/src/components/rightsizing/model.ts +158 -0
- package/src/components/rightsizing/presentation.test.ts +104 -0
- package/src/components/rightsizing/presentation.ts +94 -0
- package/src/components/settings/MyPermissionsDialog.tsx +241 -0
- package/src/components/settings/SettingsDialog.tsx +1505 -165
- package/src/components/shared/CreateResourceDialog.tsx +9 -2
- package/src/components/shared/LargeClusterNamespacePicker.tsx +3 -3
- package/src/components/timeline/LocalTimelineScrubber.tsx +212 -0
- package/src/components/timeline/RetainedTimelineScrubber.tsx +311 -0
- package/src/components/timeline/TimelineList.tsx +86 -13
- package/src/components/timeline/TimelineSwimlanes.tsx +9 -1299
- package/src/components/timeline/TimelineView.tsx +873 -24
- package/src/components/timeline/TimelineView.urlparams.test.ts +335 -0
- package/src/components/traffic/TrafficFilterSidebar.tsx +10 -45
- package/src/components/traffic/TrafficFlowList.tsx +29 -15
- package/src/components/traffic/TrafficGraph.tsx +42 -24
- package/src/components/traffic/TrafficView.tsx +32 -19
- package/src/components/ui/CommandPalette.tsx +8 -215
- package/src/components/ui/DiagnosticsOverlay.tsx +219 -9
- package/src/components/ui/Markdown.tsx +3 -3
- package/src/components/ui/Omnibar.tsx +602 -0
- package/src/components/ui/RadarOmnibar.tsx +52 -0
- package/src/components/ui/SearchSyntaxHelp.tsx +89 -0
- package/src/components/ui/ShortcutHelpOverlay.tsx +3 -2
- package/src/components/ui/UpdateNotification.tsx +48 -36
- package/src/components/ui/command-items.ts +178 -0
- package/src/components/workload/WorkloadView.tsx +1342 -158
- package/src/context/ConnectionContext.tsx +146 -21
- package/src/context/DiagnoseCustomization.tsx +93 -0
- package/src/context/NavCustomization.tsx +75 -0
- package/src/context/TimelineSource.tsx +50 -0
- package/src/contexts/CapabilitiesContext.tsx +32 -8
- package/src/filter/FilterLocationBridge.tsx +30 -0
- package/src/hooks/useClusterLoadState.ts +73 -0
- package/src/hooks/useDocumentTitle.ts +25 -0
- package/src/hooks/useEventSource.ts +6 -0
- package/src/hooks/useKeyboardShortcuts.tsx +1 -0
- package/src/hooks/useMediaQuery.ts +21 -0
- package/src/hooks/useNavRailPinned.ts +46 -0
- package/src/hooks/useRecentResources.ts +49 -0
- package/src/index.css +162 -1
- package/src/index.ts +73 -1
- package/src/main.tsx +7 -5
- package/src/types/clusterLoadState.ts +33 -0
- package/src/types.ts +2 -0
- package/src/utils/auditBadges.ts +53 -0
- package/src/utils/navigation.ts +64 -1
- package/src/components/ui/NamespaceSelector.tsx +0 -436
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { useState, useRef, useEffect, useCallback } from 'react'
|
|
2
|
+
import { createPortal } from 'react-dom'
|
|
3
|
+
import { HelpCircle } from 'lucide-react'
|
|
4
|
+
import { clsx } from 'clsx'
|
|
5
|
+
|
|
6
|
+
// The query operators recognized by the search engine (internal/search/parse.go).
|
|
7
|
+
const OPERATORS: { syntax: string; desc: string }[] = [
|
|
8
|
+
{ syntax: 'ns:prod', desc: 'filter by namespace' },
|
|
9
|
+
{ syntax: 'kind:Deployment', desc: 'filter by resource kind' },
|
|
10
|
+
{ syntax: 'label:app=api', desc: 'filter by label' },
|
|
11
|
+
{ syntax: 'image:nginx', desc: 'match container image' },
|
|
12
|
+
{ syntax: 'redis cache', desc: 'multiple terms — all must match' },
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
// A "?" affordance that reveals the search query syntax on HOVER (a transient
|
|
16
|
+
// reference tooltip, not a second click-to-open panel stacked over the results).
|
|
17
|
+
// Self-contained + portaled to <body> so the header's stacking context can't
|
|
18
|
+
// trap it; a short close grace lets the pointer travel onto the card.
|
|
19
|
+
export function SearchSyntaxHelp() {
|
|
20
|
+
const [open, setOpen] = useState(false)
|
|
21
|
+
const btnRef = useRef<HTMLButtonElement>(null)
|
|
22
|
+
const popRef = useRef<HTMLDivElement>(null)
|
|
23
|
+
const closeTimer = useRef<ReturnType<typeof setTimeout>>(undefined)
|
|
24
|
+
const [anchor, setAnchor] = useState<{ right: number; top: number } | null>(null)
|
|
25
|
+
|
|
26
|
+
const show = useCallback(() => { clearTimeout(closeTimer.current); setOpen(true) }, [])
|
|
27
|
+
const scheduleHide = useCallback(() => {
|
|
28
|
+
clearTimeout(closeTimer.current)
|
|
29
|
+
closeTimer.current = setTimeout(() => setOpen(false), 120)
|
|
30
|
+
}, [])
|
|
31
|
+
|
|
32
|
+
useEffect(() => () => clearTimeout(closeTimer.current), [])
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (!open) { setAnchor(null); return }
|
|
36
|
+
const update = () => {
|
|
37
|
+
const el = btnRef.current
|
|
38
|
+
if (el) { const r = el.getBoundingClientRect(); setAnchor({ right: window.innerWidth - r.right, top: r.bottom + 6 }) }
|
|
39
|
+
}
|
|
40
|
+
update()
|
|
41
|
+
window.addEventListener('resize', update)
|
|
42
|
+
window.addEventListener('scroll', update, true)
|
|
43
|
+
return () => {
|
|
44
|
+
window.removeEventListener('resize', update)
|
|
45
|
+
window.removeEventListener('scroll', update, true)
|
|
46
|
+
}
|
|
47
|
+
}, [open])
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<>
|
|
51
|
+
<button
|
|
52
|
+
ref={btnRef}
|
|
53
|
+
type="button"
|
|
54
|
+
tabIndex={-1}
|
|
55
|
+
aria-label="Search syntax help"
|
|
56
|
+
onMouseEnter={show}
|
|
57
|
+
onMouseLeave={scheduleHide}
|
|
58
|
+
onMouseDown={(e) => e.preventDefault()}
|
|
59
|
+
className={clsx('shrink-0 rounded p-0.5 cursor-help transition-colors', open ? 'text-theme-text-secondary' : 'text-theme-text-tertiary hover:text-theme-text-secondary')}
|
|
60
|
+
>
|
|
61
|
+
<HelpCircle className="w-3.5 h-3.5" />
|
|
62
|
+
</button>
|
|
63
|
+
{open && anchor && createPortal(
|
|
64
|
+
<div
|
|
65
|
+
ref={popRef}
|
|
66
|
+
onMouseEnter={show}
|
|
67
|
+
onMouseLeave={scheduleHide}
|
|
68
|
+
onMouseDown={(e) => e.stopPropagation()}
|
|
69
|
+
style={{ position: 'fixed', top: anchor.top, right: anchor.right, width: 280 }}
|
|
70
|
+
className="z-[130] dialog shadow-theme-lg p-3"
|
|
71
|
+
>
|
|
72
|
+
<div className="text-[10px] font-semibold uppercase tracking-wider text-theme-text-tertiary mb-2">Search syntax</div>
|
|
73
|
+
<div className="space-y-1.5">
|
|
74
|
+
{OPERATORS.map((op) => (
|
|
75
|
+
<div key={op.syntax} className="flex items-baseline gap-2">
|
|
76
|
+
<code className="shrink-0 rounded bg-theme-elevated border border-theme-border-light px-1.5 py-0.5 text-xs text-theme-text-primary">{op.syntax}</code>
|
|
77
|
+
<span className="text-xs text-theme-text-secondary">{op.desc}</span>
|
|
78
|
+
</div>
|
|
79
|
+
))}
|
|
80
|
+
</div>
|
|
81
|
+
<div className="mt-2.5 pt-2 border-t border-theme-border text-[11px] text-theme-text-tertiary">
|
|
82
|
+
Type <code className="text-theme-text-secondary">ns:</code> or <code className="text-theme-text-secondary">kind:</code> for value suggestions.
|
|
83
|
+
</div>
|
|
84
|
+
</div>,
|
|
85
|
+
document.body,
|
|
86
|
+
)}
|
|
87
|
+
</>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
@@ -19,7 +19,7 @@ const CONTEXT_CATEGORIES: ShortcutCategory[] = ['Drawer', 'Dock']
|
|
|
19
19
|
|
|
20
20
|
// Preferred ordering within the view section
|
|
21
21
|
const VIEW_CATEGORY_ORDER: ShortcutCategory[] = [
|
|
22
|
-
'Search', 'Table', 'Resource Actions', 'Topology', 'Timeline', 'Helm',
|
|
22
|
+
'Search', 'Table', 'Resource Actions', 'Topology', 'Timeline', 'Helm', 'GitOps',
|
|
23
23
|
]
|
|
24
24
|
|
|
25
25
|
const VIEW_LABELS: Record<string, string> = {
|
|
@@ -28,7 +28,8 @@ const VIEW_LABELS: Record<string, string> = {
|
|
|
28
28
|
resources: 'Resources',
|
|
29
29
|
timeline: 'Timeline',
|
|
30
30
|
helm: 'Helm',
|
|
31
|
-
|
|
31
|
+
gitops: 'GitOps',
|
|
32
|
+
traffic: 'Live Traffic',
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
type ShortcutEntry = { description: string; keys: string[] }
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
useApplyDesktopUpdate,
|
|
9
9
|
} from '../../api/client'
|
|
10
10
|
import type { DesktopUpdateState } from '../../api/client'
|
|
11
|
+
import { WithTooltip } from './Tooltip'
|
|
11
12
|
|
|
12
13
|
const DISMISSED_KEY = 'radar-update-dismissed'
|
|
13
14
|
|
|
@@ -26,21 +27,16 @@ export function UpdateNotification() {
|
|
|
26
27
|
|
|
27
28
|
const isDesktop = versionInfo?.installMethod === 'desktop'
|
|
28
29
|
|
|
29
|
-
// Listen for "Check for Updates" menu item in desktop app
|
|
30
|
-
// Un-dismisses the notification and invalidates the version check cache.
|
|
30
|
+
// Listen for "Check for Updates" menu item in desktop app.
|
|
31
31
|
useEffect(() => {
|
|
32
|
-
const
|
|
33
|
-
| { EventsOn?: (event: string, callback: () => void) => () => void }
|
|
34
|
-
| undefined
|
|
35
|
-
if (!wailsRuntime?.EventsOn) return
|
|
36
|
-
|
|
37
|
-
const cleanup = wailsRuntime.EventsOn('check-for-updates', () => {
|
|
32
|
+
const handler = () => {
|
|
38
33
|
setDismissed(false)
|
|
39
34
|
try { localStorage.removeItem(DISMISSED_KEY) } catch { /* ignore */ }
|
|
40
35
|
queryClient.invalidateQueries({ queryKey: ['version-check'] })
|
|
41
|
-
}
|
|
36
|
+
}
|
|
42
37
|
|
|
43
|
-
|
|
38
|
+
window.addEventListener('radar:check-for-updates', handler)
|
|
39
|
+
return () => window.removeEventListener('radar:check-for-updates', handler)
|
|
44
40
|
}, [queryClient])
|
|
45
41
|
|
|
46
42
|
// Log version check errors for debugging
|
|
@@ -111,13 +107,13 @@ export function UpdateNotification() {
|
|
|
111
107
|
const effectiveState: DesktopUpdateState = updateStatus?.state ?? 'idle'
|
|
112
108
|
|
|
113
109
|
return (
|
|
114
|
-
<div className="fixed bottom-4 right-4 z-50 max-w-sm bg-theme-surface border border-
|
|
110
|
+
<div className="fixed bottom-4 right-4 z-50 max-w-sm bg-theme-surface border border-accent/50 rounded-lg shadow-xl p-4 animate-in slide-in-from-right">
|
|
115
111
|
<div className="flex items-start gap-3">
|
|
116
|
-
<div className="flex items-center justify-center w-8 h-8 bg-
|
|
112
|
+
<div className="flex items-center justify-center w-8 h-8 bg-accent-muted rounded-full shrink-0">
|
|
117
113
|
<UpdateIcon state={effectiveState} />
|
|
118
114
|
</div>
|
|
119
115
|
<div className="flex-1 min-w-0">
|
|
120
|
-
<h4 className="text-sm font-medium text-theme-text-primary">
|
|
116
|
+
<h4 className="text-sm font-medium text-theme-text-primary pr-6">
|
|
121
117
|
<UpdateTitle state={effectiveState} />
|
|
122
118
|
</h4>
|
|
123
119
|
<p className="text-xs text-theme-text-secondary mt-1">
|
|
@@ -140,38 +136,54 @@ export function UpdateNotification() {
|
|
|
140
136
|
|
|
141
137
|
{/* CLI: show update command with copy button for package managers */}
|
|
142
138
|
{!isDesktop && versionInfo.updateCommand ? (
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
139
|
+
<>
|
|
140
|
+
<WithTooltip tip={versionInfo.updateCommand} delay={100}>
|
|
141
|
+
<button
|
|
142
|
+
onClick={handleCopyCommand}
|
|
143
|
+
className="flex items-center gap-2 mt-2 px-2 py-1.5 bg-theme-elevated rounded font-mono text-theme-text-primary hover:bg-theme-surface-hover transition-colors w-full"
|
|
144
|
+
>
|
|
145
|
+
<code className="inline-code flex-1 truncate text-left text-[11px]">{versionInfo.updateCommand}</code>
|
|
146
|
+
<CopyIcon copied={copied} failed={copyFailed} />
|
|
147
|
+
</button>
|
|
148
|
+
</WithTooltip>
|
|
149
|
+
{/* Direct installs may have placed the binary somewhere the install
|
|
150
|
+
script won't touch — surface a download link as a fallback. */}
|
|
151
|
+
{versionInfo.installMethod === 'direct' && versionInfo.releaseUrl && (
|
|
152
|
+
<a
|
|
153
|
+
href={versionInfo.releaseUrl}
|
|
154
|
+
target="_blank"
|
|
155
|
+
rel="noopener noreferrer"
|
|
156
|
+
className="inline-flex items-center gap-1 mt-1.5 text-xs text-theme-text-tertiary hover:text-theme-text-secondary hover:underline"
|
|
157
|
+
>
|
|
158
|
+
or download from GitHub →
|
|
159
|
+
</a>
|
|
160
|
+
)}
|
|
161
|
+
</>
|
|
150
162
|
) : (
|
|
151
|
-
/* Direct download - show release link */
|
|
152
163
|
!isDesktop && versionInfo.releaseUrl && (
|
|
153
164
|
<a
|
|
154
165
|
href={versionInfo.releaseUrl}
|
|
155
166
|
target="_blank"
|
|
156
167
|
rel="noopener noreferrer"
|
|
157
|
-
className="inline-flex items-center gap-1 mt-2 text-xs font-medium text-
|
|
168
|
+
className="inline-flex items-center gap-1 mt-2 text-xs font-medium text-accent-text hover:underline"
|
|
158
169
|
>
|
|
159
170
|
Download from GitHub →
|
|
160
171
|
</a>
|
|
161
172
|
)
|
|
162
173
|
)}
|
|
163
174
|
</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
175
|
</div>
|
|
176
|
+
{/* Dismiss is absolute so it doesn't compress the chip's width.
|
|
177
|
+
fixed on the parent already establishes the positioning context. */}
|
|
178
|
+
{effectiveState !== 'downloading' && effectiveState !== 'applying' && (
|
|
179
|
+
<button
|
|
180
|
+
onClick={handleDismiss}
|
|
181
|
+
className="absolute top-2 right-2 p-1 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded"
|
|
182
|
+
aria-label="Dismiss"
|
|
183
|
+
>
|
|
184
|
+
<X className="w-4 h-4" />
|
|
185
|
+
</button>
|
|
186
|
+
)}
|
|
175
187
|
</div>
|
|
176
188
|
)
|
|
177
189
|
}
|
|
@@ -186,11 +198,11 @@ function UpdateIcon({ state }: { state: DesktopUpdateState }) {
|
|
|
186
198
|
switch (state) {
|
|
187
199
|
case 'downloading':
|
|
188
200
|
case 'applying':
|
|
189
|
-
return <Loader2 className="w-4 h-4 text-
|
|
201
|
+
return <Loader2 className="w-4 h-4 text-accent animate-spin" />
|
|
190
202
|
case 'ready':
|
|
191
203
|
return <ArrowDownToLine className="w-4 h-4 text-green-400" />
|
|
192
204
|
default:
|
|
193
|
-
return <Download className="w-4 h-4 text-
|
|
205
|
+
return <Download className="w-4 h-4 text-accent" />
|
|
194
206
|
}
|
|
195
207
|
}
|
|
196
208
|
|
|
@@ -247,7 +259,7 @@ function DesktopUpdateControls({
|
|
|
247
259
|
<div className="mt-2 space-y-1">
|
|
248
260
|
<div className="w-full bg-theme-elevated rounded-full h-1.5 overflow-hidden">
|
|
249
261
|
<div
|
|
250
|
-
className="bg-
|
|
262
|
+
className="bg-accent h-full rounded-full transition-all duration-300"
|
|
251
263
|
style={{ width: `${Math.round((progress ?? 0) * 100)}%` }}
|
|
252
264
|
/>
|
|
253
265
|
</div>
|
|
@@ -272,7 +284,7 @@ function DesktopUpdateControls({
|
|
|
272
284
|
case 'applying':
|
|
273
285
|
return (
|
|
274
286
|
<div className="mt-2 flex items-center gap-2">
|
|
275
|
-
<Loader2 className="w-3.5 h-3.5 text-
|
|
287
|
+
<Loader2 className="w-3.5 h-3.5 text-accent animate-spin" />
|
|
276
288
|
<p className="text-xs text-theme-text-secondary">Applying update...</p>
|
|
277
289
|
</div>
|
|
278
290
|
)
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import { Home, Network, List, Clock, Package, Activity, Sun, Stethoscope, DollarSign, ShieldCheck, GitBranch, AlertTriangle, Boxes, Server } from 'lucide-react'
|
|
3
|
+
import { useNamespaces, useContexts } from '../../api/client'
|
|
4
|
+
import { CORE_RESOURCES, useAPIResources } from '../../api/apiResources'
|
|
5
|
+
import { getResourceIcon } from '../../utils/resource-icons'
|
|
6
|
+
import { parseContextName } from '../../utils/context-name'
|
|
7
|
+
|
|
8
|
+
// Drop the disambiguating " (source)" suffix the context list appends, so the
|
|
9
|
+
// GKE/EKS/AKS parser sees the bare context name (mirrors the cluster picker).
|
|
10
|
+
function stripSourceSuffix(name: string, source?: string): string {
|
|
11
|
+
if (!source) return name
|
|
12
|
+
const escaped = source.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
13
|
+
return name.replace(new RegExp(`\\s+\\(${escaped}(?:\\s+#\\d+)?\\)$`), '')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type MainView = 'home' | 'topology' | 'resources' | 'timeline' | 'issues' | 'helm' | 'traffic' | 'cost' | 'checks' | 'gitops' | 'applications'
|
|
17
|
+
|
|
18
|
+
export interface CommandItem {
|
|
19
|
+
id: string
|
|
20
|
+
label: string
|
|
21
|
+
sublabel?: string
|
|
22
|
+
category: string
|
|
23
|
+
icon?: React.ComponentType<{ className?: string }>
|
|
24
|
+
shortcut?: string
|
|
25
|
+
action: () => void
|
|
26
|
+
/** Extra terms to match against during search (not displayed). */
|
|
27
|
+
searchTerms?: string[]
|
|
28
|
+
/** Small priority bonus added to the final score (only if the item matched). */
|
|
29
|
+
priorityBonus?: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Built-in k8s API groups. Used to nudge these above CRDs on tied matches.
|
|
33
|
+
const CORE_GROUP_BONUS = 10
|
|
34
|
+
const WELL_KNOWN_GROUP_BONUS = 5
|
|
35
|
+
const WELL_KNOWN_GROUPS = new Set([
|
|
36
|
+
'apps', 'batch', 'autoscaling', 'policy', 'networking.k8s.io', 'rbac.authorization.k8s.io',
|
|
37
|
+
'storage.k8s.io', 'scheduling.k8s.io', 'coordination.k8s.io', 'apiextensions.k8s.io',
|
|
38
|
+
'admissionregistration.k8s.io', 'apiregistration.k8s.io', 'certificates.k8s.io',
|
|
39
|
+
'events.k8s.io', 'discovery.k8s.io', 'flowcontrol.apiserver.k8s.io', 'node.k8s.io',
|
|
40
|
+
'authentication.k8s.io', 'authorization.k8s.io',
|
|
41
|
+
])
|
|
42
|
+
|
|
43
|
+
function groupPriorityBonus(group: string): number {
|
|
44
|
+
if (!group) return CORE_GROUP_BONUS
|
|
45
|
+
if (WELL_KNOWN_GROUPS.has(group)) return WELL_KNOWN_GROUP_BONUS
|
|
46
|
+
return 0
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Fuzzy match scoring: exact > prefix > word boundary > substring. Within a
|
|
50
|
+
// tier, a coverage bonus (up to +20) breaks ties in favor of shorter labels.
|
|
51
|
+
export function scoreMatch(text: string, query: string): number {
|
|
52
|
+
const lower = text.toLowerCase()
|
|
53
|
+
const q = query.toLowerCase()
|
|
54
|
+
if (!lower.includes(q)) return 0
|
|
55
|
+
let base: number
|
|
56
|
+
if (lower === q) base = 150
|
|
57
|
+
else if (lower.startsWith(q)) base = 100
|
|
58
|
+
else {
|
|
59
|
+
const wordStart = lower.indexOf(q)
|
|
60
|
+
const prev = lower[wordStart - 1]
|
|
61
|
+
base = wordStart > 0 && (prev === ' ' || prev === '/' || prev === '-' || prev === '.') ? 75 : 50
|
|
62
|
+
}
|
|
63
|
+
return base + (q.length / lower.length) * 20
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function bestScore(item: CommandItem, query: string): number {
|
|
67
|
+
let best = scoreMatch(item.label, query)
|
|
68
|
+
const secondary = Math.floor(Math.max(scoreMatch(item.sublabel || '', query), scoreMatch(item.category, query)) * 0.6)
|
|
69
|
+
best = Math.max(best, secondary)
|
|
70
|
+
if (item.searchTerms) {
|
|
71
|
+
for (const term of item.searchTerms) best = Math.max(best, scoreMatch(term, query))
|
|
72
|
+
}
|
|
73
|
+
return best > 0 ? best + (item.priorityBonus || 0) : 0
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface CommandItemCallbacks {
|
|
77
|
+
onNavigateView: (view: MainView) => void
|
|
78
|
+
onNavigateKind: (kind: string, group: string) => void
|
|
79
|
+
onSwitchContext: (name: string) => void
|
|
80
|
+
onSetNamespaces: (ns: string[]) => void
|
|
81
|
+
onToggleTheme: () => void
|
|
82
|
+
onShowDiagnostics?: () => void
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const VIEW_ENTRIES: { view: MainView; label: string; icon: React.ComponentType<{ className?: string }>; shortcut: string }[] = [
|
|
86
|
+
{ view: 'home', label: 'Home', icon: Home, shortcut: 'g h' },
|
|
87
|
+
{ view: 'resources', label: 'Resources', icon: List, shortcut: 'g r' },
|
|
88
|
+
{ view: 'issues', label: 'Issues', icon: AlertTriangle, shortcut: 'g i' },
|
|
89
|
+
{ view: 'topology', label: 'Topology', icon: Network, shortcut: 'g t' },
|
|
90
|
+
{ view: 'applications', label: 'Applications', icon: Boxes, shortcut: 'g a' },
|
|
91
|
+
{ view: 'timeline', label: 'Timeline', icon: Clock, shortcut: 'g l' },
|
|
92
|
+
{ view: 'helm', label: 'Helm', icon: Package, shortcut: 'g m' },
|
|
93
|
+
{ view: 'gitops', label: 'GitOps', icon: GitBranch, shortcut: 'g o' },
|
|
94
|
+
{ view: 'traffic', label: 'Live Traffic', icon: Activity, shortcut: 'g f' },
|
|
95
|
+
{ view: 'checks', label: 'Checks', icon: ShieldCheck, shortcut: 'g u' },
|
|
96
|
+
{ view: 'cost', label: 'Cost', icon: DollarSign, shortcut: 'g c' },
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
// The static command-palette items (Views, Resource Kinds, Contexts,
|
|
100
|
+
// Namespaces, Actions) — shared by the centered modal (embedded) and the
|
|
101
|
+
// standalone omnibar so the two never drift.
|
|
102
|
+
export function useCommandItems(cb: CommandItemCallbacks): CommandItem[] {
|
|
103
|
+
const { data: namespacesData } = useNamespaces()
|
|
104
|
+
const { data: contexts } = useContexts()
|
|
105
|
+
const { data: apiResources } = useAPIResources()
|
|
106
|
+
|
|
107
|
+
return useMemo<CommandItem[]>(() => {
|
|
108
|
+
const result: CommandItem[] = []
|
|
109
|
+
|
|
110
|
+
for (const v of VIEW_ENTRIES) {
|
|
111
|
+
result.push({ id: `view-${v.view}`, label: `Go to ${v.label}`, category: 'Views', icon: v.icon, shortcut: v.shortcut, action: () => cb.onNavigateView(v.view) })
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const resources = apiResources || CORE_RESOURCES
|
|
115
|
+
const seenKinds = new Set<string>()
|
|
116
|
+
for (const r of resources) {
|
|
117
|
+
if (!r.verbs?.includes('list')) continue
|
|
118
|
+
const kindKey = `${r.name}/${r.group}`
|
|
119
|
+
if (seenKinds.has(kindKey)) continue
|
|
120
|
+
seenKinds.add(kindKey)
|
|
121
|
+
result.push({
|
|
122
|
+
// Group shown only when it disambiguates (CRDs) — "core" is noise on
|
|
123
|
+
// built-in kinds. priorityBonus still nudges core/well-known above CRDs.
|
|
124
|
+
id: `kind-${r.name}-${r.group}`, label: r.kind, sublabel: r.group || undefined, category: 'Resource Kinds',
|
|
125
|
+
icon: getResourceIcon(r.kind), action: () => cb.onNavigateKind(r.name, r.group),
|
|
126
|
+
searchTerms: [r.name, r.kind], priorityBonus: groupPriorityBonus(r.group),
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (contexts) {
|
|
131
|
+
// Show the friendly parsed cluster name (like the cluster picker), not the
|
|
132
|
+
// raw ARN/gke context string. provider/region may live in the cluster id
|
|
133
|
+
// (e.g. EKS ARN) when the context name is already friendly — fall back to
|
|
134
|
+
// it. Count display names so genuine duplicates (same cluster name from
|
|
135
|
+
// different kubeconfig sources) stay distinguishable; unique ones stay clean.
|
|
136
|
+
const parsedCtx = contexts.map((ctx) => {
|
|
137
|
+
const parsed = parseContextName(stripSourceSuffix(ctx.name, ctx.source))
|
|
138
|
+
const fromCluster = ctx.cluster ? parseContextName(ctx.cluster) : null
|
|
139
|
+
const meta = [parsed.provider ?? fromCluster?.provider, parsed.region ?? fromCluster?.region].filter(Boolean).join(' · ')
|
|
140
|
+
return { ctx, clusterName: parsed.clusterName, account: parsed.account, base: ctx.isCurrent ? 'current' : meta }
|
|
141
|
+
})
|
|
142
|
+
// Disambiguate on the FINAL visible (label, sublabel) pair, not just the
|
|
143
|
+
// cluster name — same name + same provider/region from the same kubeconfig
|
|
144
|
+
// file would otherwise render identically while switching different
|
|
145
|
+
// contexts. Collisions fall back to the raw context name (unique by id).
|
|
146
|
+
const pairCount = new Map<string, number>()
|
|
147
|
+
for (const p of parsedCtx) pairCount.set(`${p.clusterName}\x00${p.base}`, (pairCount.get(`${p.clusterName}\x00${p.base}`) ?? 0) + 1)
|
|
148
|
+
for (const { ctx, clusterName, account, base } of parsedCtx) {
|
|
149
|
+
const collides = (pairCount.get(`${clusterName}\x00${base}`) ?? 0) > 1
|
|
150
|
+
const sub = [base, collides ? ctx.name : ''].filter(Boolean).join(' · ')
|
|
151
|
+
result.push({
|
|
152
|
+
id: `context-${ctx.name}`,
|
|
153
|
+
label: clusterName,
|
|
154
|
+
sublabel: sub || undefined,
|
|
155
|
+
category: 'Clusters',
|
|
156
|
+
icon: Server,
|
|
157
|
+
action: () => { if (!ctx.isCurrent) cb.onSwitchContext(ctx.name) },
|
|
158
|
+
searchTerms: [ctx.name, account || ''].filter(Boolean),
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (namespacesData) {
|
|
164
|
+
for (const ns of namespacesData) {
|
|
165
|
+
result.push({ id: `ns-${ns.name}`, label: ns.name, category: 'Namespaces', action: () => cb.onSetNamespaces([ns.name]) })
|
|
166
|
+
}
|
|
167
|
+
result.push({ id: 'ns-all', label: 'All Namespaces', category: 'Namespaces', action: () => cb.onSetNamespaces([]) })
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
result.push({ id: 'action-theme', label: 'Toggle Theme', category: 'Actions', icon: Sun, shortcut: 't', action: () => cb.onToggleTheme() })
|
|
171
|
+
if (cb.onShowDiagnostics) {
|
|
172
|
+
result.push({ id: 'action-diagnostics', label: 'Diagnostics', category: 'Actions', icon: Stethoscope, action: () => cb.onShowDiagnostics?.(), searchTerms: ['debug', 'health', 'status', 'snapshot'] })
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return result
|
|
176
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
177
|
+
}, [apiResources, contexts, namespacesData, cb.onNavigateView, cb.onNavigateKind, cb.onSwitchContext, cb.onSetNamespaces, cb.onToggleTheme, cb.onShowDiagnostics])
|
|
178
|
+
}
|