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 ( +
+ + {settings ? : } +
+ ); +} + +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 ( +
+ + {settings ? : } +
+ ); +} + +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() {