@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,460 @@
|
|
|
1
|
+
import { useState, useMemo, useRef, useEffect, useCallback } from 'react'
|
|
2
|
+
import { TRANSITION_BACKDROP, TRANSITION_PANEL } from '../../utils/animation'
|
|
3
|
+
import { Search, X, ChevronRight } from 'lucide-react'
|
|
4
|
+
import { Home, Network, List, Clock, Package, Activity, Sun, Stethoscope, DollarSign, ShieldCheck } from 'lucide-react'
|
|
5
|
+
import { clsx } from 'clsx'
|
|
6
|
+
import { useNamespaces, useContexts } from '../../api/client'
|
|
7
|
+
import { CORE_RESOURCES, useAPIResources } from '../../api/apiResources'
|
|
8
|
+
import { getResourceIcon } from '../../utils/resource-icons'
|
|
9
|
+
type MainView = 'home' | 'topology' | 'resources' | 'timeline' | 'helm' | 'traffic' | 'cost' | 'audit'
|
|
10
|
+
|
|
11
|
+
interface CommandPaletteProps {
|
|
12
|
+
onClose: () => void
|
|
13
|
+
onNavigateView: (view: MainView) => void
|
|
14
|
+
onNavigateKind: (kind: string, group: string) => void
|
|
15
|
+
onSwitchContext: (name: string) => void
|
|
16
|
+
onSetNamespaces: (ns: string[]) => void
|
|
17
|
+
onToggleTheme: () => void
|
|
18
|
+
onShowDiagnostics?: () => void
|
|
19
|
+
/** Controls fade-in/out animation (driven by useAnimatedUnmount) */
|
|
20
|
+
isOpen?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface CommandItem {
|
|
24
|
+
id: string
|
|
25
|
+
label: string
|
|
26
|
+
sublabel?: string
|
|
27
|
+
category: string
|
|
28
|
+
icon?: React.ComponentType<{ className?: string }>
|
|
29
|
+
shortcut?: string
|
|
30
|
+
action: () => void
|
|
31
|
+
/** Extra terms to match against during search (not displayed) */
|
|
32
|
+
searchTerms?: string[]
|
|
33
|
+
/** Small priority bonus added to the final score (only if the item matched). Used to nudge built-in k8s kinds above CRDs on tied queries like "policy" or "event". */
|
|
34
|
+
priorityBonus?: number
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Built-in k8s API groups. Used to nudge these above CRDs on tied matches.
|
|
38
|
+
const CORE_GROUP_BONUS = 10
|
|
39
|
+
const WELL_KNOWN_GROUP_BONUS = 5
|
|
40
|
+
const WELL_KNOWN_GROUPS = new Set([
|
|
41
|
+
'apps',
|
|
42
|
+
'batch',
|
|
43
|
+
'autoscaling',
|
|
44
|
+
'policy',
|
|
45
|
+
'networking.k8s.io',
|
|
46
|
+
'rbac.authorization.k8s.io',
|
|
47
|
+
'storage.k8s.io',
|
|
48
|
+
'scheduling.k8s.io',
|
|
49
|
+
'coordination.k8s.io',
|
|
50
|
+
'apiextensions.k8s.io',
|
|
51
|
+
'admissionregistration.k8s.io',
|
|
52
|
+
'apiregistration.k8s.io',
|
|
53
|
+
'certificates.k8s.io',
|
|
54
|
+
'events.k8s.io',
|
|
55
|
+
'discovery.k8s.io',
|
|
56
|
+
'flowcontrol.apiserver.k8s.io',
|
|
57
|
+
'node.k8s.io',
|
|
58
|
+
'authentication.k8s.io',
|
|
59
|
+
'authorization.k8s.io',
|
|
60
|
+
])
|
|
61
|
+
|
|
62
|
+
function groupPriorityBonus(group: string): number {
|
|
63
|
+
if (!group) return CORE_GROUP_BONUS
|
|
64
|
+
if (WELL_KNOWN_GROUPS.has(group)) return WELL_KNOWN_GROUP_BONUS
|
|
65
|
+
return 0
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Fuzzy match scoring: exact > prefix > word boundary > substring.
|
|
69
|
+
// Within a tier, a coverage bonus (up to +20) breaks ties in favor of
|
|
70
|
+
// shorter labels — so "serv" picks Service over ServiceAccount, and
|
|
71
|
+
// "service" picks Service (exact) decisively. Bonus is capped below the
|
|
72
|
+
// 25-point tier gap, so tier ordering is preserved.
|
|
73
|
+
function scoreMatch(text: string, query: string): number {
|
|
74
|
+
const lower = text.toLowerCase()
|
|
75
|
+
const q = query.toLowerCase()
|
|
76
|
+
if (!lower.includes(q)) return 0
|
|
77
|
+
let base: number
|
|
78
|
+
if (lower === q) base = 150
|
|
79
|
+
else if (lower.startsWith(q)) base = 100
|
|
80
|
+
else {
|
|
81
|
+
const wordStart = lower.indexOf(q)
|
|
82
|
+
const prev = lower[wordStart - 1]
|
|
83
|
+
base = wordStart > 0 && (prev === ' ' || prev === '/' || prev === '-' || prev === '.') ? 75 : 50
|
|
84
|
+
}
|
|
85
|
+
return base + (q.length / lower.length) * 20
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function bestScore(item: CommandItem, query: string): number {
|
|
89
|
+
// Primary label gets full score; secondary fields are discounted
|
|
90
|
+
// so that e.g. "node" matching the label "Node" ranks above
|
|
91
|
+
// "UpdateInfo" where "node" only matches the group "nodemanagement.gke.io"
|
|
92
|
+
let best = scoreMatch(item.label, query)
|
|
93
|
+
const secondary = Math.floor(Math.max(
|
|
94
|
+
scoreMatch(item.sublabel || '', query),
|
|
95
|
+
scoreMatch(item.category, query)
|
|
96
|
+
) * 0.6)
|
|
97
|
+
best = Math.max(best, secondary)
|
|
98
|
+
if (item.searchTerms) {
|
|
99
|
+
for (const term of item.searchTerms) {
|
|
100
|
+
best = Math.max(best, scoreMatch(term, query))
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Only apply the priority bonus to items that actually matched, so we don't
|
|
104
|
+
// surface unrelated built-ins ahead of a relevant CRD.
|
|
105
|
+
return best > 0 ? best + (item.priorityBonus || 0) : 0
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function CommandPalette({
|
|
109
|
+
onClose,
|
|
110
|
+
onNavigateView,
|
|
111
|
+
onNavigateKind,
|
|
112
|
+
onSwitchContext,
|
|
113
|
+
onSetNamespaces,
|
|
114
|
+
onToggleTheme,
|
|
115
|
+
onShowDiagnostics,
|
|
116
|
+
isOpen = true,
|
|
117
|
+
}: CommandPaletteProps) {
|
|
118
|
+
const [query, setQuery] = useState('')
|
|
119
|
+
const [selectedIndex, setSelectedIndex] = useState(0)
|
|
120
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
121
|
+
const resultsRef = useRef<HTMLDivElement>(null)
|
|
122
|
+
const isKeyboardNav = useRef(false)
|
|
123
|
+
|
|
124
|
+
const { data: namespacesData } = useNamespaces()
|
|
125
|
+
const { data: contexts } = useContexts()
|
|
126
|
+
const { data: apiResources } = useAPIResources()
|
|
127
|
+
|
|
128
|
+
// Focus input on mount
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
inputRef.current?.focus()
|
|
131
|
+
}, [])
|
|
132
|
+
|
|
133
|
+
// Close on Escape (capture phase to beat the shortcut system)
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
const handler = (e: KeyboardEvent) => {
|
|
136
|
+
if (e.key === 'Escape') {
|
|
137
|
+
e.preventDefault()
|
|
138
|
+
e.stopPropagation()
|
|
139
|
+
onClose()
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
document.addEventListener('keydown', handler, true)
|
|
143
|
+
return () => document.removeEventListener('keydown', handler, true)
|
|
144
|
+
}, [onClose])
|
|
145
|
+
|
|
146
|
+
// Build command items
|
|
147
|
+
const items = useMemo<CommandItem[]>(() => {
|
|
148
|
+
const result: CommandItem[] = []
|
|
149
|
+
|
|
150
|
+
// Views
|
|
151
|
+
const viewEntries: { view: MainView; label: string; icon: React.ComponentType<{ className?: string }>; shortcut: string }[] = [
|
|
152
|
+
{ view: 'home', label: 'Home', icon: Home, shortcut: '1' },
|
|
153
|
+
{ view: 'topology', label: 'Topology', icon: Network, shortcut: '2' },
|
|
154
|
+
{ view: 'resources', label: 'Resources', icon: List, shortcut: '3' },
|
|
155
|
+
{ view: 'timeline', label: 'Timeline', icon: Clock, shortcut: '4' },
|
|
156
|
+
{ view: 'helm', label: 'Helm', icon: Package, shortcut: '5' },
|
|
157
|
+
{ view: 'traffic', label: 'Traffic', icon: Activity, shortcut: '6' },
|
|
158
|
+
{ view: 'cost', label: 'Cost', icon: DollarSign, shortcut: '7' },
|
|
159
|
+
{ view: 'audit', label: 'Audit', icon: ShieldCheck, shortcut: '8' },
|
|
160
|
+
]
|
|
161
|
+
for (const v of viewEntries) {
|
|
162
|
+
result.push({
|
|
163
|
+
id: `view-${v.view}`,
|
|
164
|
+
label: `Go to ${v.label}`,
|
|
165
|
+
category: 'Views',
|
|
166
|
+
icon: v.icon,
|
|
167
|
+
shortcut: v.shortcut,
|
|
168
|
+
action: () => { onNavigateView(v.view) },
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Resource kinds (deduplicate by name+group — backend may return multiple API versions)
|
|
173
|
+
const resources = apiResources || CORE_RESOURCES
|
|
174
|
+
const seenKinds = new Set<string>()
|
|
175
|
+
for (const r of resources) {
|
|
176
|
+
if (!r.verbs?.includes('list')) continue
|
|
177
|
+
const kindKey = `${r.name}/${r.group}`
|
|
178
|
+
if (seenKinds.has(kindKey)) continue
|
|
179
|
+
seenKinds.add(kindKey)
|
|
180
|
+
result.push({
|
|
181
|
+
id: `kind-${r.name}-${r.group}`,
|
|
182
|
+
label: r.kind,
|
|
183
|
+
sublabel: r.group || 'core',
|
|
184
|
+
category: 'Resource Kinds',
|
|
185
|
+
icon: getResourceIcon(r.kind),
|
|
186
|
+
action: () => { onNavigateKind(r.name, r.group) },
|
|
187
|
+
searchTerms: [r.name, r.kind],
|
|
188
|
+
priorityBonus: groupPriorityBonus(r.group),
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Contexts
|
|
193
|
+
if (contexts) {
|
|
194
|
+
for (const ctx of contexts) {
|
|
195
|
+
result.push({
|
|
196
|
+
id: `context-${ctx.name}`,
|
|
197
|
+
label: ctx.name,
|
|
198
|
+
sublabel: ctx.isCurrent ? 'current' : ctx.cluster,
|
|
199
|
+
category: 'Contexts',
|
|
200
|
+
action: () => { if (!ctx.isCurrent) onSwitchContext(ctx.name) },
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Namespaces
|
|
206
|
+
if (namespacesData) {
|
|
207
|
+
for (const ns of namespacesData) {
|
|
208
|
+
result.push({
|
|
209
|
+
id: `ns-${ns.name}`,
|
|
210
|
+
label: ns.name,
|
|
211
|
+
category: 'Namespaces',
|
|
212
|
+
action: () => { onSetNamespaces([ns.name]) },
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
// "All namespaces" option
|
|
216
|
+
result.push({
|
|
217
|
+
id: 'ns-all',
|
|
218
|
+
label: 'All Namespaces',
|
|
219
|
+
category: 'Namespaces',
|
|
220
|
+
action: () => { onSetNamespaces([]) },
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Actions
|
|
225
|
+
result.push({
|
|
226
|
+
id: 'action-theme',
|
|
227
|
+
label: 'Toggle Theme',
|
|
228
|
+
category: 'Actions',
|
|
229
|
+
icon: Sun,
|
|
230
|
+
shortcut: 't',
|
|
231
|
+
action: () => { onToggleTheme() },
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
if (onShowDiagnostics) {
|
|
235
|
+
result.push({
|
|
236
|
+
id: 'action-diagnostics',
|
|
237
|
+
label: 'Diagnostics',
|
|
238
|
+
category: 'Actions',
|
|
239
|
+
icon: Stethoscope,
|
|
240
|
+
shortcut: 'Ctrl+Shift+D',
|
|
241
|
+
action: () => { onShowDiagnostics() },
|
|
242
|
+
searchTerms: ['debug', 'health', 'status', 'snapshot'],
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return result
|
|
247
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
248
|
+
}, [apiResources, contexts, namespacesData, onNavigateView, onNavigateKind, onSwitchContext, onSetNamespaces, onToggleTheme, onShowDiagnostics])
|
|
249
|
+
|
|
250
|
+
// Filter and rank results
|
|
251
|
+
const filteredItems = useMemo(() => {
|
|
252
|
+
if (!query.trim()) {
|
|
253
|
+
// Show views and actions when no query
|
|
254
|
+
return items.filter(i => i.category === 'Views' || i.category === 'Actions')
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return items
|
|
258
|
+
.map(item => {
|
|
259
|
+
let score = bestScore(item, query)
|
|
260
|
+
// Boost core K8s resources over CRDs at the same match quality
|
|
261
|
+
if (score > 0 && item.category === 'Resource Kinds' && item.sublabel === 'core') {
|
|
262
|
+
score += 10
|
|
263
|
+
}
|
|
264
|
+
return { item, score }
|
|
265
|
+
})
|
|
266
|
+
.filter(({ score }) => score > 0)
|
|
267
|
+
.sort((a, b) => b.score - a.score)
|
|
268
|
+
.slice(0, 20)
|
|
269
|
+
.map(({ item }) => item)
|
|
270
|
+
}, [items, query])
|
|
271
|
+
|
|
272
|
+
// Group filtered items by category
|
|
273
|
+
const grouped = useMemo(() => {
|
|
274
|
+
const groups = new Map<string, CommandItem[]>()
|
|
275
|
+
for (const item of filteredItems) {
|
|
276
|
+
const list = groups.get(item.category) || []
|
|
277
|
+
list.push(item)
|
|
278
|
+
groups.set(item.category, list)
|
|
279
|
+
}
|
|
280
|
+
return groups
|
|
281
|
+
}, [filteredItems])
|
|
282
|
+
|
|
283
|
+
// Single source of truth for flat ordering — derived from grouped to match render order
|
|
284
|
+
const flatItems = useMemo(() => {
|
|
285
|
+
const result: CommandItem[] = []
|
|
286
|
+
for (const [, categoryItems] of grouped) {
|
|
287
|
+
result.push(...categoryItems)
|
|
288
|
+
}
|
|
289
|
+
return result
|
|
290
|
+
}, [grouped])
|
|
291
|
+
|
|
292
|
+
// Reset selection when results change
|
|
293
|
+
useEffect(() => {
|
|
294
|
+
setSelectedIndex(0)
|
|
295
|
+
}, [flatItems.length, query])
|
|
296
|
+
|
|
297
|
+
// Scroll selected into view
|
|
298
|
+
useEffect(() => {
|
|
299
|
+
if (resultsRef.current && selectedIndex >= 0) {
|
|
300
|
+
const el = resultsRef.current.querySelector('[data-selected="true"]') as HTMLElement
|
|
301
|
+
el?.scrollIntoView({ block: 'nearest' })
|
|
302
|
+
}
|
|
303
|
+
}, [selectedIndex])
|
|
304
|
+
|
|
305
|
+
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
|
306
|
+
switch (e.key) {
|
|
307
|
+
case 'ArrowDown':
|
|
308
|
+
e.preventDefault()
|
|
309
|
+
isKeyboardNav.current = true
|
|
310
|
+
setSelectedIndex(i => Math.min(i + 1, flatItems.length - 1))
|
|
311
|
+
break
|
|
312
|
+
case 'ArrowUp':
|
|
313
|
+
e.preventDefault()
|
|
314
|
+
isKeyboardNav.current = true
|
|
315
|
+
setSelectedIndex(i => Math.max(i - 1, 0))
|
|
316
|
+
break
|
|
317
|
+
case 'Enter':
|
|
318
|
+
e.preventDefault()
|
|
319
|
+
if (flatItems[selectedIndex]) {
|
|
320
|
+
flatItems[selectedIndex].action()
|
|
321
|
+
onClose()
|
|
322
|
+
}
|
|
323
|
+
break
|
|
324
|
+
}
|
|
325
|
+
}, [flatItems, selectedIndex, onClose])
|
|
326
|
+
|
|
327
|
+
// Flatten for index tracking
|
|
328
|
+
let flatIndex = 0
|
|
329
|
+
|
|
330
|
+
return (
|
|
331
|
+
<div className="fixed inset-0 z-[100] flex items-start justify-center pt-[15vh]">
|
|
332
|
+
{/* Backdrop */}
|
|
333
|
+
<div
|
|
334
|
+
className={clsx(
|
|
335
|
+
'absolute inset-0 bg-theme-base/60 backdrop-blur-sm',
|
|
336
|
+
TRANSITION_BACKDROP,
|
|
337
|
+
isOpen ? 'opacity-100' : 'opacity-0'
|
|
338
|
+
)}
|
|
339
|
+
onClick={onClose}
|
|
340
|
+
/>
|
|
341
|
+
|
|
342
|
+
{/* Panel */}
|
|
343
|
+
<div className={clsx(
|
|
344
|
+
'relative w-full max-w-lg mx-4 dialog overflow-hidden',
|
|
345
|
+
TRANSITION_PANEL,
|
|
346
|
+
isOpen ? 'opacity-100 scale-100 translate-y-0' : 'opacity-0 scale-[0.97] translate-y-3'
|
|
347
|
+
)}>
|
|
348
|
+
{/* Search input */}
|
|
349
|
+
<div className="flex items-center gap-3 px-4 py-3 border-b border-theme-border">
|
|
350
|
+
<Search className="w-5 h-5 text-theme-text-secondary shrink-0" />
|
|
351
|
+
<input
|
|
352
|
+
ref={inputRef}
|
|
353
|
+
type="text"
|
|
354
|
+
value={query}
|
|
355
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
356
|
+
onKeyDown={handleKeyDown}
|
|
357
|
+
placeholder="Type a command or search..."
|
|
358
|
+
className="flex-1 bg-transparent text-theme-text-primary placeholder-theme-text-disabled outline-none text-sm"
|
|
359
|
+
autoFocus
|
|
360
|
+
/>
|
|
361
|
+
{query && (
|
|
362
|
+
<button
|
|
363
|
+
onClick={() => setQuery('')}
|
|
364
|
+
className="p-1 text-theme-text-secondary hover:text-theme-text-primary"
|
|
365
|
+
>
|
|
366
|
+
<X className="w-4 h-4" />
|
|
367
|
+
</button>
|
|
368
|
+
)}
|
|
369
|
+
<kbd className="px-1.5 py-0.5 text-xs text-theme-text-tertiary bg-theme-elevated rounded border border-theme-border-light">
|
|
370
|
+
ESC
|
|
371
|
+
</kbd>
|
|
372
|
+
</div>
|
|
373
|
+
|
|
374
|
+
{/* Results */}
|
|
375
|
+
<div ref={resultsRef} className="max-h-[50vh] overflow-y-auto">
|
|
376
|
+
{query && filteredItems.length === 0 && (
|
|
377
|
+
<div className="px-4 py-8 text-center text-theme-text-tertiary">
|
|
378
|
+
<Search className="w-8 h-8 mx-auto mb-2 opacity-50" />
|
|
379
|
+
<p>No results for "{query}"</p>
|
|
380
|
+
</div>
|
|
381
|
+
)}
|
|
382
|
+
|
|
383
|
+
{Array.from(grouped.entries()).map(([category, categoryItems]) => (
|
|
384
|
+
<div key={category}>
|
|
385
|
+
<div className="px-4 py-1.5 text-[10px] font-semibold text-theme-text-tertiary uppercase tracking-wider bg-theme-surface/50 sticky top-0">
|
|
386
|
+
{category}
|
|
387
|
+
</div>
|
|
388
|
+
{categoryItems.map((item) => {
|
|
389
|
+
const thisIndex = flatIndex++
|
|
390
|
+
const isSelected = thisIndex === selectedIndex
|
|
391
|
+
const Icon = item.icon
|
|
392
|
+
|
|
393
|
+
return (
|
|
394
|
+
<button
|
|
395
|
+
key={item.id}
|
|
396
|
+
data-selected={isSelected}
|
|
397
|
+
onClick={() => { item.action(); onClose() }}
|
|
398
|
+
onMouseMove={() => {
|
|
399
|
+
if (isKeyboardNav.current) { isKeyboardNav.current = false; return }
|
|
400
|
+
setSelectedIndex(thisIndex)
|
|
401
|
+
}}
|
|
402
|
+
className={clsx(
|
|
403
|
+
'w-full flex items-center gap-3 px-4 py-2.5 text-left transition-colors',
|
|
404
|
+
isSelected ? 'selection' : 'hover:bg-theme-elevated/30'
|
|
405
|
+
)}
|
|
406
|
+
>
|
|
407
|
+
{Icon && (
|
|
408
|
+
<div className={clsx(
|
|
409
|
+
'flex items-center justify-center w-7 h-7 rounded-md shrink-0',
|
|
410
|
+
isSelected ? 'selection-strong' : 'bg-theme-elevated/50'
|
|
411
|
+
)}>
|
|
412
|
+
<Icon className="w-3.5 h-3.5 text-theme-text-secondary" />
|
|
413
|
+
</div>
|
|
414
|
+
)}
|
|
415
|
+
{!Icon && <div className="w-7 h-7 shrink-0" />}
|
|
416
|
+
|
|
417
|
+
<div className="flex-1 min-w-0">
|
|
418
|
+
<span className="text-sm text-theme-text-primary truncate block">{item.label}</span>
|
|
419
|
+
{item.sublabel && (
|
|
420
|
+
<span className="text-xs text-theme-text-tertiary truncate block">{item.sublabel}</span>
|
|
421
|
+
)}
|
|
422
|
+
</div>
|
|
423
|
+
|
|
424
|
+
{item.shortcut && (
|
|
425
|
+
<kbd className="px-1.5 py-0.5 text-xs text-theme-text-tertiary bg-theme-elevated rounded border border-theme-border-light shrink-0">
|
|
426
|
+
{item.shortcut}
|
|
427
|
+
</kbd>
|
|
428
|
+
)}
|
|
429
|
+
|
|
430
|
+
<ChevronRight className={clsx(
|
|
431
|
+
'w-3.5 h-3.5 shrink-0 transition-opacity',
|
|
432
|
+
isSelected ? 'text-theme-text-secondary opacity-100' : 'opacity-0'
|
|
433
|
+
)} />
|
|
434
|
+
</button>
|
|
435
|
+
)
|
|
436
|
+
})}
|
|
437
|
+
</div>
|
|
438
|
+
))}
|
|
439
|
+
</div>
|
|
440
|
+
|
|
441
|
+
{/* Footer hints */}
|
|
442
|
+
{filteredItems.length > 0 && (
|
|
443
|
+
<div className="px-4 py-2 border-t border-theme-border bg-theme-surface/50">
|
|
444
|
+
<div className="flex items-center gap-4 text-xs text-theme-text-tertiary">
|
|
445
|
+
<span className="flex items-center gap-1">
|
|
446
|
+
<kbd className="px-1 py-0.5 bg-theme-elevated rounded">↑</kbd>
|
|
447
|
+
<kbd className="px-1 py-0.5 bg-theme-elevated rounded">↓</kbd>
|
|
448
|
+
Navigate
|
|
449
|
+
</span>
|
|
450
|
+
<span className="flex items-center gap-1">
|
|
451
|
+
<kbd className="px-1.5 py-0.5 bg-theme-elevated rounded">↵</kbd>
|
|
452
|
+
Select
|
|
453
|
+
</span>
|
|
454
|
+
</div>
|
|
455
|
+
</div>
|
|
456
|
+
)}
|
|
457
|
+
</div>
|
|
458
|
+
</div>
|
|
459
|
+
)
|
|
460
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ConfirmDialog } from '@skyhook-io/k8s-ui/components/ui/ConfirmDialog'
|