SPB Git

spb/akos

Public
6.6 KB · 115 lines tsx
1import { createContext, useContext, useEffect, useState, type FormEvent } from "react";2import { NavLink, Navigate, Route, Routes, useLocation, useNavigate } from "react-router";3import { api, UNAUTH_EVENT, type Me } from "./api";4import { useTheme } from "./hooks";5import { Loading } from "./components/ui";6import Login from "./pages/Login";7import Overview from "./pages/Overview";8import Apis from "./pages/Apis";9import ApiDetail from "./pages/ApiDetail";10import AkpDetail from "./pages/AkpDetail";11import GraphPage from "./pages/GraphPage";12import Executions from "./pages/Executions";13import ExecutionDetail from "./pages/ExecutionDetail";14import Credentials from "./pages/Credentials";15import Policy from "./pages/Policy";16import Lineage from "./pages/Lineage";17import DatasetDetail from "./pages/DatasetDetail";18import DebtPage from "./pages/Debt";19import Projects from "./pages/Projects";20import ProjectDetail from "./pages/ProjectDetail";21import Settings from "./pages/Settings";22import Operations from "./pages/Operations";23import SearchPage from "./pages/Search";24import VersionDetail from "./pages/VersionDetail";25import EndpointDetail from "./pages/EndpointDetail";2627export const MeContext = createContext<{ me: Me | null; refresh: () => void }>({ me: null, refresh: () => {} });28export const useMe = () => useContext(MeContext);2930const NAV: { group: string; items: { to: string; label: string }[] }[] = [31  { group: "Knowledge", items: [{ to: "/", label: "Overview" }, { to: "/apis", label: "API Atlas" }, { to: "/graph", label: "Knowledge Graph" }] },32  { group: "Execution", items: [{ to: "/executions", label: "Execution Observatory" }, { to: "/credentials", label: "Credential Vault" }, { to: "/policy", label: "Policy Center" }] },33  { group: "Research", items: [{ to: "/lineage", label: "Research Lineage" }, { to: "/debt", label: "Knowledge Debt" }] },34  { group: "Admin", items: [{ to: "/projects", label: "Agents & Projects" }, { to: "/operations", label: "Operations" }, { to: "/settings", label: "Settings" }] },35];3637function Nav({ onNavigate }: { onNavigate?: () => void }) {38  return (39    <div className="nav">40      {NAV.map((g) => (<div key={g.group}><div className="group">{g.group}</div>{g.items.map((it) => <NavLink key={it.to} to={it.to} end={it.to === "/"} onClick={onNavigate}>{it.label}</NavLink>)}</div>))}41    </div>42  );43}4445function Shell({ me, refresh, children }: { me: Me; refresh: () => void; children: React.ReactNode }) {46  const [open, setOpen] = useState(false);47  const [q, setQ] = useState("");48  const nav = useNavigate();49  const loc = useLocation();50  const { theme, toggle } = useTheme();51  useEffect(() => { setOpen(false); }, [loc.pathname]);52  const submit = (e: FormEvent) => { e.preventDefault(); if (q.trim()) nav(`/search?q=${encodeURIComponent(q.trim())}`); };53  const logout = async () => { await api("auth/logout", { method: "POST" }).catch(() => {}); refresh(); };54  return (55    <div className="app">56      <header className="topbar">57        <button className="iconbtn hamburger" aria-label="Open navigation" aria-expanded={open} aria-controls="drawer" onClick={() => setOpen(true)}>☰</button>58        <NavLink to="/" className="brand">AKOS</NavLink>59        <form onSubmit={submit} role="search"><label htmlFor="q" className="sr-only">Search knowledge</label><input id="q" placeholder="Search APIs, endpoints, variables…" value={q} onChange={(e) => setQ(e.target.value)} /></form>60        <button className="iconbtn" aria-label="Toggle dark mode" title={`theme: ${theme}`} onClick={toggle}>{theme === "dark" ? "☀" : "☾"}</button>61        <span className="userbox">{me.email} · {me.role}</span>62        <button className="btn sm" onClick={logout}>Logout</button>63      </header>64      <div className="body">65        <aside className="sidebar" aria-label="Main navigation"><Nav /></aside>66        <div id="drawer" className={`drawer ${open ? "open" : ""}`} aria-hidden={!open}>67          <div className="scrim" onClick={() => setOpen(false)} />68          <nav aria-label="Main navigation"><div className="row between" style={{ marginBottom: 8 }}><b>AKOS</b><button className="iconbtn" aria-label="Close navigation" onClick={() => setOpen(false)}>✕</button></div><Nav onNavigate={() => setOpen(false)} /><div className="hint" style={{ marginTop: 12 }}>{me.email}</div></nav>69        </div>70        <main className="main">{children}</main>71      </div>72    </div>73  );74}7576export default function App() {77  const [me, setMe] = useState<Me | null | undefined>(undefined);78  const [tick, setTick] = useState(0);79  const refresh = () => setTick((t) => t + 1);80  useEffect(() => { api<Me>("auth/me", { noRedirect: true }).then(setMe).catch(() => setMe(null)); }, [tick]);81  useEffect(() => { const h = () => setMe(null); window.addEventListener(UNAUTH_EVENT, h); return () => window.removeEventListener(UNAUTH_EVENT, h); }, []);82  const loc = useLocation();83  if (me === undefined) return <div className="login"><Loading what="Connecting to AKOS" /></div>;84  if (!me) return <Routes><Route path="/login" element={<Login onLogin={refresh} />} /><Route path="*" element={<Navigate to={`/login?next=${encodeURIComponent(loc.pathname + loc.search)}`} replace />} /></Routes>;85  return (86    <MeContext.Provider value={{ me, refresh }}>87      <Shell me={me} refresh={refresh}>88        <Routes>89          <Route path="/login" element={<Navigate to="/" replace />} />90          <Route path="/" element={<Overview />} />91          <Route path="/apis" element={<Apis />} />92          <Route path="/apis/:id" element={<ApiDetail />} />93          <Route path="/versions/:id" element={<VersionDetail />} />94          <Route path="/endpoints/:id" element={<EndpointDetail />} />95          <Route path="/akps/:id" element={<AkpDetail />} />96          <Route path="/graph" element={<GraphPage />} />97          <Route path="/executions" element={<Executions />} />98          <Route path="/executions/:id" element={<ExecutionDetail />} />99          <Route path="/credentials" element={<Credentials />} />100          <Route path="/policy" element={<Policy />} />101          <Route path="/lineage" element={<Lineage />} />102          <Route path="/datasets/:id" element={<DatasetDetail />} />103          <Route path="/debt" element={<DebtPage />} />104          <Route path="/projects" element={<Projects />} />105          <Route path="/projects/:id" element={<ProjectDetail />} />106          <Route path="/settings" element={<Settings />} />107          <Route path="/operations" element={<Operations />} />108          <Route path="/search" element={<SearchPage />} />109          <Route path="*" element={<div className="empty">Page not found.</div>} />110        </Routes>111      </Shell>112    </MeContext.Provider>113  );114}115
Files · da58f809c01c422e65c786b05d6ffa3079c9feba
Menu

Theme