import { NavLink } from 'react-router-dom'; import { OverviewIcon, FindingsIcon, ScansIcon, RulesIcon, TriageIcon, ConfigIcon, ExplorerIcon, DebugIcon, TagIcon, } from '../icons/Icons'; import { useEffect, useRef, useState, type FC, type FormEvent } from 'react'; import type { IconProps } from '../icons/Icons'; import { useHealth } from '../../api/queries/health'; import { useOverview } from '../../api/queries/overview'; import { useAddTarget, useSelectTarget, useTargets, } from '../../api/queries/targets'; import { useSSE } from '../../contexts/SSEContext'; interface NavItem { id: string; label: string; path: string; Icon: FC; group: 'primary' | 'secondary' | 'footer'; } const NAV_SECTIONS: NavItem[] = [ { id: 'overview', label: 'Overview', path: '/', Icon: OverviewIcon, group: 'primary', }, { id: 'findings', label: 'Findings', path: '/findings', Icon: FindingsIcon, group: 'primary', }, { id: 'scans', label: 'Scans', path: '/scans', Icon: ScansIcon, group: 'primary', }, { id: 'rules', label: 'Rules', path: '/rules', Icon: RulesIcon, group: 'primary', }, { id: 'triage', label: 'Triage', path: '/triage', Icon: TriageIcon, group: 'primary', }, { id: 'explorer', label: 'Explorer', path: '/explorer', Icon: ExplorerIcon, group: 'secondary', }, { id: 'surface', label: 'Surface', path: '/surface', Icon: ExplorerIcon, group: 'secondary', }, { id: 'debug', label: 'Debug', path: '/debug', Icon: DebugIcon, group: 'secondary', }, { id: 'config', label: 'Config', path: '/config', Icon: ConfigIcon, group: 'footer', }, ]; function navLinkClass({ isActive }: { isActive: boolean }) { return `nav-link${isActive ? ' active' : ''}`; } function targetNameFromPath(path: string) { const parts = path.split(/[\\/]/).filter(Boolean); return parts[parts.length - 1] || path || 'Project'; } function targetInitial(name: string) { return name.trim().charAt(0).toUpperCase() || '?'; } function compactPath(path: string) { return path.replace(/^\/Users\/[^/]+/, '~'); } function TargetSwitcher({ scanRoot }: { scanRoot?: string }) { const { data: targets = [] } = useTargets(); const addTarget = useAddTarget(); const selectTarget = useSelectTarget(); const [open, setOpen] = useState(false); const [newPath, setNewPath] = useState(''); const menuRef = useRef(null); const activeTarget = targets.find((target) => target.active) ?? (scanRoot ? { id: '__active__', name: targetNameFromPath(scanRoot), path: scanRoot, active: true, exists: true, } : undefined); useEffect(() => { if (!open) return; function handlePointerDown(event: MouseEvent) { if ( menuRef.current && event.target instanceof Node && !menuRef.current.contains(event.target) ) { setOpen(false); } } function handleKeyDown(event: KeyboardEvent) { if (event.key === 'Escape') setOpen(false); } document.addEventListener('mousedown', handlePointerDown); document.addEventListener('keydown', handleKeyDown); return () => { document.removeEventListener('mousedown', handlePointerDown); document.removeEventListener('keydown', handleKeyDown); }; }, [open]); function handleSelect(id: string) { selectTarget.mutate( { id }, { onSuccess: () => setOpen(false), }, ); } function handleAddSubmit(event: FormEvent) { event.preventDefault(); const path = newPath.trim(); if (!path || addTarget.isPending) return; addTarget.mutate( { path }, { onSuccess: (target) => { setNewPath(''); selectTarget.mutate( { id: target.id }, { onSuccess: () => setOpen(false), }, ); }, }, ); } const isBusy = addTarget.isPending || selectTarget.isPending; const errorMessage = addTarget.error instanceof Error ? addTarget.error.message : null; return (
{open && (
{targets.map((target) => ( ))}
setNewPath(event.target.value)} placeholder="/path/to/project" aria-label="Project path" />
{errorMessage &&
{errorMessage}
}
)}
); } export function Sidebar() { const { data: health } = useHealth(); const { data: overview } = useOverview(); const { isScanRunning } = useSSE(); const primary = NAV_SECTIONS.filter((n) => n.group === 'primary'); const secondary = NAV_SECTIONS.filter((n) => n.group === 'secondary'); const footer = NAV_SECTIONS.filter((n) => n.group === 'footer'); const findingsCount = overview && overview.state !== 'empty' ? overview.total_findings : null; return ( ); }