admin / SAynapse-Horizon
publicSelf Hosted Cyber Threat Intelligence Hub
SAynapse-Horizon / Synapse-Horizon-v2 / src / components / Sidebar.jsx
6426 B · main
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 | import { RefreshCw, Rss, Radio, Timer, DownloadCloud, Layers } from 'lucide-react' import { SOURCES, REFRESH_OPTIONS, CADENCE_OPTIONS } from '../data/sources' import ToggleSwitch from './ToggleSwitch' import { accentChip } from '../utils/theme' // Rigid left control deck: feed toggles + lifecycle controls. export default function Sidebar({ enabled, onToggle, refreshKey, onRefreshChange, cadenceKey, onCadenceChange, onForceSync, loading, visibleCountBySource, sourceStatus = {}, }) { return ( <aside className="flex h-full w-72 shrink-0 flex-col border-r border-slate-800/80 bg-slate-950/40"> <div className="flex items-center gap-2 border-b border-slate-800/80 px-4 py-3"> <Radio className="h-4 w-4 text-sky-400" /> <h2 className="text-xs font-semibold uppercase tracking-[0.16em] text-slate-300"> Feed Control Panel </h2> </div> <div className="flex-1 overflow-y-auto px-4 py-4"> {/* Feed source toggles */} <SectionLabel icon={Layers}>Feed Source Toggles</SectionLabel> <ul className="mt-2 space-y-1"> {SOURCES.map((s) => { const on = enabled[s.key] const count = visibleCountBySource[s.key] || 0 const st = sourceStatus[s.key] || {} const needsKey = st.configured === false const errored = st.status === 'error' return ( <li key={s.key} className={`group flex items-center gap-3 rounded-md border px-2.5 py-2 transition-colors ${ on ? 'border-slate-700/70 bg-slate-800/40' : 'border-slate-800/50 bg-transparent' }`} > <div className="min-w-0 flex-1"> <div className="flex items-center gap-1.5"> {s.key === 'rss' ? ( <Rss className="h-3 w-3 shrink-0 text-slate-500" /> ) : null} <span className={`truncate text-sm font-medium ${ on ? 'text-slate-100' : 'text-slate-500' }`} > {s.name} </span> </div> <div className="mt-0.5 flex items-center gap-1.5"> <span className={`inline-flex items-center rounded px-1 py-px text-[9px] font-semibold uppercase tracking-wide ${accentChip( s.accent )}`} > {s.kind} </span> <span className={`truncate text-[10px] ${ needsKey ? 'text-amber-400/80' : errored ? 'text-rose-400/80' : 'text-slate-500' }`} title={st.message || undefined} > {needsKey ? 'needs API key' : errored ? 'fetch error' : on ? `${count} IOC${count === 1 ? '' : 's'}` : 'muted'} </span> </div> </div> <ToggleSwitch checked={on} onChange={() => onToggle(s.key)} label={`Toggle ${s.name}`} /> </li> ) })} </ul> {/* Lifecycle controls */} <div className="mt-6 border-t border-slate-800/70 pt-4"> <SectionLabel icon={RefreshCw}>Auto-Refresh Interval</SectionLabel> <p className="mt-1 text-[10px] leading-snug text-slate-500"> How often the display layer re-polls local state. </p> <SegmentedControl options={REFRESH_OPTIONS} value={refreshKey} onChange={onRefreshChange} /> </div> <div className="mt-5"> <SectionLabel icon={Timer}>Feed Pull Cadence</SectionLabel> <p className="mt-1 text-[10px] leading-snug text-slate-500"> How often the ingestion pipeline hits external APIs. </p> <SegmentedControl options={CADENCE_OPTIONS} value={cadenceKey} onChange={onCadenceChange} columns={1} /> </div> </div> {/* Force sync pinned to the bottom */} <div className="border-t border-slate-800/80 p-4"> <button type="button" onClick={onForceSync} disabled={loading} className={`flex w-full items-center justify-center gap-2 rounded-md px-3 py-2.5 text-sm font-semibold transition-all ${ loading ? 'cursor-wait bg-sky-500/20 text-sky-300 ring-1 ring-inset ring-sky-500/40' : 'bg-sky-500 text-slate-950 hover:bg-sky-400 active:scale-[0.98]' }`} > {loading ? ( <> <RefreshCw className="h-4 w-4 animate-spin" /> Syncing channels… </> ) : ( <> <DownloadCloud className="h-4 w-4" /> Manual Force Sync </> )} </button> </div> </aside> ) } function SectionLabel({ icon: Icon, children }) { return ( <div className="flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-[0.14em] text-slate-400"> <Icon className="h-3.5 w-3.5 text-slate-500" /> {children} </div> ) } function SegmentedControl({ options, value, onChange, columns = 2 }) { return ( <div className={`mt-2 grid gap-1.5 ${columns === 1 ? 'grid-cols-2' : 'grid-cols-3'}`} > {options.map((o) => { const active = o.key === value return ( <button key={o.key} type="button" onClick={() => onChange(o.key)} className={`rounded-md px-2 py-1.5 text-xs font-medium transition-colors ${ active ? 'bg-slate-700 text-slate-100 ring-1 ring-inset ring-slate-600' : 'bg-slate-800/40 text-slate-400 hover:bg-slate-800 hover:text-slate-200' }`} > {o.label} </button> ) })} </div> ) } |