diff --git a/apps/fabro-web/app/components/settings-panel.tsx b/apps/fabro-web/app/components/settings-panel.tsx
index ca4861cca..1fa8bcb42 100644
--- a/apps/fabro-web/app/components/settings-panel.tsx
+++ b/apps/fabro-web/app/components/settings-panel.tsx
@@ -51,6 +51,25 @@ export function Row({
);
}
+export function SettingsPageIntro({
+ description,
+ view,
+ setView,
+}: {
+ description: ReactNode;
+ view?: SettingsView;
+ setView?: (v: SettingsView) => void;
+}) {
+ return (
+
+
{description}
+ {view !== undefined && setView ? (
+
+ ) : null}
+
+ );
+}
+
export function ViewToggle({
view,
setView,
@@ -146,6 +165,59 @@ export function UrlValue({ url }: { url: string }) {
);
}
+export function ObjectStoreRows({
+ store,
+ prefix,
+}: {
+ store: ObjectStoreSettings;
+ prefix: string;
+}) {
+ const prefixRow = (
+
+ {prefix ? {prefix} : None}
+
+ );
+
+ if (store.type === "s3") {
+ return (
+ <>
+
+ s3
+
+
+ {store.bucket}
+
+
+ {store.region}
+
+ {store.endpoint ? (
+
+ {store.endpoint}
+
+ ) : null}
+ {store.path_style ? (
+
+
+
+ ) : null}
+ {prefixRow}
+ >
+ );
+ }
+
+ return (
+ <>
+
+ local
+
+
+ {store.root}
+
+ {prefixRow}
+ >
+ );
+}
+
export function ObjectStoreValue({
store,
prefix,
diff --git a/apps/fabro-web/app/router.tsx b/apps/fabro-web/app/router.tsx
index 602e3b234..4cb1e7f10 100644
--- a/apps/fabro-web/app/router.tsx
+++ b/apps/fabro-web/app/router.tsx
@@ -32,7 +32,10 @@ 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 SettingsOverview from "./routes/settings-overview";
+import * as SettingsGeneral from "./routes/settings-general";
+import * as SettingsIntegrations from "./routes/settings-integrations";
+import * as SettingsSecurity from "./routes/settings-security";
+import * as SettingsStorage from "./routes/settings-storage";
import * as SettingsLiveEvents from "./routes/settings-live-events";
import * as Profile from "./routes/profile";
import * as ProfileOverview from "./routes/profile-overview";
@@ -130,7 +133,10 @@ export const routes: RouteObject[] = [
}),
route("settings", Settings, {
children: [
- indexRoute(SettingsOverview),
+ indexRoute(SettingsGeneral),
+ route("integrations", SettingsIntegrations),
+ route("security", SettingsSecurity),
+ route("storage", SettingsStorage),
route("live-events", SettingsLiveEvents),
],
}),
diff --git a/apps/fabro-web/app/routes/settings-general.tsx b/apps/fabro-web/app/routes/settings-general.tsx
new file mode 100644
index 000000000..e76aec902
--- /dev/null
+++ b/apps/fabro-web/app/routes/settings-general.tsx
@@ -0,0 +1,104 @@
+import { useState } from "react";
+import type {
+ ServerListenSettings,
+ ServerSettings,
+} from "@qltysh/fabro-api-client";
+import { useServerSettings } from "../lib/queries";
+import { CollapsibleFile } from "../components/collapsible-file";
+import {
+ Badge,
+ Muted,
+ NumberValue,
+ Panel,
+ PanelSkeleton,
+ Row,
+ SettingsPageIntro,
+ type SettingsView,
+ Toggle,
+ UrlValue,
+} from "../components/settings-panel";
+
+export function meta() {
+ return [{ title: "General settings — Fabro" }];
+}
+
+const DESCRIPTION = (
+ <>
+ Server URLs, listen address, and scheduler limits. Edit via{" "}
+ settings.toml; changes take
+ effect on the next server restart.
+ >
+);
+
+export default function SettingsGeneral() {
+ const settingsQuery = useServerSettings();
+ const settings = settingsQuery.data;
+ const [view, setView] = useState("settings");
+
+ if (!settings) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+ {view === "settings" ? (
+
+ ) : (
+
+ )}
+
+ );
+}
+
+function ServerPanel({ settings }: { settings: ServerSettings }) {
+ const { listen, web, api, scheduler } = settings.server;
+ return (
+
+
+ {web.enabled ? : }
+
+
+ {api.url ? : Same origin}
+
+
+
+
+
+
+
+
+ );
+}
+
+function ListenValue({ listen }: { listen: ServerListenSettings }) {
+ if (listen.type === "tcp") {
+ return (
+
+ tcp
+
+ {listen.address}
+
+
+ );
+ }
+ return (
+
+ unix
+
+ {listen.path}
+
+
+ );
+}
diff --git a/apps/fabro-web/app/routes/settings-integrations.tsx b/apps/fabro-web/app/routes/settings-integrations.tsx
new file mode 100644
index 000000000..cd81a2852
--- /dev/null
+++ b/apps/fabro-web/app/routes/settings-integrations.tsx
@@ -0,0 +1,71 @@
+import type { ServerSettings } from "@qltysh/fabro-api-client";
+import { useServerSettings } from "../lib/queries";
+import {
+ Panel,
+ PanelSkeleton,
+ Row,
+ SettingsPageIntro,
+ Toggle,
+} from "../components/settings-panel";
+
+export function meta() {
+ return [{ title: "Integrations — Fabro" }];
+}
+
+const DESCRIPTION = (
+ <>
+ External services connected to this server. Edit via{" "}
+ settings.toml; changes take
+ effect on the next server restart.
+ >
+);
+
+export default function SettingsIntegrations() {
+ const settingsQuery = useServerSettings();
+ const settings = settingsQuery.data;
+
+ return (
+
+ );
+}
+
+function IntegrationsPanel({ settings }: { settings: ServerSettings }) {
+ const { integrations } = settings.server;
+ return (
+
+
+
+
+
+ );
+}
+
+function IntegrationValue({
+ enabled,
+ detail,
+}: {
+ enabled: boolean;
+ detail?: string;
+}) {
+ if (!enabled) return ;
+ return (
+
+
+ {detail ? (
+ {detail}
+ ) : null}
+
+ );
+}
diff --git a/apps/fabro-web/app/routes/settings-overview.tsx b/apps/fabro-web/app/routes/settings-overview.tsx
deleted file mode 100644
index cdab04d34..000000000
--- a/apps/fabro-web/app/routes/settings-overview.tsx
+++ /dev/null
@@ -1,221 +0,0 @@
-import { useState } from "react";
-import type {
- ServerListenSettings,
- ServerSettings,
-} from "@qltysh/fabro-api-client";
-import { useServerSettings } from "../lib/queries";
-import { CollapsibleFile } from "../components/collapsible-file";
-import {
- Badge,
- Count,
- Mono,
- Muted,
- NumberValue,
- ObjectStoreValue,
- Panel,
- PanelSkeleton,
- Row,
- type SettingsView,
- Toggle,
- UrlValue,
- UsernameList,
- ViewToggle,
- plural,
-} from "../components/settings-panel";
-
-export default function SettingsOverview() {
- const settingsQuery = useServerSettings();
- const settings = settingsQuery.data;
- const [view, setView] = useState("settings");
-
- if (!settings) {
- return (
-
- );
- }
-
- return (
-
-
- {view === "settings" ? (
- <>
-
-
-
-
- >
- ) : (
-
- )}
-
- );
-}
-
-function PageIntro({
- view,
- setView,
-}: {
- view: SettingsView;
- setView: (v: SettingsView) => void;
-}) {
- return (
-
-
- Snapshot of the server configuration. Edit via{" "}
- settings.toml; changes take
- effect on the next server restart.
-
-
-
- );
-}
-
-function ServerPanel({ settings }: { settings: ServerSettings }) {
- const { listen, web, api, scheduler } = settings.server;
- return (
-
-
- {web.enabled ? : }
-
-
- {api.url ? : Same origin}
-
-
-
-
-
-
-
-
- );
-}
-
-function DataPanel({ settings }: { settings: ServerSettings }) {
- const { storage, slatedb, artifacts } = settings.server;
- return (
-
-
- {storage.root}
-
-
-
-
-
-
-
-
- );
-}
-
-function SecurityPanel({ settings }: { settings: ServerSettings }) {
- const { auth, ip_allowlist } = settings.server;
- const githubUsers = auth.github.allowed_usernames;
- return (
-
-
- {auth.methods.length === 0 ? (
- None configured
- ) : (
-
- {auth.methods.map((m) => (
- {m}
- ))}
-
- )}
-
-
- {githubUsers.length === 0 ? (
- Anyone
- ) : (
-
- )}
-
-
- 0
- ? `· ${ip_allowlist.trusted_proxy_count} trusted ${plural(ip_allowlist.trusted_proxy_count, "proxy", "proxies")}`
- : undefined
- }
- />
-
-
- );
-}
-
-function IntegrationsPanel({ settings }: { settings: ServerSettings }) {
- const { integrations } = settings.server;
- return (
-
-
-
-
-
- );
-}
-
-function ListenValue({ listen }: { listen: ServerListenSettings }) {
- if (listen.type === "tcp") {
- return (
-
- tcp
-
- {listen.address}
-
-
- );
- }
- return (
-
- unix
-
- {listen.path}
-
-
- );
-}
-
-function IntegrationValue({
- enabled,
- detail,
-}: {
- enabled: boolean;
- detail?: string;
-}) {
- if (!enabled) return ;
- return (
-
-
- {detail ? (
- {detail}
- ) : null}
-
- );
-}
diff --git a/apps/fabro-web/app/routes/settings-security.tsx b/apps/fabro-web/app/routes/settings-security.tsx
new file mode 100644
index 000000000..fbcedd8bd
--- /dev/null
+++ b/apps/fabro-web/app/routes/settings-security.tsx
@@ -0,0 +1,79 @@
+import type { ServerSettings } from "@qltysh/fabro-api-client";
+import { useServerSettings } from "../lib/queries";
+import {
+ Badge,
+ Count,
+ Muted,
+ Panel,
+ PanelSkeleton,
+ Row,
+ SettingsPageIntro,
+ UsernameList,
+ plural,
+} from "../components/settings-panel";
+
+export function meta() {
+ return [{ title: "Security — Fabro" }];
+}
+
+const DESCRIPTION = (
+ <>
+ Authentication methods and network allowlist. Edit via{" "}
+ settings.toml; changes take
+ effect on the next server restart.
+ >
+);
+
+export default function SettingsSecurity() {
+ const settingsQuery = useServerSettings();
+ const settings = settingsQuery.data;
+
+ return (
+
+ );
+}
+
+function SecurityPanel({ settings }: { settings: ServerSettings }) {
+ const { auth, ip_allowlist } = settings.server;
+ const githubUsers = auth.github.allowed_usernames;
+ return (
+
+
+ {auth.methods.length === 0 ? (
+ None configured
+ ) : (
+
+ {auth.methods.map((m) => (
+ {m}
+ ))}
+
+ )}
+
+
+ {githubUsers.length === 0 ? (
+ Anyone
+ ) : (
+
+ )}
+
+
+ 0
+ ? `· ${ip_allowlist.trusted_proxy_count} trusted ${plural(ip_allowlist.trusted_proxy_count, "proxy", "proxies")}`
+ : undefined
+ }
+ />
+
+
+ );
+}
diff --git a/apps/fabro-web/app/routes/settings-storage.tsx b/apps/fabro-web/app/routes/settings-storage.tsx
new file mode 100644
index 000000000..78f3325b0
--- /dev/null
+++ b/apps/fabro-web/app/routes/settings-storage.tsx
@@ -0,0 +1,76 @@
+import type { ServerSettings } from "@qltysh/fabro-api-client";
+import { useServerSettings } from "../lib/queries";
+import {
+ Mono,
+ ObjectStoreRows,
+ Panel,
+ PanelSkeleton,
+ Row,
+ SettingsPageIntro,
+} from "../components/settings-panel";
+
+export function meta() {
+ return [{ title: "Storage — Fabro" }];
+}
+
+const DESCRIPTION = (
+ <>
+ Filesystem and object store locations for run state, the embedded database,
+ and artifacts. Edit via{" "}
+ settings.toml; changes take
+ effect on the next server restart.
+ >
+);
+
+export default function SettingsStorage() {
+ const settingsQuery = useServerSettings();
+ const settings = settingsQuery.data;
+
+ return (
+
+
+ {settings ? (
+ <>
+
+
+
+ >
+ ) : (
+ <>
+
+
+
+ >
+ )}
+
+ );
+}
+
+function StorageRootPanel({ settings }: { settings: ServerSettings }) {
+ const { storage } = settings.server;
+ return (
+
+
+ {storage.root}
+
+
+ );
+}
+
+function SlateDbPanel({ settings }: { settings: ServerSettings }) {
+ const { slatedb } = settings.server;
+ return (
+
+
+
+ );
+}
+
+function ArtifactsPanel({ settings }: { settings: ServerSettings }) {
+ const { artifacts } = settings.server;
+ return (
+
+
+
+ );
+}
diff --git a/apps/fabro-web/app/routes/settings.tsx b/apps/fabro-web/app/routes/settings.tsx
index 1173d16f4..eef4be32e 100644
--- a/apps/fabro-web/app/routes/settings.tsx
+++ b/apps/fabro-web/app/routes/settings.tsx
@@ -1,4 +1,10 @@
-import { BoltIcon, Cog6ToothIcon } from "@heroicons/react/24/outline";
+import {
+ BoltIcon,
+ CircleStackIcon,
+ Cog6ToothIcon,
+ PuzzlePieceIcon,
+ ShieldCheckIcon,
+} from "@heroicons/react/24/outline";
import { Link, Outlet, useLocation, useMatches } from "react-router";
export function meta({}: any) {
@@ -8,19 +14,43 @@ export function meta({}: any) {
export const handle = { hideHeader: true };
type NavItem = {
+ type?: "link";
name: string;
href: string;
icon: typeof Cog6ToothIcon;
match: (pathname: string) => boolean;
};
-const navItems: NavItem[] = [
+type NavDivider = { type: "divider"; key: string };
+
+type NavEntry = NavItem | NavDivider;
+
+const navItems: NavEntry[] = [
{
- name: "Settings",
+ name: "General",
href: "/settings",
icon: Cog6ToothIcon,
match: (p) => p === "/settings",
},
+ {
+ name: "Integrations",
+ href: "/settings/integrations",
+ icon: PuzzlePieceIcon,
+ match: (p) => p.startsWith("/settings/integrations"),
+ },
+ {
+ name: "Security",
+ href: "/settings/security",
+ icon: ShieldCheckIcon,
+ match: (p) => p.startsWith("/settings/security"),
+ },
+ {
+ name: "Storage",
+ href: "/settings/storage",
+ icon: CircleStackIcon,
+ match: (p) => p.startsWith("/settings/storage"),
+ },
+ { type: "divider", key: "after-storage" },
{
name: "Live Events",
href: "/settings/live-events",
@@ -29,6 +59,10 @@ const navItems: NavItem[] = [
},
];
+function isLink(entry: NavEntry): entry is NavItem {
+ return entry.type !== "divider";
+}
+
function classNames(...classes: Array) {
return classes.filter(Boolean).join(" ");
}
@@ -36,7 +70,8 @@ function classNames(...classes: Array) {
export default function SettingsLayout() {
const { pathname } = useLocation();
const matches = useMatches();
- const currentName = navItems.find((item) => item.match(pathname))?.name ?? "Settings";
+ const currentName =
+ navItems.filter(isLink).find((item) => item.match(pathname))?.name ?? "Settings";
const fullHeight = matches.some(
(m) => (m.handle as { fullHeight?: boolean } | undefined)?.fullHeight,
);
@@ -51,12 +86,22 @@ export default function SettingsLayout() {