feat: The Other Dude v9.0.1 — full-featured email system
ci: add GitHub Pages deployment workflow for docs site Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
320
frontend/src/components/command-palette/CommandPalette.tsx
Normal file
320
frontend/src/components/command-palette/CommandPalette.tsx
Normal file
@@ -0,0 +1,320 @@
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { Command } from 'cmdk'
|
||||
import { useNavigate } from '@tanstack/react-router'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import {
|
||||
Monitor,
|
||||
MapPin,
|
||||
Terminal,
|
||||
FileCode,
|
||||
Download,
|
||||
Bell,
|
||||
BellRing,
|
||||
Users,
|
||||
Building2,
|
||||
Settings,
|
||||
Search,
|
||||
LayoutDashboard,
|
||||
Moon,
|
||||
Sun,
|
||||
PanelLeft,
|
||||
} from 'lucide-react'
|
||||
import { useCommandPalette } from './useCommandPalette'
|
||||
import { useAuth, isSuperAdmin, isTenantAdmin } from '@/lib/auth'
|
||||
import { useUIStore } from '@/lib/store'
|
||||
import type { DeviceListResponse } from '@/lib/api'
|
||||
|
||||
interface PageCommand {
|
||||
label: string
|
||||
href: string
|
||||
icon: React.FC<{ className?: string }>
|
||||
description?: string
|
||||
visible: boolean
|
||||
}
|
||||
|
||||
export function CommandPalette() {
|
||||
const { open, setOpen } = useCommandPalette()
|
||||
const navigate = useNavigate()
|
||||
const { user } = useAuth()
|
||||
const { theme, setTheme, toggleSidebar } = useUIStore()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
// Global Cmd+K / Ctrl+K listener
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault()
|
||||
setOpen(!open)
|
||||
}
|
||||
}
|
||||
document.addEventListener('keydown', handleKeyDown)
|
||||
return () => document.removeEventListener('keydown', handleKeyDown)
|
||||
}, [open, setOpen])
|
||||
|
||||
const tenantDevicesHref = isSuperAdmin(user)
|
||||
? '/tenants'
|
||||
: `/tenants/${user?.tenant_id ?? ''}/devices`
|
||||
|
||||
// Build page commands based on user role
|
||||
const pageCommands: PageCommand[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: 'Dashboard',
|
||||
href: tenantDevicesHref,
|
||||
icon: LayoutDashboard,
|
||||
description: 'Fleet overview',
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
label: 'Devices',
|
||||
href: tenantDevicesHref,
|
||||
icon: Monitor,
|
||||
description: 'Device list',
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
label: 'Map',
|
||||
href: '/map',
|
||||
icon: MapPin,
|
||||
description: 'Network map',
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
label: 'Config Editor',
|
||||
href: '/config-editor',
|
||||
icon: Terminal,
|
||||
description: 'Device configuration',
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
label: 'Templates',
|
||||
href: '/templates',
|
||||
icon: FileCode,
|
||||
description: 'Config templates',
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
label: 'Firmware',
|
||||
href: '/firmware',
|
||||
icon: Download,
|
||||
description: 'Firmware management',
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
label: 'Alerts',
|
||||
href: '/alerts',
|
||||
icon: Bell,
|
||||
description: 'Active alerts',
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
label: 'Alert Rules',
|
||||
href: '/alert-rules',
|
||||
icon: BellRing,
|
||||
description: 'Alert rule configuration',
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
label: 'Users',
|
||||
href: `/tenants/${user?.tenant_id ?? ''}/users`,
|
||||
icon: Users,
|
||||
description: 'User management',
|
||||
visible: isTenantAdmin(user) && !!user?.tenant_id,
|
||||
},
|
||||
{
|
||||
label: 'Organizations',
|
||||
href: '/tenants',
|
||||
icon: Building2,
|
||||
description: 'Organization management',
|
||||
visible: isSuperAdmin(user) || isTenantAdmin(user),
|
||||
},
|
||||
{
|
||||
label: 'Settings',
|
||||
href: '/settings',
|
||||
icon: Settings,
|
||||
description: 'Application settings',
|
||||
visible: true,
|
||||
},
|
||||
],
|
||||
[user, tenantDevicesHref],
|
||||
)
|
||||
|
||||
// Get cached devices from TanStack Query (try common query key patterns)
|
||||
const devicesCache = useMemo(() => {
|
||||
// Try to find devices data in the query cache
|
||||
const allQueries = queryClient.getQueriesData<DeviceListResponse>({
|
||||
queryKey: ['devices'],
|
||||
})
|
||||
const devices: Array<{
|
||||
id: string
|
||||
hostname: string
|
||||
ip_address: string
|
||||
tenant_id: string
|
||||
}> = []
|
||||
|
||||
for (const [, data] of allQueries) {
|
||||
if (data && 'items' in data && Array.isArray(data.items)) {
|
||||
for (const device of data.items) {
|
||||
devices.push({
|
||||
id: device.id,
|
||||
hostname: device.hostname,
|
||||
ip_address: device.ip_address,
|
||||
tenant_id: user?.tenant_id ?? '',
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also try fleet summary cache
|
||||
const fleetQueries = queryClient.getQueriesData<
|
||||
Array<{
|
||||
id: string
|
||||
hostname: string
|
||||
ip_address: string
|
||||
tenant_id: string
|
||||
}>
|
||||
>({ queryKey: ['fleet'] })
|
||||
for (const [, data] of fleetQueries) {
|
||||
if (Array.isArray(data)) {
|
||||
for (const device of data) {
|
||||
if (
|
||||
device.id &&
|
||||
device.hostname &&
|
||||
!devices.some((d) => d.id === device.id)
|
||||
) {
|
||||
devices.push({
|
||||
id: device.id,
|
||||
hostname: device.hostname,
|
||||
ip_address: device.ip_address,
|
||||
tenant_id: device.tenant_id ?? user?.tenant_id ?? '',
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return devices
|
||||
}, [queryClient, user?.tenant_id, open])
|
||||
|
||||
const handleSelect = (href: string) => {
|
||||
setOpen(false)
|
||||
void navigate({ to: href })
|
||||
}
|
||||
|
||||
const visiblePages = pageCommands.filter((p) => p.visible)
|
||||
|
||||
const itemClass =
|
||||
'flex items-center gap-3 px-2 py-2 rounded-lg text-sm text-text-secondary cursor-pointer data-[selected=true]:bg-accent-muted data-[selected=true]:text-accent'
|
||||
const groupHeadingClass =
|
||||
'[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-semibold [&_[cmdk-group-heading]]:text-text-muted [&_[cmdk-group-heading]]:uppercase [&_[cmdk-group-heading]]:tracking-wider'
|
||||
|
||||
return (
|
||||
<Command.Dialog
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
label="Command Menu"
|
||||
overlayClassName="fixed inset-0 z-50 bg-background/80 backdrop-blur-sm"
|
||||
contentClassName="fixed left-1/2 top-[20%] -translate-x-1/2 z-50 w-full max-w-lg"
|
||||
>
|
||||
<div className="rounded-xl border border-border bg-surface shadow-2xl overflow-hidden">
|
||||
<div className="flex items-center gap-2 px-4 border-b border-border">
|
||||
<Search className="h-4 w-4 text-text-muted flex-shrink-0" />
|
||||
<Command.Input
|
||||
placeholder="Search pages, devices, actions..."
|
||||
className="w-full py-3 text-sm bg-transparent text-text-primary placeholder:text-text-muted outline-none"
|
||||
/>
|
||||
</div>
|
||||
<Command.List className="max-h-80 overflow-y-auto p-2">
|
||||
<Command.Empty className="px-4 py-6 text-center text-sm text-text-muted">
|
||||
No results found.
|
||||
</Command.Empty>
|
||||
|
||||
{/* Pages group */}
|
||||
<Command.Group heading="Pages" className={groupHeadingClass}>
|
||||
{visiblePages.map((page) => {
|
||||
const Icon = page.icon
|
||||
return (
|
||||
<Command.Item
|
||||
key={page.label}
|
||||
value={`${page.label} ${page.description ?? ''}`}
|
||||
onSelect={() => handleSelect(page.href)}
|
||||
className={itemClass}
|
||||
>
|
||||
<Icon className="h-4 w-4 flex-shrink-0" />
|
||||
<span>{page.label}</span>
|
||||
{page.description && (
|
||||
<span className="ml-auto text-xs text-text-muted">
|
||||
{page.description}
|
||||
</span>
|
||||
)}
|
||||
</Command.Item>
|
||||
)
|
||||
})}
|
||||
</Command.Group>
|
||||
|
||||
{/* Devices group (from cache) */}
|
||||
{devicesCache.length > 0 && (
|
||||
<Command.Group heading="Devices" className={groupHeadingClass}>
|
||||
{devicesCache.slice(0, 20).map((device) => (
|
||||
<Command.Item
|
||||
key={device.id}
|
||||
value={`${device.hostname} ${device.ip_address}`}
|
||||
onSelect={() =>
|
||||
handleSelect(
|
||||
`/tenants/${device.tenant_id}/devices`,
|
||||
)
|
||||
}
|
||||
className={itemClass}
|
||||
>
|
||||
<Monitor className="h-4 w-4 flex-shrink-0" />
|
||||
<span>{device.hostname}</span>
|
||||
<span className="ml-auto text-xs text-text-muted font-mono">
|
||||
{device.ip_address}
|
||||
</span>
|
||||
</Command.Item>
|
||||
))}
|
||||
</Command.Group>
|
||||
)}
|
||||
|
||||
{/* Actions group */}
|
||||
<Command.Group heading="Actions" className={groupHeadingClass}>
|
||||
<Command.Item
|
||||
value="toggle dark light mode theme"
|
||||
onSelect={() => {
|
||||
setTheme(theme === 'dark' ? 'light' : 'dark')
|
||||
setOpen(false)
|
||||
}}
|
||||
className={itemClass}
|
||||
>
|
||||
{theme === 'dark' ? (
|
||||
<Sun className="h-4 w-4 flex-shrink-0" />
|
||||
) : (
|
||||
<Moon className="h-4 w-4 flex-shrink-0" />
|
||||
)}
|
||||
<span>
|
||||
Toggle {theme === 'dark' ? 'Light' : 'Dark'} Mode
|
||||
</span>
|
||||
</Command.Item>
|
||||
<Command.Item
|
||||
value="toggle sidebar collapse expand"
|
||||
onSelect={() => {
|
||||
toggleSidebar()
|
||||
setOpen(false)
|
||||
}}
|
||||
className={itemClass}
|
||||
>
|
||||
<PanelLeft className="h-4 w-4 flex-shrink-0" />
|
||||
<span>Toggle Sidebar</span>
|
||||
</Command.Item>
|
||||
</Command.Group>
|
||||
</Command.List>
|
||||
|
||||
{/* Footer with shortcut hints */}
|
||||
<div className="flex items-center justify-between px-4 py-2 border-t border-border text-xs text-text-muted">
|
||||
<span>Navigate with arrow keys</span>
|
||||
<span>Open with Cmd+K</span>
|
||||
</div>
|
||||
</div>
|
||||
</Command.Dialog>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user