@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.
- package/package.json +4 -4
- package/src/App.tsx +168 -42
- package/src/RadarApp.tsx +9 -1
- package/src/api/client.ts +65 -2
- package/src/components/UserMenu.tsx +56 -10
- package/src/components/applications/ApplicationsView.tsx +27 -19
- package/src/components/audit/AuditSettingsDialog.tsx +1 -1
- package/src/components/audit/AuditView.tsx +23 -35
- package/src/components/gitops/GitOpsView.tsx +24 -2
- package/src/components/helm/HelmView.tsx +12 -8
- package/src/components/home/HomeView.tsx +1 -1
- package/src/components/home/mcpToolCatalog.ts +34 -0
- package/src/components/issues/IssuesPane.tsx +82 -28
- package/src/components/nav/PrimaryNavRail.tsx +282 -0
- package/src/components/resource/HPACharts.tsx +7 -2
- package/src/components/resource/RestartChart.tsx +8 -0
- package/src/components/resources/renderers/HPARenderer.tsx +4 -1
- package/src/components/resources/renderers/WorkloadRenderer.tsx +34 -3
- package/src/components/settings/SettingsDialog.tsx +18 -1
- package/src/components/ui/CommandPalette.tsx +6 -215
- package/src/components/ui/Omnibar.tsx +493 -0
- package/src/components/ui/SearchSyntaxHelp.tsx +89 -0
- package/src/components/ui/command-items.ts +178 -0
- package/src/components/workload/WorkloadView.tsx +3 -1
- package/src/context/NavCustomization.tsx +11 -0
- package/src/hooks/useMediaQuery.ts +21 -0
- package/src/hooks/useNavRailPinned.ts +46 -0
- package/src/hooks/useRecentResources.ts +49 -0
- package/src/utils/navigation.ts +11 -0
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
const
|
|
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()) {
|