mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-30 20:39:39 +02:00
Dynamic (#77)
This commit is contained in:
parent
55247b7fcd
commit
991c84a1eb
1464 changed files with 225448 additions and 1985 deletions
|
|
@ -17,6 +17,7 @@ import { RulesPage } from '../../pages/RulesPage';
|
|||
import { TriagePage } from '../../pages/TriagePage';
|
||||
import { ConfigPage } from '../../pages/ConfigPage';
|
||||
import { ExplorerPage } from '../../pages/ExplorerPage';
|
||||
import { SurfacePage } from '../../pages/SurfacePage';
|
||||
import { DebugLayout } from '../../pages/debug/DebugLayout';
|
||||
import { CallGraphPage } from '../../pages/debug/CallGraphPage';
|
||||
import { SummaryExplorerPage } from '../../pages/debug/SummaryExplorerPage';
|
||||
|
|
@ -50,6 +51,12 @@ export function AppLayout() {
|
|||
label: 'Explorer',
|
||||
to: '/explorer',
|
||||
},
|
||||
{
|
||||
id: 'go-surface',
|
||||
group: 'Navigate',
|
||||
label: 'Attack surface',
|
||||
to: '/surface',
|
||||
},
|
||||
{
|
||||
id: 'go-debug-cg',
|
||||
group: 'Navigate',
|
||||
|
|
@ -141,6 +148,7 @@ export function AppLayout() {
|
|||
<Route path="/triage" element={<TriagePage />} />
|
||||
<Route path="/config" element={<ConfigPage />} />
|
||||
<Route path="/explorer" element={<ExplorerPage />} />
|
||||
<Route path="/surface" element={<SurfacePage />} />
|
||||
<Route path="/debug" element={<DebugLayout />}>
|
||||
<Route
|
||||
index
|
||||
|
|
|
|||
|
|
@ -8,13 +8,17 @@ import {
|
|||
ConfigIcon,
|
||||
ExplorerIcon,
|
||||
DebugIcon,
|
||||
FolderIcon,
|
||||
TagIcon,
|
||||
} from '../icons/Icons';
|
||||
import type { FC } from 'react';
|
||||
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 {
|
||||
|
|
@ -68,6 +72,13 @@ const NAV_SECTIONS: NavItem[] = [
|
|||
Icon: ExplorerIcon,
|
||||
group: 'secondary',
|
||||
},
|
||||
{
|
||||
id: 'surface',
|
||||
label: 'Surface',
|
||||
path: '/surface',
|
||||
Icon: ExplorerIcon,
|
||||
group: 'secondary',
|
||||
},
|
||||
{
|
||||
id: 'debug',
|
||||
label: 'Debug',
|
||||
|
|
@ -88,6 +99,167 @@ 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<HTMLDivElement | null>(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 (
|
||||
<div className="target-switcher" ref={menuRef}>
|
||||
<button
|
||||
type="button"
|
||||
className="target-trigger"
|
||||
onClick={() => setOpen((value) => !value)}
|
||||
aria-expanded={open}
|
||||
aria-label="Select project target"
|
||||
title={activeTarget?.path}
|
||||
>
|
||||
<span className="target-avatar">
|
||||
{targetInitial(activeTarget?.name ?? 'Project')}
|
||||
</span>
|
||||
<span className="target-trigger-copy">
|
||||
<span className="target-name">
|
||||
{activeTarget?.name ?? 'Select target'}
|
||||
</span>
|
||||
<span className="target-path">
|
||||
{activeTarget?.path ? compactPath(activeTarget.path) : 'No target'}
|
||||
</span>
|
||||
</span>
|
||||
<span className={`target-caret${open ? ' open' : ''}`} />
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div className="target-menu" role="menu">
|
||||
<div className="target-options">
|
||||
{targets.map((target) => (
|
||||
<button
|
||||
key={target.id}
|
||||
type="button"
|
||||
className={`target-option${target.active ? ' active' : ''}`}
|
||||
onClick={() => handleSelect(target.id)}
|
||||
disabled={target.active || !target.exists || isBusy}
|
||||
title={target.path}
|
||||
>
|
||||
<span className="target-option-avatar">
|
||||
{targetInitial(target.name)}
|
||||
</span>
|
||||
<span className="target-option-copy">
|
||||
<span className="target-option-name">{target.name}</span>
|
||||
<span className="target-option-path">
|
||||
{target.exists ? compactPath(target.path) : 'Missing path'}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<form className="target-add-form" onSubmit={handleAddSubmit}>
|
||||
<input
|
||||
value={newPath}
|
||||
onChange={(event) => setNewPath(event.target.value)}
|
||||
placeholder="/path/to/project"
|
||||
aria-label="Project path"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="target-add-button"
|
||||
disabled={!newPath.trim() || addTarget.isPending}
|
||||
title="Add target"
|
||||
aria-label="Add target"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</form>
|
||||
{errorMessage && <div className="target-error">{errorMessage}</div>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Sidebar() {
|
||||
const { data: health } = useHealth();
|
||||
const { data: overview } = useOverview();
|
||||
|
|
@ -105,6 +277,8 @@ export function Sidebar() {
|
|||
<img src="/logo.png" alt="Nyx" className="sidebar-logo-img" />
|
||||
</div>
|
||||
|
||||
<TargetSwitcher scanRoot={health?.scan_root} />
|
||||
|
||||
<ul className="nav-list">
|
||||
{primary.map((item) => (
|
||||
<li key={item.id}>
|
||||
|
|
@ -154,12 +328,6 @@ export function Sidebar() {
|
|||
</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 />
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue