mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
feat(web): regroup settings nav under General and Administration
Group sidebar items under General and Administration section labels; default Settings landing page to Models; rename General page to Server (now at /settings/server); rename Resources to Monitoring (now at /settings/monitoring) with ChartBarSquare icon. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
67d8c5906e
commit
a508ee3d2e
6 changed files with 59 additions and 44 deletions
|
|
@ -33,9 +33,9 @@ describe("browser router", () => {
|
|||
expect(paths).not.toContain("/setup/complete");
|
||||
});
|
||||
|
||||
test("exposes the resources settings page", () => {
|
||||
test("exposes the monitoring settings page", () => {
|
||||
const paths = collectPaths(routes);
|
||||
|
||||
expect(paths).toContain("/settings/resources");
|
||||
expect(paths).toContain("/settings/monitoring");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ import * as SettingsModels from "./routes/settings-models";
|
|||
import * as SettingsSandboxes from "./routes/settings-sandboxes";
|
||||
import * as SettingsSecrets from "./routes/settings-secrets";
|
||||
import * as SettingsSecretsNew from "./routes/settings-secrets-new";
|
||||
import * as SettingsResources from "./routes/settings-resources";
|
||||
import * as SettingsMonitoring from "./routes/settings-monitoring";
|
||||
import * as SettingsSecurity from "./routes/settings-security";
|
||||
import * as SettingsStorage from "./routes/settings-storage";
|
||||
import * as SettingsLiveEvents from "./routes/settings-live-events";
|
||||
|
|
@ -142,15 +142,16 @@ export const routes: RouteObject[] = [
|
|||
}),
|
||||
route("settings", Settings, {
|
||||
children: [
|
||||
indexRoute(SettingsGeneral),
|
||||
route("integrations", SettingsIntegrations),
|
||||
indexRoute(SettingsModels),
|
||||
route("models", SettingsModels),
|
||||
route("integrations", SettingsIntegrations),
|
||||
route("sandboxes", SettingsSandboxes),
|
||||
route("security", SettingsSecurity),
|
||||
route("secrets", SettingsSecrets),
|
||||
route("secrets/new", SettingsSecretsNew),
|
||||
route("security", SettingsSecurity),
|
||||
route("server", SettingsGeneral),
|
||||
route("storage", SettingsStorage),
|
||||
route("resources", SettingsResources),
|
||||
route("monitoring", SettingsMonitoring),
|
||||
route("live-events", SettingsLiveEvents),
|
||||
],
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import {
|
|||
} from "../components/settings-panel";
|
||||
|
||||
export function meta() {
|
||||
return [{ title: "General settings — Fabro" }];
|
||||
return [{ title: "Server settings — Fabro" }];
|
||||
}
|
||||
|
||||
const DESCRIPTION = (
|
||||
|
|
|
|||
|
|
@ -21,14 +21,14 @@ mock.module("../lib/queries", () => ({
|
|||
useSystemResources: () => ({ data: systemResources }),
|
||||
}));
|
||||
|
||||
const { default: SettingsResources } = await import("./settings-resources");
|
||||
const { default: SettingsMonitoring } = await import("./settings-monitoring");
|
||||
|
||||
const mountedRenderers: TestRenderer.ReactTestRenderer[] = [];
|
||||
|
||||
function renderSettingsResources() {
|
||||
function renderSettingsMonitoring() {
|
||||
let renderer: TestRenderer.ReactTestRenderer | undefined;
|
||||
act(() => {
|
||||
renderer = TestRenderer.create(<SettingsResources />);
|
||||
renderer = TestRenderer.create(<SettingsMonitoring />);
|
||||
});
|
||||
mountedRenderers.push(renderer!);
|
||||
return renderer!;
|
||||
|
|
@ -103,7 +103,7 @@ function sampleServerSettings(maxConcurrentRuns = 8): ServerSettings {
|
|||
} as unknown as ServerSettings;
|
||||
}
|
||||
|
||||
describe("SettingsResources route", () => {
|
||||
describe("SettingsMonitoring route", () => {
|
||||
beforeEach(() => {
|
||||
teardownReactTestEnv = setupReactTestEnv();
|
||||
systemInfo = { runs: { active: 3, total: 12 } };
|
||||
|
|
@ -126,7 +126,7 @@ describe("SettingsResources route", () => {
|
|||
test("renders loaded resource data", () => {
|
||||
systemResources = sampleResources();
|
||||
|
||||
const renderer = renderSettingsResources();
|
||||
const renderer = renderSettingsMonitoring();
|
||||
const text = textContent(renderer.toJSON());
|
||||
|
||||
expect(text).toContain("18%");
|
||||
|
|
@ -148,7 +148,7 @@ describe("SettingsResources route", () => {
|
|||
},
|
||||
});
|
||||
|
||||
const renderer = renderSettingsResources();
|
||||
const renderer = renderSettingsMonitoring();
|
||||
|
||||
expect(textContent(renderer.toJSON())).toContain("Collecting sample");
|
||||
});
|
||||
|
|
@ -179,7 +179,7 @@ describe("SettingsResources route", () => {
|
|||
},
|
||||
});
|
||||
|
||||
const renderer = renderSettingsResources();
|
||||
const renderer = renderSettingsMonitoring();
|
||||
const text = textContent(renderer.toJSON());
|
||||
|
||||
expect(text).toContain("Unsupported");
|
||||
|
|
@ -192,7 +192,7 @@ describe("SettingsResources route", () => {
|
|||
notes: ["Memory is scoped to the current container."],
|
||||
});
|
||||
|
||||
const renderer = renderSettingsResources();
|
||||
const renderer = renderSettingsMonitoring();
|
||||
|
||||
expect(textContent(renderer.toJSON())).toContain(
|
||||
"Memory is scoped to the current container.",
|
||||
|
|
@ -17,13 +17,13 @@ import {
|
|||
} from "../components/settings-panel";
|
||||
|
||||
export function meta() {
|
||||
return [{ title: "Resources — Fabro" }];
|
||||
return [{ title: "Monitoring — Fabro" }];
|
||||
}
|
||||
|
||||
const DESCRIPTION =
|
||||
"Server-visible run concurrency, CPU, memory, and storage filesystem usage for this Fabro process.";
|
||||
|
||||
export default function SettingsResources() {
|
||||
export default function SettingsMonitoring() {
|
||||
const resourcesQuery = useSystemResources();
|
||||
const resources = resourcesQuery.data;
|
||||
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
import {
|
||||
BoltIcon,
|
||||
ChartBarSquareIcon,
|
||||
CircleStackIcon,
|
||||
Cog6ToothIcon,
|
||||
CpuChipIcon,
|
||||
CubeTransparentIcon,
|
||||
KeyIcon,
|
||||
PuzzlePieceIcon,
|
||||
ServerStackIcon,
|
||||
ShieldCheckIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { Link, Outlet, useLocation, useMatches } from "react-router";
|
||||
|
|
@ -25,28 +25,19 @@ type NavItem = {
|
|||
match: (pathname: string) => boolean;
|
||||
};
|
||||
|
||||
type NavSection = { type: "section"; key: string; label: string };
|
||||
|
||||
type NavDivider = { type: "divider"; key: string };
|
||||
|
||||
type NavEntry = NavItem | NavDivider;
|
||||
type NavEntry = NavItem | NavSection | NavDivider;
|
||||
|
||||
const navItems: NavEntry[] = [
|
||||
{
|
||||
name: "General",
|
||||
href: "/settings",
|
||||
icon: Cog6ToothIcon,
|
||||
match: (p) => p === "/settings",
|
||||
},
|
||||
{ type: "section", key: "general", label: "General" },
|
||||
{
|
||||
name: "Models",
|
||||
href: "/settings/models",
|
||||
icon: CpuChipIcon,
|
||||
match: (p) => p.startsWith("/settings/models"),
|
||||
},
|
||||
{
|
||||
name: "Sandboxes",
|
||||
href: "/settings/sandboxes",
|
||||
icon: CubeTransparentIcon,
|
||||
match: (p) => p.startsWith("/settings/sandboxes"),
|
||||
match: (p) => p === "/settings" || p.startsWith("/settings/models"),
|
||||
},
|
||||
{
|
||||
name: "Integrations",
|
||||
|
|
@ -55,10 +46,10 @@ const navItems: NavEntry[] = [
|
|||
match: (p) => p.startsWith("/settings/integrations"),
|
||||
},
|
||||
{
|
||||
name: "Secrets",
|
||||
href: "/settings/secrets",
|
||||
icon: KeyIcon,
|
||||
match: (p) => p.startsWith("/settings/secrets"),
|
||||
name: "Sandboxes",
|
||||
href: "/settings/sandboxes",
|
||||
icon: CubeTransparentIcon,
|
||||
match: (p) => p.startsWith("/settings/sandboxes"),
|
||||
},
|
||||
{
|
||||
name: "Security",
|
||||
|
|
@ -66,6 +57,19 @@ const navItems: NavEntry[] = [
|
|||
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",
|
||||
|
|
@ -73,12 +77,12 @@ const navItems: NavEntry[] = [
|
|||
match: (p) => p.startsWith("/settings/storage"),
|
||||
},
|
||||
{
|
||||
name: "Resources",
|
||||
href: "/settings/resources",
|
||||
icon: ServerStackIcon,
|
||||
match: (p) => p.startsWith("/settings/resources"),
|
||||
name: "Monitoring",
|
||||
href: "/settings/monitoring",
|
||||
icon: ChartBarSquareIcon,
|
||||
match: (p) => p.startsWith("/settings/monitoring"),
|
||||
},
|
||||
{ type: "divider", key: "after-storage" },
|
||||
{ type: "divider", key: "after-administration" },
|
||||
{
|
||||
name: "Live Events",
|
||||
href: "/settings/live-events",
|
||||
|
|
@ -88,7 +92,7 @@ const navItems: NavEntry[] = [
|
|||
];
|
||||
|
||||
function isLink(entry: NavEntry): entry is NavItem {
|
||||
return entry.type !== "divider";
|
||||
return entry.type !== "divider" && entry.type !== "section";
|
||||
}
|
||||
|
||||
function classNames(...classes: Array<string | false | null | undefined>) {
|
||||
|
|
@ -115,7 +119,7 @@ export default function SettingsLayout() {
|
|||
<nav className="sticky top-6">
|
||||
<ul role="list" className="flex gap-1 overflow-x-auto lg:flex-col lg:gap-0.5">
|
||||
{navItems.map((entry) => {
|
||||
if (!isLink(entry)) {
|
||||
if (entry.type === "divider") {
|
||||
return (
|
||||
<li
|
||||
key={entry.key}
|
||||
|
|
@ -125,6 +129,16 @@ export default function SettingsLayout() {
|
|||
/>
|
||||
);
|
||||
}
|
||||
if (entry.type === "section") {
|
||||
return (
|
||||
<li
|
||||
key={entry.key}
|
||||
className="hidden lg:block lg:px-2.5 lg:pt-4 lg:pb-1 lg:text-xs lg:font-medium lg:uppercase lg:tracking-wider lg:text-fg-muted first:lg:pt-0"
|
||||
>
|
||||
{entry.label}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
const current = entry.match(pathname);
|
||||
return (
|
||||
<li key={entry.name}>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue