diff --git a/apps/fabro-web/app/router.tsx b/apps/fabro-web/app/router.tsx index fd41edaba..703bb2e31 100644 --- a/apps/fabro-web/app/router.tsx +++ b/apps/fabro-web/app/router.tsx @@ -34,6 +34,7 @@ import * as Insights from "./routes/insights"; import * as InsightsEditor from "./routes/insights-editor"; import * as InsightsNew from "./routes/insights-new"; import * as Settings from "./routes/settings"; +import * as SettingsIndex from "./routes/settings-index"; import * as SettingsGeneral from "./routes/settings-general"; import * as SettingsIntegrations from "./routes/settings-integrations"; import * as SettingsModels from "./routes/settings-models"; @@ -142,7 +143,7 @@ export const routes: RouteObject[] = [ }), route("settings", Settings, { children: [ - indexRoute(SettingsModels), + indexRoute(SettingsIndex), route("models", SettingsModels), route("integrations", SettingsIntegrations), route("sandboxes", SettingsSandboxes), diff --git a/apps/fabro-web/app/routes/settings-index.tsx b/apps/fabro-web/app/routes/settings-index.tsx new file mode 100644 index 000000000..42978f70a --- /dev/null +++ b/apps/fabro-web/app/routes/settings-index.tsx @@ -0,0 +1,47 @@ +import { Fragment } from "react"; +import { Link } from "react-router"; +import { navSections } from "./settings"; + +export function meta() { + return [{ title: "Settings — Fabro" }]; +} + +export default function SettingsIndex() { + return ( +
+ {navSections.map((section, sectionIdx) => ( + + {!section.label && sectionIdx > 0 ? ( +
+ ) : null} +
+ {section.label ? ( +

+ {section.label} +

+ ) : null} +
    + {section.items.map((item) => ( +
  • + +
  • + ))} +
+
+
+ ))} +
+ ); +} diff --git a/apps/fabro-web/app/routes/settings.tsx b/apps/fabro-web/app/routes/settings.tsx index c577262c7..ca3560ff4 100644 --- a/apps/fabro-web/app/routes/settings.tsx +++ b/apps/fabro-web/app/routes/settings.tsx @@ -9,6 +9,7 @@ import { PuzzlePieceIcon, ShieldCheckIcon, } from "@heroicons/react/24/outline"; +import { Fragment } from "react"; import { Link, Outlet, useLocation, useMatches } from "react-router"; export function meta({}: any) { @@ -17,83 +18,104 @@ export function meta({}: any) { export const handle = { hideHeader: true }; -type NavItem = { - type?: "link"; +export type NavItem = { name: string; href: string; icon: typeof Cog6ToothIcon; + description: string; match: (pathname: string) => boolean; }; -type NavSection = { type: "section"; key: string; label: string }; +export type NavSection = { + key: string; + label?: string; + items: NavItem[]; +}; -type NavDivider = { type: "divider"; key: string }; - -type NavEntry = NavItem | NavSection | NavDivider; - -const navItems: NavEntry[] = [ - { type: "section", key: "general", label: "General" }, +export const navSections: NavSection[] = [ { - name: "Models", - href: "/settings/models", - icon: CpuChipIcon, - match: (p) => p === "/settings" || p.startsWith("/settings/models"), + key: "general", + label: "General", + items: [ + { + name: "Models", + href: "/settings/models", + icon: CpuChipIcon, + description: "LLM providers and credentials.", + match: (p) => p.startsWith("/settings/models"), + }, + { + name: "Integrations", + href: "/settings/integrations", + icon: PuzzlePieceIcon, + description: "Slack, GitHub, and other services.", + match: (p) => p.startsWith("/settings/integrations"), + }, + { + name: "Sandboxes", + href: "/settings/sandboxes", + icon: CubeTransparentIcon, + description: "Where workflow stages execute.", + match: (p) => p.startsWith("/settings/sandboxes"), + }, + { + name: "Security", + href: "/settings/security", + icon: ShieldCheckIcon, + description: "Authentication and network allowlist.", + match: (p) => p.startsWith("/settings/security"), + }, + { + name: "Secrets", + href: "/settings/secrets", + icon: KeyIcon, + description: "Write-only values for workflow runs.", + match: (p) => p.startsWith("/settings/secrets"), + }, + ], }, { - name: "Integrations", - href: "/settings/integrations", - icon: PuzzlePieceIcon, - match: (p) => p.startsWith("/settings/integrations"), + key: "administration", + label: "Administration", + items: [ + { + name: "Server", + href: "/settings/server", + icon: Cog6ToothIcon, + description: "URLs, listen address, scheduler.", + match: (p) => p.startsWith("/settings/server"), + }, + { + name: "Storage", + href: "/settings/storage", + icon: CircleStackIcon, + description: "Database, runs, and artifacts.", + match: (p) => p.startsWith("/settings/storage"), + }, + { + name: "Monitoring", + href: "/settings/monitoring", + icon: ChartBarSquareIcon, + description: "CPU, memory, disk, concurrency.", + match: (p) => p.startsWith("/settings/monitoring"), + }, + ], }, { - name: "Sandboxes", - href: "/settings/sandboxes", - icon: CubeTransparentIcon, - match: (p) => p.startsWith("/settings/sandboxes"), - }, - { - name: "Security", - href: "/settings/security", - icon: ShieldCheckIcon, - match: (p) => p.startsWith("/settings/security"), - }, - { - name: "Secrets", - href: "/settings/secrets", - icon: KeyIcon, - match: (p) => p.startsWith("/settings/secrets"), - }, - { type: "section", key: "administration", label: "Administration" }, - { - name: "Server", - href: "/settings/server", - icon: Cog6ToothIcon, - match: (p) => p.startsWith("/settings/server"), - }, - { - name: "Storage", - href: "/settings/storage", - icon: CircleStackIcon, - match: (p) => p.startsWith("/settings/storage"), - }, - { - name: "Monitoring", - href: "/settings/monitoring", - icon: ChartBarSquareIcon, - match: (p) => p.startsWith("/settings/monitoring"), - }, - { type: "divider", key: "after-administration" }, - { - name: "Live Events", - href: "/settings/live-events", - icon: BoltIcon, - match: (p) => p.startsWith("/settings/live-events"), + key: "diagnostics", + items: [ + { + name: "Live Events", + href: "/settings/live-events", + icon: BoltIcon, + description: "Real-time event stream.", + match: (p) => p.startsWith("/settings/live-events"), + }, + ], }, ]; -function isLink(entry: NavEntry): entry is NavItem { - return entry.type !== "divider" && entry.type !== "section"; -} +const allItems = navSections.flatMap((s) => s.items); function classNames(...classes: Array) { return classes.filter(Boolean).join(" "); @@ -103,7 +125,7 @@ export default function SettingsLayout() { const { pathname } = useLocation(); const matches = useMatches(); const currentName = - navItems.filter(isLink).find((item) => item.match(pathname))?.name ?? "Settings"; + allItems.find((item) => item.match(pathname))?.name ?? "Settings"; const fullHeight = matches.some( (m) => (m.handle as { fullHeight?: boolean } | undefined)?.fullHeight, ); @@ -118,46 +140,46 @@ export default function SettingsLayout() {