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) => (
+ -
+
+
+
+
{item.name}
+
{item.description}
+
+
+
+ ))}
+
+
+
+ ))}
+
+ );
+}
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() {