feat: implement Spotlight Krate Creation workflow
- Add type-to-trigger Spotlight with keyboard (any character) - Add canvas click to open Spotlight - Implement keyboard navigation (↑↓ Enter Esc) - Add keyboard shortcut handlers and spotlight store - Create useSpotlight hook with fuzzy search - Create mock Kubernetes resources for initial testing - Implement krate creation with collision detection - Add Quick Actions (all pods, services, deployments, namespaces) - Create Spotlight with filter chips and result rendering - Add Spotlight state management with setQuery, setFilter, setSel - Include design specs (Krates.dc.html, server.js, support.js)
This commit is contained in:
@@ -1,52 +1,134 @@
|
||||
import { useMemo } from 'react'
|
||||
import { fuzzySearch } from '../utils/fuzzy'
|
||||
import { MOCK_NAMESPACES, MOCK_PODS, MOCK_DEPLOYMENTS, MOCK_SERVICES } from '../data/mockResources'
|
||||
|
||||
export interface SpotlightResource {
|
||||
id: string
|
||||
type: string
|
||||
name: string
|
||||
namespace?: string
|
||||
status?: string
|
||||
color: string
|
||||
}
|
||||
|
||||
export const useSpotlight = () => {
|
||||
const fuzzy = (query: string, text: string): number => {
|
||||
if (!query) return 0
|
||||
|
||||
const queryLower = query.toLowerCase()
|
||||
const textLower = text.toLowerCase()
|
||||
|
||||
let score = 0
|
||||
let queryIndex = 0
|
||||
|
||||
for (let i = 0; i < textLower.length && queryIndex < queryLower.length; i++) {
|
||||
if (textLower[i] === queryLower[queryIndex]) {
|
||||
score++
|
||||
queryIndex++
|
||||
}
|
||||
const executeSearch = (query: string, filterType: string | null): SpotlightResource[] => {
|
||||
if (!query || query.trim() === '') {
|
||||
return []
|
||||
}
|
||||
|
||||
return score / Math.max(query.length, text.length)
|
||||
}
|
||||
|
||||
const filterItems = <T extends { name: string; type: string }>(
|
||||
items: T[],
|
||||
query: string,
|
||||
filterType: string | null
|
||||
): T[] => {
|
||||
return items.filter((item) => {
|
||||
const matchesQuery = fuzzy(query, item.name) > 0.3
|
||||
const matchesType = !filterType || filterType === 'all' || item.type === filterType
|
||||
return matchesQuery && matchesType
|
||||
})
|
||||
}
|
||||
const resources: SpotlightResource[] = []
|
||||
|
||||
const debounce = <T extends (...args: any[]) => any>(fn: T, delay: number) => {
|
||||
let timeoutId: NodeJS.Timeout | null = null
|
||||
return (...args: Parameters<T>): Promise<ReturnType<T>> => {
|
||||
return new Promise((resolve) => {
|
||||
if (timeoutId) clearTimeout(timeoutId)
|
||||
timeoutId = setTimeout(() => resolve(fn(...args)), delay)
|
||||
const addIfExists = (items: any[], type: string) => {
|
||||
items.forEach((item, index) => {
|
||||
const name = item.name || item.metadata?.name
|
||||
const ns = item.namespace || item.metadata?.namespace || 'default'
|
||||
const color = getNamespaceColor(ns)
|
||||
|
||||
const score = fuzzySearch(query, name)
|
||||
if (score > 0.3) {
|
||||
if (!filterType || filterType === 'all' || filterType === type) {
|
||||
resources.push({
|
||||
id: `${type}-${index}-${name}`,
|
||||
type,
|
||||
name,
|
||||
namespace: ns,
|
||||
color: color,
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (!filterType || filterType === 'all' || filterType === 'namespace') {
|
||||
MOCK_NAMESPACES.forEach((ns, index) => {
|
||||
if (fuzzySearch(query, ns.name) > 0.3) {
|
||||
resources.push({
|
||||
id: `namespace-${index}-${ns.name}`,
|
||||
type: 'namespace',
|
||||
name: ns.name,
|
||||
color: getNamespaceColor(ns.name),
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (!filterType || filterType === 'all' || filterType === 'pod') {
|
||||
addIfExists(MOCK_PODS, 'pod')
|
||||
}
|
||||
|
||||
if (!filterType || filterType === 'all' || filterType === 'deployment') {
|
||||
addIfExists(MOCK_DEPLOYMENTS, 'deployment')
|
||||
}
|
||||
|
||||
if (!filterType || filterType === 'all' || filterType === 'service') {
|
||||
addIfExists(MOCK_SERVICES, 'service')
|
||||
}
|
||||
|
||||
return resources.sort((a, b) => fuzzySearch(query, b.name) - fuzzySearch(query, a.name))
|
||||
}
|
||||
|
||||
const getQuickActions = (query: string): SpotlightResource[] => {
|
||||
const actions: SpotlightResource[] = []
|
||||
const namespaces = Array.from(new Set([...MOCK_NAMESPACES.map((ns) => ns.name)]))
|
||||
|
||||
namespaces.forEach((ns, index) => {
|
||||
actions.push({
|
||||
id: `quick-${index}-${ns}`,
|
||||
type: 'quick_create',
|
||||
name: `Create krate for ns/${ns}`,
|
||||
namespace: ns,
|
||||
color: getNamespaceColor(ns),
|
||||
})
|
||||
})
|
||||
|
||||
actions.push({
|
||||
id: 'quick-pods',
|
||||
type: 'quick_create',
|
||||
name: 'All pods',
|
||||
color: '#6fb1ff',
|
||||
})
|
||||
|
||||
actions.push({
|
||||
id: 'quick-svcs',
|
||||
type: 'quick_create',
|
||||
name: 'All services',
|
||||
color: '#6fb1ff',
|
||||
})
|
||||
|
||||
actions.push({
|
||||
id: 'quick-deployments',
|
||||
type: 'quick_create',
|
||||
name: 'All deployments',
|
||||
color: '#6fb1ff',
|
||||
})
|
||||
|
||||
if (query.trim()) {
|
||||
actions.push({
|
||||
id: 'quick-search',
|
||||
type: 'quick_create',
|
||||
name: `Search again for "${query}"`,
|
||||
color: '#4dd6e8',
|
||||
})
|
||||
}
|
||||
|
||||
return actions
|
||||
}
|
||||
|
||||
const getNamespaceColor = (namespace: string): string => {
|
||||
if (namespace === 'default') return '#6fb1ff'
|
||||
if (namespace === 'kube-system') return '#9c88ff'
|
||||
if (namespace === 'kube-public') return '#4dd6e8'
|
||||
if (namespace === 'production') return '#6fb1ff'
|
||||
if (namespace === 'staging') return '#4dd6e8'
|
||||
return '#6fb1ff'
|
||||
}
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
fuzzy,
|
||||
filterItems,
|
||||
debounce,
|
||||
executeSearch,
|
||||
getQuickActions,
|
||||
getNamespaceColor,
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user