nyx/frontend/src/components/layout/Sidebar.tsx

176 lines
4.3 KiB
TypeScript

import { NavLink } from 'react-router-dom';
import {
OverviewIcon,
FindingsIcon,
ScansIcon,
RulesIcon,
TriageIcon,
ConfigIcon,
ExplorerIcon,
DebugIcon,
FolderIcon,
TagIcon,
} from '../icons/Icons';
import type { FC } from 'react';
import type { IconProps } from '../icons/Icons';
import { useHealth } from '../../api/queries/health';
import { useOverview } from '../../api/queries/overview';
import { useSSE } from '../../contexts/SSEContext';
interface NavItem {
id: string;
label: string;
path: string;
Icon: FC<IconProps>;
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: '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' : ''}`;
}
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 (
<aside className="sidebar">
<div className="sidebar-header">
<img src="/logo.png" alt="Nyx" className="sidebar-logo-img" />
</div>
<ul className="nav-list">
{primary.map((item) => (
<li key={item.id}>
<NavLink
to={item.path}
end={item.path === '/'}
className={navLinkClass}
>
<span className="nav-icon">
<item.Icon />
</span>
<span className="nav-label">{item.label}</span>
{item.id === 'findings' && findingsCount != null && (
<span className="nav-badge">{findingsCount}</span>
)}
</NavLink>
</li>
))}
<li className="nav-section-header">Tools</li>
{secondary.map((item) => (
<li key={item.id}>
<NavLink to={item.path} className={navLinkClass}>
<span className="nav-icon">
<item.Icon />
</span>
<span className="nav-label">{item.label}</span>
</NavLink>
</li>
))}
</ul>
<div className="sidebar-footer">
<ul className="nav-list" style={{ flex: 'none' }}>
{footer.map((item) => (
<li key={item.id}>
<NavLink to={item.path} className={navLinkClass}>
<span className="nav-icon">
<item.Icon />
</span>
<span className="nav-label">{item.label}</span>
</NavLink>
</li>
))}
</ul>
</div>
<div className="sidebar-meta">
{health?.scan_root && (
<div className="sidebar-meta-item" title={health.scan_root}>
<FolderIcon />
<span>{health.scan_root}</span>
</div>
)}
{health?.version && (
<div className="sidebar-meta-item">
<TagIcon />
<span>v{health.version}</span>
</div>
)}
<div className={`scan-indicator${isScanRunning ? ' visible' : ''}`}>
<span className="status-dot running" />
Scanning...
</div>
</div>
</aside>
);
}