@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.
Files changed (177) hide show
  1. package/README.md +7 -1
  2. package/package.json +33 -25
  3. package/src/App.tsx +1449 -382
  4. package/src/RadarApp.tsx +132 -19
  5. package/src/api/apiResources.ts +1 -1
  6. package/src/api/client.argoResourceSync.test.ts +69 -0
  7. package/src/api/client.delta.test.ts +89 -0
  8. package/src/api/client.deltaSync.test.ts +216 -0
  9. package/src/api/client.metrics.test.ts +106 -0
  10. package/src/api/client.rightsizing.test.ts +32 -0
  11. package/src/api/client.ts +2730 -271
  12. package/src/api/client.yaml.test.ts +45 -0
  13. package/src/api/diagnose.ts +289 -0
  14. package/src/api/quotas.ts +16 -0
  15. package/src/api/rbac.ts +57 -0
  16. package/src/api/timelineSource.test.ts +217 -0
  17. package/src/api/timelineSource.ts +582 -0
  18. package/src/components/ConnectionErrorView.tsx +186 -70
  19. package/src/components/ContextSwitcher.tsx +63 -18
  20. package/src/components/DebugOverlay.tsx +5 -3
  21. package/src/components/NamespaceSwitcher.tsx +41 -0
  22. package/src/components/UserMenu.tsx +69 -21
  23. package/src/components/applications/ApplicationsView.tsx +936 -0
  24. package/src/components/audit/AuditSettingsDialog.tsx +79 -17
  25. package/src/components/audit/AuditView.tsx +65 -62
  26. package/src/components/compare/CompareViewRoute.tsx +124 -0
  27. package/src/components/compare/useCompareCandidates.ts +27 -0
  28. package/src/components/compare/useCompareLauncher.tsx +79 -0
  29. package/src/components/cost/ApplicationCostTab.test.ts +204 -0
  30. package/src/components/cost/ApplicationCostTab.tsx +571 -0
  31. package/src/components/cost/CostTrendChart.tsx +106 -75
  32. package/src/components/cost/CostView.test.ts +12 -0
  33. package/src/components/cost/CostView.tsx +507 -223
  34. package/src/components/cost/CostViewTabs.test.tsx +21 -0
  35. package/src/components/cost/CostViewTabs.tsx +40 -0
  36. package/src/components/cost/CurrentAllocationUse.test.ts +21 -0
  37. package/src/components/cost/CurrentAllocationUse.tsx +126 -0
  38. package/src/components/cost/WorkloadCostTab.test.ts +153 -0
  39. package/src/components/cost/WorkloadCostTab.tsx +372 -0
  40. package/src/components/cost/cloud-console.test.ts +39 -0
  41. package/src/components/cost/cloud-console.ts +81 -0
  42. package/src/components/cost/errors.ts +8 -0
  43. package/src/components/cost/format.test.ts +27 -0
  44. package/src/components/cost/format.ts +46 -0
  45. package/src/components/cost/kinds.ts +5 -0
  46. package/src/components/curl/ServiceCurlButton.tsx +445 -0
  47. package/src/components/diagnose/AISettings.tsx +147 -0
  48. package/src/components/diagnose/DiagnoseContext.tsx +495 -0
  49. package/src/components/diagnose/DiagnoseSurface.tsx +394 -0
  50. package/src/components/diagnose/Home.tsx +163 -0
  51. package/src/components/diagnose/InvestigationView.tsx +622 -0
  52. package/src/components/diagnose/LocalDiagnoseAction.tsx +162 -0
  53. package/src/components/diagnose/launch.ts +65 -0
  54. package/src/components/diagnose/parts.tsx +1756 -0
  55. package/src/components/dock/BottomDock.tsx +2 -3
  56. package/src/components/dock/DockContext.tsx +1 -0
  57. package/src/components/dock/TerminalTab.tsx +1 -1
  58. package/src/components/dock/WorkloadLogsTab.tsx +21 -5
  59. package/src/components/dock/index.ts +1 -1
  60. package/src/components/execution/BatchExecutionView.test.ts +170 -0
  61. package/src/components/execution/BatchExecutionView.tsx +1329 -0
  62. package/src/components/execution/batch-run-actions.test.ts +48 -0
  63. package/src/components/execution/batch-run-actions.ts +24 -0
  64. package/src/components/execution/batch-timeline.test.ts +57 -0
  65. package/src/components/execution/batch-timeline.ts +46 -0
  66. package/src/components/execution/execution-definition.test.ts +208 -0
  67. package/src/components/execution/execution-definition.ts +245 -0
  68. package/src/components/gitops/ArgoResourceDiffLoader.tsx +23 -0
  69. package/src/components/gitops/GitOpsView.tsx +1042 -0
  70. package/src/components/gitops/RevisionMetaChip.tsx +63 -0
  71. package/src/components/helm/ChartBrowser.tsx +87 -31
  72. package/src/components/helm/HelmCompareRoute.tsx +1341 -0
  73. package/src/components/helm/HelmReleaseDrawer.test.ts +17 -0
  74. package/src/components/helm/HelmReleaseDrawer.tsx +1073 -102
  75. package/src/components/helm/HelmView.tsx +237 -96
  76. package/src/components/helm/InstallWizard.tsx +94 -38
  77. package/src/components/helm/ManifestDiffViewer.tsx +8 -27
  78. package/src/components/helm/OwnedResources.tsx +34 -59
  79. package/src/components/helm/RevisionHistory.tsx +52 -3
  80. package/src/components/helm/RoleGatedPanel.tsx +3 -3
  81. package/src/components/helm/TrackChartSourceDialog.tsx +185 -0
  82. package/src/components/helm/ValuesDiffPreview.tsx +17 -7
  83. package/src/components/helm/ValuesViewer.tsx +49 -53
  84. package/src/components/helm/helm-utils.ts +4 -0
  85. package/src/components/home/ActivitySummary.tsx +4 -1
  86. package/src/components/home/ClusterHealthCard.tsx +56 -42
  87. package/src/components/home/CostCard.tsx +21 -36
  88. package/src/components/home/GitOpsControllersCard.tsx +110 -0
  89. package/src/components/home/HelmSummary.tsx +3 -1
  90. package/src/components/home/HomeView.tsx +339 -105
  91. package/src/components/home/MCPSetupDialog.tsx +29 -87
  92. package/src/components/home/TrafficSummary.tsx +2 -2
  93. package/src/components/home/mcpToolCatalog.ts +333 -0
  94. package/src/components/issues/IssuesPane.tsx +151 -0
  95. package/src/components/logs/LogsViewer.tsx +4 -1
  96. package/src/components/logs/ScheduledWorkloadLogsViewer.tsx +135 -0
  97. package/src/components/logs/WorkloadLogsViewer.tsx +4 -1
  98. package/src/components/nav/PrimaryNavRail.tsx +285 -0
  99. package/src/components/portforward/PortForwardButton.tsx +118 -47
  100. package/src/components/portforward/PortForwardManager.tsx +253 -131
  101. package/src/components/resource/HPACharts.tsx +237 -0
  102. package/src/components/resource/PVCUsageBar.tsx +59 -0
  103. package/src/components/resource/PrometheusCharts.tsx +160 -584
  104. package/src/components/resource/PrometheusChartsGrid.tsx +270 -0
  105. package/src/components/resource/RestartChart.tsx +133 -0
  106. package/src/components/resource/RightsizingStrip.test.ts +109 -0
  107. package/src/components/resource/RightsizingStrip.tsx +363 -0
  108. package/src/components/resource-drawer/ResourceDrawer.tsx +3 -1
  109. package/src/components/resources/CompositeRenderer.tsx +101 -0
  110. package/src/components/resources/ImageFilesystemModal.tsx +19 -12
  111. package/src/components/resources/PodFilesystemModal.tsx +6 -5
  112. package/src/components/resources/ResourceDetailDrawer.tsx +13 -3
  113. package/src/components/resources/ResourcesView.tsx +194 -17
  114. package/src/components/resources/renderers/CronWorkflowRenderer.tsx +1 -0
  115. package/src/components/resources/renderers/HPARenderer.tsx +20 -1
  116. package/src/components/resources/renderers/NamespaceRenderer.tsx +31 -0
  117. package/src/components/resources/renderers/NodeRenderer.tsx +10 -4
  118. package/src/components/resources/renderers/PVCRenderer.tsx +19 -1
  119. package/src/components/resources/renderers/PodRenderer.tsx +30 -6
  120. package/src/components/resources/renderers/RoleBindingRenderer.tsx +45 -1
  121. package/src/components/resources/renderers/RoleRenderer.tsx +27 -1
  122. package/src/components/resources/renderers/ServiceAccountRenderer.tsx +28 -1
  123. package/src/components/resources/renderers/ServiceRenderer.tsx +81 -8
  124. package/src/components/resources/renderers/WorkloadRenderer.tsx +51 -4
  125. package/src/components/resources/renderers/index.ts +2 -0
  126. package/src/components/resources/resource-utils.ts +2 -1
  127. package/src/components/rightsizing/RightsizingScanView.tsx +938 -0
  128. package/src/components/rightsizing/copy.test.ts +56 -0
  129. package/src/components/rightsizing/model.test.ts +227 -0
  130. package/src/components/rightsizing/model.ts +158 -0
  131. package/src/components/rightsizing/presentation.test.ts +104 -0
  132. package/src/components/rightsizing/presentation.ts +94 -0
  133. package/src/components/settings/MyPermissionsDialog.tsx +241 -0
  134. package/src/components/settings/SettingsDialog.tsx +1505 -165
  135. package/src/components/shared/CreateResourceDialog.tsx +9 -2
  136. package/src/components/shared/LargeClusterNamespacePicker.tsx +3 -3
  137. package/src/components/timeline/LocalTimelineScrubber.tsx +212 -0
  138. package/src/components/timeline/RetainedTimelineScrubber.tsx +311 -0
  139. package/src/components/timeline/TimelineList.tsx +86 -13
  140. package/src/components/timeline/TimelineSwimlanes.tsx +9 -1299
  141. package/src/components/timeline/TimelineView.tsx +873 -24
  142. package/src/components/timeline/TimelineView.urlparams.test.ts +335 -0
  143. package/src/components/traffic/TrafficFilterSidebar.tsx +10 -45
  144. package/src/components/traffic/TrafficFlowList.tsx +29 -15
  145. package/src/components/traffic/TrafficGraph.tsx +42 -24
  146. package/src/components/traffic/TrafficView.tsx +32 -19
  147. package/src/components/ui/CommandPalette.tsx +8 -215
  148. package/src/components/ui/DiagnosticsOverlay.tsx +219 -9
  149. package/src/components/ui/Markdown.tsx +3 -3
  150. package/src/components/ui/Omnibar.tsx +602 -0
  151. package/src/components/ui/RadarOmnibar.tsx +52 -0
  152. package/src/components/ui/SearchSyntaxHelp.tsx +89 -0
  153. package/src/components/ui/ShortcutHelpOverlay.tsx +3 -2
  154. package/src/components/ui/UpdateNotification.tsx +48 -36
  155. package/src/components/ui/command-items.ts +178 -0
  156. package/src/components/workload/WorkloadView.tsx +1342 -158
  157. package/src/context/ConnectionContext.tsx +146 -21
  158. package/src/context/DiagnoseCustomization.tsx +93 -0
  159. package/src/context/NavCustomization.tsx +75 -0
  160. package/src/context/TimelineSource.tsx +50 -0
  161. package/src/contexts/CapabilitiesContext.tsx +32 -8
  162. package/src/filter/FilterLocationBridge.tsx +30 -0
  163. package/src/hooks/useClusterLoadState.ts +73 -0
  164. package/src/hooks/useDocumentTitle.ts +25 -0
  165. package/src/hooks/useEventSource.ts +6 -0
  166. package/src/hooks/useKeyboardShortcuts.tsx +1 -0
  167. package/src/hooks/useMediaQuery.ts +21 -0
  168. package/src/hooks/useNavRailPinned.ts +46 -0
  169. package/src/hooks/useRecentResources.ts +49 -0
  170. package/src/index.css +162 -1
  171. package/src/index.ts +73 -1
  172. package/src/main.tsx +7 -5
  173. package/src/types/clusterLoadState.ts +33 -0
  174. package/src/types.ts +2 -0
  175. package/src/utils/auditBadges.ts +53 -0
  176. package/src/utils/navigation.ts +64 -1
  177. package/src/components/ui/NamespaceSelector.tsx +0 -436
@@ -1,5 +1,4 @@
1
1
  import { useState, useEffect, useMemo, useRef, useCallback } from 'react'
2
- import { useRefreshAnimation } from '../../hooks/useRefreshAnimation'
3
2
  import { useTrafficSources, useTrafficFlows, useTrafficConnect, useSetTrafficSource } from '../../api/traffic'
4
3
  import { useClusterInfo } from '../../api/client'
5
4
  import type { TrafficWizardState, AggregatedFlow } from '../../types'
@@ -7,11 +6,13 @@ import { TrafficWizard } from './TrafficWizard'
7
6
  import { TrafficGraph, type TrafficGraphSelection } from './TrafficGraph'
8
7
  import { TrafficFilterSidebar } from './TrafficFilterSidebar'
9
8
  import { TrafficFlowListProvider } from './TrafficFlowListContext'
10
- import { Loader2, RefreshCw, Filter, Plug, ChevronDown, List, Activity, AlertTriangle } from 'lucide-react'
9
+ import { Loader2, Filter, Plug, ChevronDown, List, Activity, AlertTriangle } from 'lucide-react'
11
10
  import { clsx } from 'clsx'
12
11
  import { useQueryClient } from '@tanstack/react-query'
13
12
  import { useDock } from '../dock'
14
- import { EmptyState, PaneLoader } from '@skyhook-io/k8s-ui'
13
+ import { EmptyState, PaneLoader, FreshnessControl } from '@skyhook-io/k8s-ui'
14
+ import { useConnection } from '../../context/ConnectionContext'
15
+ import { Tooltip } from '../ui/Tooltip'
15
16
 
16
17
  // Addon types for filtering
17
18
  export type AddonMode = 'show' | 'group' | 'hide'
@@ -338,6 +339,7 @@ interface TrafficViewProps {
338
339
  }
339
340
 
340
341
  export function TrafficView({ namespaces }: TrafficViewProps) {
342
+ const { connection } = useConnection()
341
343
  const [wizardState, setWizardState] = useState<TrafficWizardState>('detecting')
342
344
  const [timeRange, setTimeRange] = useState<string>('5m')
343
345
  const [hideSystem, setHideSystem] = useState(true)
@@ -420,8 +422,8 @@ export function TrafficView({ namespaces }: TrafficViewProps) {
420
422
 
421
423
  const {
422
424
  data: flowsData,
423
- isLoading: flowsLoading,
424
425
  isFetching: flowsFetching,
426
+ dataUpdatedAt: flowsUpdatedAt,
425
427
  refetch: refetchFlowsRaw,
426
428
  } = useTrafficFlows({
427
429
  namespaces,
@@ -429,7 +431,6 @@ export function TrafficView({ namespaces }: TrafficViewProps) {
429
431
  // Only fetch flows when connected (not connecting and no connection error)
430
432
  enabled: wizardState === 'ready' && !isConnecting && !connectionError,
431
433
  })
432
- const [refetchFlows, isRefreshAnimating] = useRefreshAnimation(refetchFlowsRaw)
433
434
 
434
435
  // Auto-retry when flows return with warning but no data (e.g., port-forward not ready yet)
435
436
  useEffect(() => {
@@ -619,13 +620,13 @@ export function TrafficView({ namespaces }: TrafficViewProps) {
619
620
 
620
621
  // Toggle L7 filter helpers
621
622
  const toggleL7Method = useCallback((method: string) => {
622
- setL7Methods(prev => { const next = new Set(prev); next.has(method) ? next.delete(method) : next.add(method); return next })
623
+ setL7Methods(prev => { const next = new Set(prev); if (next.has(method)) next.delete(method); else next.add(method); return next })
623
624
  }, [])
624
625
  const toggleL7StatusRange = useCallback((range: string) => {
625
- setL7StatusRanges(prev => { const next = new Set(prev); next.has(range) ? next.delete(range) : next.add(range); return next })
626
+ setL7StatusRanges(prev => { const next = new Set(prev); if (next.has(range)) next.delete(range); else next.add(range); return next })
626
627
  }, [])
627
628
  const toggleL7Verdict = useCallback((verdict: string) => {
628
- setL7Verdicts(prev => { const next = new Set(prev); next.has(verdict) ? next.delete(verdict) : next.add(verdict); return next })
629
+ setL7Verdicts(prev => { const next = new Set(prev); if (next.has(verdict)) next.delete(verdict); else next.add(verdict); return next })
629
630
  }, [])
630
631
 
631
632
  // Toggle namespace visibility
@@ -957,7 +958,11 @@ export function TrafficView({ namespaces }: TrafficViewProps) {
957
958
  }
958
959
  }, [sourcesData, sourcesLoading])
959
960
 
960
- // Shared connection handler — used by auto-connect and retry buttons
961
+ // Shared connection handler — used by auto-connect and retry buttons.
962
+ // Deliberately does NOT reset hasAutoConnectedRef on failure: that ref gates
963
+ // the auto-connect effect, and re-arming it would re-fire auto-connect on
964
+ // every failed attempt (unbounded loop). Retries come from the explicit
965
+ // Retry button, which calls handleConnect directly.
961
966
  const handleConnect = useCallback(() => {
962
967
  setIsConnecting(true)
963
968
  setConnectionError(null)
@@ -968,18 +973,18 @@ export function TrafficView({ namespaces }: TrafficViewProps) {
968
973
  setIsConnecting(false)
969
974
  if (!data.connected && data.error) {
970
975
  setConnectionError(data.error)
971
- hasAutoConnectedRef.current = false // allow retry
972
976
  }
973
977
  },
974
978
  onError: (error) => {
975
979
  setIsConnecting(false)
976
980
  setConnectionError(error.message)
977
- hasAutoConnectedRef.current = false // allow retry
978
981
  },
979
982
  })
980
983
  }, [connectMutation, queryClient])
981
984
 
982
- // Auto-connect when source is detected
985
+ // Auto-connect once when a source is first detected. Strictly one-shot per
986
+ // mount / cluster (the ref resets on cluster change); failures surface a
987
+ // manual Retry rather than re-arming this effect.
983
988
  useEffect(() => {
984
989
  if (wizardState === 'ready' && !hasAutoConnectedRef.current && !isConnecting) {
985
990
  hasAutoConnectedRef.current = true
@@ -1117,18 +1122,26 @@ export function TrafficView({ namespaces }: TrafficViewProps) {
1117
1122
  {/* Top-right: stats + actions */}
1118
1123
  <div className="absolute top-3 right-3 z-10 flex items-center gap-2">
1119
1124
  {flowsData?.flows && flowsData.flows.length > 0 && (
1125
+ <Tooltip content="Open flow list in dock">
1120
1126
  <button onClick={openFlowListDock}
1121
- className="flex items-center gap-1 px-2 py-1 text-[10px] rounded-lg bg-theme-surface/90 backdrop-blur border border-theme-border text-theme-text-secondary hover:text-theme-text-primary transition-colors"
1122
- title="Open flow list in dock">
1127
+ className="flex items-center gap-1 px-2 py-1 text-[10px] rounded-lg bg-theme-surface/90 backdrop-blur border border-theme-border text-theme-text-secondary hover:text-theme-text-primary transition-colors">
1123
1128
  <List className="w-3 h-3" /> Flows
1124
1129
  </button>
1130
+ </Tooltip>
1125
1131
  )}
1126
- <div className="flex items-center gap-1.5 px-2 py-1 rounded-lg bg-theme-surface/90 backdrop-blur border border-theme-border text-[10px] text-theme-text-tertiary">
1132
+ <div className="flex items-center px-2 py-1 rounded-lg bg-theme-surface/90 backdrop-blur border border-theme-border text-[10px] text-theme-text-tertiary tabular-nums">
1127
1133
  {flowStats.shown}/{flowStats.total}
1128
- <button onClick={refetchFlows} disabled={flowsLoading || isRefreshAnimating}
1129
- className={clsx('p-0.5 rounded hover:text-theme-text-primary transition-colors', (flowsLoading || isRefreshAnimating) && 'opacity-50')}>
1130
- {flowsLoading ? <Loader2 className="h-3 w-3 animate-spin" /> : <RefreshCw className={clsx('h-3 w-3', isRefreshAnimating && 'animate-spin')} />}
1131
- </button>
1134
+ </div>
1135
+ {/* Flows are a REST snapshot (no poll, no stream), so this is
1136
+ an honest "Updated N ago" + manual refresh not "live". */}
1137
+ <div className="flex items-center rounded-lg bg-theme-surface/90 backdrop-blur border border-theme-border px-1.5 py-0.5">
1138
+ <FreshnessControl
1139
+ mode="snapshot"
1140
+ dataUpdatedAt={flowsUpdatedAt}
1141
+ isFetching={flowsFetching}
1142
+ onRefresh={() => refetchFlowsRaw()}
1143
+ connectionState={connection.state}
1144
+ />
1132
1145
  </div>
1133
1146
  </div>
1134
1147
  </>
@@ -1,129 +1,27 @@
1
1
  import { useState, useMemo, useRef, useEffect, useCallback } from 'react'
2
2
  import { TRANSITION_BACKDROP, TRANSITION_PANEL } from '../../utils/animation'
3
3
  import { Search, X, ChevronRight } from 'lucide-react'
4
- import { Home, Network, List, Clock, Package, Activity, Sun, Stethoscope, DollarSign, ShieldCheck } from 'lucide-react'
5
4
  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'
5
+ import { useCommandItems, bestScore, type CommandItem, type CommandItemCallbacks } from './command-items'
6
+ import { Input } from '@skyhook-io/k8s-ui'
10
7
 
11
- interface CommandPaletteProps {
8
+ interface CommandPaletteProps extends CommandItemCallbacks {
12
9
  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
10
  /** Controls fade-in/out animation (driven by useAnimatedUnmount) */
20
11
  isOpen?: boolean
21
12
  }
22
13
 
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
14
 
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) {
15
+ export function CommandPalette({ onClose, isOpen = true, ...callbacks }: CommandPaletteProps) {
118
16
  const [query, setQuery] = useState('')
119
17
  const [selectedIndex, setSelectedIndex] = useState(0)
120
18
  const inputRef = useRef<HTMLInputElement>(null)
121
19
  const resultsRef = useRef<HTMLDivElement>(null)
122
20
  const isKeyboardNav = useRef(false)
123
21
 
124
- const { data: namespacesData } = useNamespaces()
125
- const { data: contexts } = useContexts()
126
- const { data: apiResources } = useAPIResources()
22
+ // The static command items (Views / Kinds / Namespaces / Actions) — shared
23
+ // with the standalone omnibar via useCommandItems so the two never drift.
24
+ const items = useCommandItems(callbacks)
127
25
 
128
26
  // Focus input on mount
129
27
  useEffect(() => {
@@ -143,110 +41,6 @@ export function CommandPalette({
143
41
  return () => document.removeEventListener('keydown', handler, true)
144
42
  }, [onClose])
145
43
 
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
44
  // Filter and rank results
251
45
  const filteredItems = useMemo(() => {
252
46
  if (!query.trim()) {
@@ -348,9 +142,8 @@ export function CommandPalette({
348
142
  {/* Search input */}
349
143
  <div className="flex items-center gap-3 px-4 py-3 border-b border-theme-border">
350
144
  <Search className="w-5 h-5 text-theme-text-secondary shrink-0" />
351
- <input
145
+ <Input
352
146
  ref={inputRef}
353
- type="text"
354
147
  value={query}
355
148
  onChange={(e) => setQuery(e.target.value)}
356
149
  onKeyDown={handleKeyDown}