@skyhook-io/radar-app 1.5.0 → 1.6.0

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.
@@ -1,130 +1,26 @@
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
- import { GitBranch } from 'lucide-react'
6
4
  import { clsx } from 'clsx'
7
- import { useNamespaces, useContexts } from '../../api/client'
8
- import { CORE_RESOURCES, useAPIResources } from '../../api/apiResources'
9
- import { getResourceIcon } from '../../utils/resource-icons'
10
- type MainView = 'home' | 'topology' | 'resources' | 'timeline' | 'helm' | 'traffic' | 'cost' | 'audit' | 'gitops'
5
+ import { useCommandItems, bestScore, type CommandItem, type CommandItemCallbacks } from './command-items'
11
6
 
12
- interface CommandPaletteProps {
7
+ interface CommandPaletteProps extends CommandItemCallbacks {
13
8
  onClose: () => void
14
- onNavigateView: (view: MainView) => void
15
- onNavigateKind: (kind: string, group: string) => void
16
- onSwitchContext: (name: string) => void
17
- onSetNamespaces: (ns: string[]) => void
18
- onToggleTheme: () => void
19
- onShowDiagnostics?: () => void
20
9
  /** Controls fade-in/out animation (driven by useAnimatedUnmount) */
21
10
  isOpen?: boolean
22
11
  }
23
12
 
24
- interface CommandItem {
25
- id: string
26
- label: string
27
- sublabel?: string
28
- category: string
29
- icon?: React.ComponentType<{ className?: string }>
30
- shortcut?: string
31
- action: () => void
32
- /** Extra terms to match against during search (not displayed) */
33
- searchTerms?: string[]
34
- /** 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". */
35
- priorityBonus?: number
36
- }
37
-
38
- // Built-in k8s API groups. Used to nudge these above CRDs on tied matches.
39
- const CORE_GROUP_BONUS = 10
40
- const WELL_KNOWN_GROUP_BONUS = 5
41
- const WELL_KNOWN_GROUPS = new Set([
42
- 'apps',
43
- 'batch',
44
- 'autoscaling',
45
- 'policy',
46
- 'networking.k8s.io',
47
- 'rbac.authorization.k8s.io',
48
- 'storage.k8s.io',
49
- 'scheduling.k8s.io',
50
- 'coordination.k8s.io',
51
- 'apiextensions.k8s.io',
52
- 'admissionregistration.k8s.io',
53
- 'apiregistration.k8s.io',
54
- 'certificates.k8s.io',
55
- 'events.k8s.io',
56
- 'discovery.k8s.io',
57
- 'flowcontrol.apiserver.k8s.io',
58
- 'node.k8s.io',
59
- 'authentication.k8s.io',
60
- 'authorization.k8s.io',
61
- ])
62
-
63
- function groupPriorityBonus(group: string): number {
64
- if (!group) return CORE_GROUP_BONUS
65
- if (WELL_KNOWN_GROUPS.has(group)) return WELL_KNOWN_GROUP_BONUS
66
- return 0
67
- }
68
13
 
69
- // Fuzzy match scoring: exact > prefix > word boundary > substring.
70
- // Within a tier, a coverage bonus (up to +20) breaks ties in favor of
71
- // shorter labels — so "serv" picks Service over ServiceAccount, and
72
- // "service" picks Service (exact) decisively. Bonus is capped below the
73
- // 25-point tier gap, so tier ordering is preserved.
74
- function scoreMatch(text: string, query: string): number {
75
- const lower = text.toLowerCase()
76
- const q = query.toLowerCase()
77
- if (!lower.includes(q)) return 0
78
- let base: number
79
- if (lower === q) base = 150
80
- else if (lower.startsWith(q)) base = 100
81
- else {
82
- const wordStart = lower.indexOf(q)
83
- const prev = lower[wordStart - 1]
84
- base = wordStart > 0 && (prev === ' ' || prev === '/' || prev === '-' || prev === '.') ? 75 : 50
85
- }
86
- return base + (q.length / lower.length) * 20
87
- }
88
-
89
- function bestScore(item: CommandItem, query: string): number {
90
- // Primary label gets full score; secondary fields are discounted
91
- // so that e.g. "node" matching the label "Node" ranks above
92
- // "UpdateInfo" where "node" only matches the group "nodemanagement.gke.io"
93
- let best = scoreMatch(item.label, query)
94
- const secondary = Math.floor(Math.max(
95
- scoreMatch(item.sublabel || '', query),
96
- scoreMatch(item.category, query)
97
- ) * 0.6)
98
- best = Math.max(best, secondary)
99
- if (item.searchTerms) {
100
- for (const term of item.searchTerms) {
101
- best = Math.max(best, scoreMatch(term, query))
102
- }
103
- }
104
- // Only apply the priority bonus to items that actually matched, so we don't
105
- // surface unrelated built-ins ahead of a relevant CRD.
106
- return best > 0 ? best + (item.priorityBonus || 0) : 0
107
- }
108
-
109
- export function CommandPalette({
110
- onClose,
111
- onNavigateView,
112
- onNavigateKind,
113
- onSwitchContext,
114
- onSetNamespaces,
115
- onToggleTheme,
116
- onShowDiagnostics,
117
- isOpen = true,
118
- }: CommandPaletteProps) {
14
+ export function CommandPalette({ onClose, isOpen = true, ...callbacks }: CommandPaletteProps) {
119
15
  const [query, setQuery] = useState('')
120
16
  const [selectedIndex, setSelectedIndex] = useState(0)
121
17
  const inputRef = useRef<HTMLInputElement>(null)
122
18
  const resultsRef = useRef<HTMLDivElement>(null)
123
19
  const isKeyboardNav = useRef(false)
124
20
 
125
- const { data: namespacesData } = useNamespaces()
126
- const { data: contexts } = useContexts()
127
- const { data: apiResources } = useAPIResources()
21
+ // The static command items (Views / Kinds / Namespaces / Actions) — shared
22
+ // with the standalone omnibar via useCommandItems so the two never drift.
23
+ const items = useCommandItems(callbacks)
128
24
 
129
25
  // Focus input on mount
130
26
  useEffect(() => {
@@ -144,111 +40,6 @@ export function CommandPalette({
144
40
  return () => document.removeEventListener('keydown', handler, true)
145
41
  }, [onClose])
146
42
 
147
- // Build command items
148
- const items = useMemo<CommandItem[]>(() => {
149
- const result: CommandItem[] = []
150
-
151
- // Views
152
- const viewEntries: { view: MainView; label: string; icon: React.ComponentType<{ className?: string }>; shortcut: string }[] = [
153
- { view: 'home', label: 'Home', icon: Home, shortcut: '1' },
154
- { view: 'topology', label: 'Topology', icon: Network, shortcut: '2' },
155
- { view: 'resources', label: 'Resources', icon: List, shortcut: '3' },
156
- { view: 'timeline', label: 'Timeline', icon: Clock, shortcut: '4' },
157
- { view: 'helm', label: 'Helm', icon: Package, shortcut: '5' },
158
- { view: 'gitops', label: 'GitOps', icon: GitBranch, shortcut: '6' },
159
- { view: 'traffic', label: 'Traffic', icon: Activity, shortcut: '7' },
160
- { view: 'cost', label: 'Cost', icon: DollarSign, shortcut: '8' },
161
- { view: 'audit', label: 'Audit', icon: ShieldCheck, shortcut: '9' },
162
- ]
163
- for (const v of viewEntries) {
164
- result.push({
165
- id: `view-${v.view}`,
166
- label: `Go to ${v.label}`,
167
- category: 'Views',
168
- icon: v.icon,
169
- shortcut: v.shortcut,
170
- action: () => { onNavigateView(v.view) },
171
- })
172
- }
173
-
174
- // Resource kinds (deduplicate by name+group — backend may return multiple API versions)
175
- const resources = apiResources || CORE_RESOURCES
176
- const seenKinds = new Set<string>()
177
- for (const r of resources) {
178
- if (!r.verbs?.includes('list')) continue
179
- const kindKey = `${r.name}/${r.group}`
180
- if (seenKinds.has(kindKey)) continue
181
- seenKinds.add(kindKey)
182
- result.push({
183
- id: `kind-${r.name}-${r.group}`,
184
- label: r.kind,
185
- sublabel: r.group || 'core',
186
- category: 'Resource Kinds',
187
- icon: getResourceIcon(r.kind),
188
- action: () => { onNavigateKind(r.name, r.group) },
189
- searchTerms: [r.name, r.kind],
190
- priorityBonus: groupPriorityBonus(r.group),
191
- })
192
- }
193
-
194
- // Contexts
195
- if (contexts) {
196
- for (const ctx of contexts) {
197
- result.push({
198
- id: `context-${ctx.name}`,
199
- label: ctx.name,
200
- sublabel: ctx.isCurrent ? 'current' : ctx.cluster,
201
- category: 'Contexts',
202
- action: () => { if (!ctx.isCurrent) onSwitchContext(ctx.name) },
203
- })
204
- }
205
- }
206
-
207
- // Namespaces
208
- if (namespacesData) {
209
- for (const ns of namespacesData) {
210
- result.push({
211
- id: `ns-${ns.name}`,
212
- label: ns.name,
213
- category: 'Namespaces',
214
- action: () => { onSetNamespaces([ns.name]) },
215
- })
216
- }
217
- // "All namespaces" option
218
- result.push({
219
- id: 'ns-all',
220
- label: 'All Namespaces',
221
- category: 'Namespaces',
222
- action: () => { onSetNamespaces([]) },
223
- })
224
- }
225
-
226
- // Actions
227
- result.push({
228
- id: 'action-theme',
229
- label: 'Toggle Theme',
230
- category: 'Actions',
231
- icon: Sun,
232
- shortcut: 't',
233
- action: () => { onToggleTheme() },
234
- })
235
-
236
- if (onShowDiagnostics) {
237
- result.push({
238
- id: 'action-diagnostics',
239
- label: 'Diagnostics',
240
- category: 'Actions',
241
- icon: Stethoscope,
242
- shortcut: 'Ctrl+Shift+D',
243
- action: () => { onShowDiagnostics() },
244
- searchTerms: ['debug', 'health', 'status', 'snapshot'],
245
- })
246
- }
247
-
248
- return result
249
- // eslint-disable-next-line react-hooks/exhaustive-deps
250
- }, [apiResources, contexts, namespacesData, onNavigateView, onNavigateKind, onSwitchContext, onSetNamespaces, onToggleTheme, onShowDiagnostics])
251
-
252
43
  // Filter and rank results
253
44
  const filteredItems = useMemo(() => {
254
45
  if (!query.trim()) {