flagon-io/g1t

public

Git for AI scale: a forge for thousands of agents working on the same code at once.

Commit

Project settings move into the sidebar, grouped, with a heading per page

syntaqxcommitted Parentbee6db5Browse files
12 files+122−630/12 viewed
+30−0
1+import { useLocation } from "react-router";
2+
3+/** Each of a project's settings pages: its name and what it is for. */
4+const PAGES: Record<string, { title: string; about: string }> = {
5+ "": { title: "General", about: "The project's name, where its code lives, and what it is for." },
6+ deployments: { title: "Deployments", about: "How the project builds, where it runs, and its previews." },
7+ domains: { title: "Domains", about: "Addresses of your own for the production deployment." },
8+ dependencies: { title: "Dependencies", about: "What the project relies on and what relies on it." },
9+ agents: { title: "Agents", about: "How g1t's agents pick up work here, and what they read first." },
10+ guardrails: { title: "Guardrails", about: "What agents may reach, run and spend while they work here." },
11+ repository: { title: "Repository", about: "Branches, protection, merging and who can see the code." },
12+ secrets: { title: "Secrets and variables", about: "Values workflows, builds and deployments read at run time." },
13+ webhooks: { title: "Webhooks", about: "Addresses g1t calls when something happens in the project." },
14+};
15+
16+/**
17+ * The heading of a project's settings page, named from the address. The
18+ * pages themselves are listed in the sidebar while you are in settings.
19+ */
20+export function RepoSettingsHeading({ base }: { base: string }) {
21+ const { pathname } = useLocation();
22+ const section = pathname.slice(`${base}/settings`.length).replace(/^\//, "").split("/")[0] ?? "";
23+ const page = PAGES[section] ?? PAGES[""]!;
24+ return (
25+ <header className="mb-8 border-b border-line pb-5">
26+ <h1 className="text-lg font-semibold tracking-tight">{page.title}</h1>
27+ <p className="mt-1 text-sm text-muted">{page.about}</p>
28+ </header>
29+ );
30+}
+0−41
1−import { Bot, GitBranch, Globe, Lock, Network, Rocket, Settings, ShieldCheck, Webhook } from "lucide-react";
2−
3−import { TabLink } from "./ui";
4−
5−/**
6− * A project's settings, and its repository's: what is about running it
7− * first, then what is about the code.
8− */
9−export function RepoSettingsTabs({ base }: { base: string }) {
10− return (
11− <nav className="mb-8 flex gap-x-6 border-b border-line">
12− <TabLink to={`${base}/settings`} end icon={<Settings size={15} />}>
13− General
14− </TabLink>
15− <TabLink to={`${base}/settings/deployments`} icon={<Rocket size={15} />}>
16− Deployments
17− </TabLink>
18− <TabLink to={`${base}/settings/domains`} icon={<Globe size={15} />}>
19− Domains
20− </TabLink>
21− <TabLink to={`${base}/settings/dependencies`} icon={<Network size={15} />}>
22− Dependencies
23− </TabLink>
24− <TabLink to={`${base}/settings/agents`} icon={<Bot size={15} />}>
25− Agents
26− </TabLink>
27− <TabLink to={`${base}/settings/guardrails`} icon={<ShieldCheck size={15} />}>
28− Guardrails
29− </TabLink>
30− <TabLink to={`${base}/settings/secrets`} icon={<Lock size={15} />}>
31− Secrets and variables
32− </TabLink>
33− <TabLink to={`${base}/settings/repository`} icon={<GitBranch size={15} />}>
34− Repository
35− </TabLink>
36− <TabLink to={`${base}/settings/webhooks`} icon={<Webhook size={15} />}>
37− Webhooks
38− </TabLink>
39− </nav>
40− );
41−}
+74−4
3232 Settings,
3333 Users,
3434 Webhook,
35+ Globe,
36+ GitBranch,
3537 PlayCircle,
3638 Bot,
3739 Brain,
364366 }
365367
366368 /** A workspace's settings pages, which the sidebar slides over to. */
367−const SETTINGS_PAGE = /^\/([^/]+)\/-\/(settings|people|tokens|billing|integrations|webhooks|secrets|audit)(\/|$)/;
369+const SETTINGS_PAGE = /^\/([^/]+)\/-\/(settings|people|tokens|billing|integrations|webhooks|secrets|guardrails|audit)(\/|$)/;
370+/** A project's settings pages, which the project's menu gives way to. */
371+const REPO_SETTINGS_PAGE = /^\/([^/]+)\/([^/-][^/]*)\/settings(\/|$)/;
368372
369373 /**
370374 * The sidebar's menus are two layers, the way a phone pushes a screen: the
526530 );
527531 }
528532
533+/**
534+ * A project's settings, as the sidebar shows them in place of the project's
535+ * menu: what is about running it, then its agents, then its code and who
536+ * can reach it. The way back leads to the project.
537+ */
538+function RepoSettingsMenu({ repo, open }: { repo: MenuRepo; open: boolean }) {
539+ const base = `/${repo.namespace}/${repo.name}`;
540+ return (
541+ <nav aria-label={`${repo.namespace}/${repo.name} settings`} inert={!open} className={PANEL}>
542+ <Link
543+ to={base}
544+ className="group mt-3 flex h-8 items-center gap-2 rounded-md px-2 text-[0.8125rem] text-muted transition-colors hover:bg-raised/60 hover:text-fg"
545+ >
546+ <ArrowLeft size={15} className="text-faint transition-transform group-hover:-translate-x-0.5 group-hover:text-muted" />
547+ <span className="min-w-0 truncate font-mono">
548+ <span className="text-faint">{repo.namespace}/</span>
549+ {repo.name}
550+ </span>
551+ </Link>
552+ <SidebarGroup title="Settings">
553+ <SidebarLink to={`${base}/settings`} end icon={<Settings size={15} />}>
554+ General
555+ </SidebarLink>
556+ </SidebarGroup>
557+ <SidebarGroup title="Running">
558+ <SidebarLink to={`${base}/settings/deployments`} icon={<Rocket size={15} />}>
559+ Deployments
560+ </SidebarLink>
561+ <SidebarLink to={`${base}/settings/domains`} icon={<Globe size={15} />}>
562+ Domains
563+ </SidebarLink>
564+ <SidebarLink to={`${base}/settings/dependencies`} icon={<Network size={15} />}>
565+ Dependencies
566+ </SidebarLink>
567+ </SidebarGroup>
568+ <SidebarGroup title="Agents">
569+ <SidebarLink to={`${base}/settings/agents`} icon={<Bot size={15} />}>
570+ Agents
571+ </SidebarLink>
572+ <SidebarLink to={`${base}/settings/guardrails`} icon={<ShieldCheck size={15} />}>
573+ Guardrails
574+ </SidebarLink>
575+ </SidebarGroup>
576+ <SidebarGroup title="Code and access">
577+ <SidebarLink to={`${base}/settings/repository`} icon={<GitBranch size={15} />}>
578+ Repository
579+ </SidebarLink>
580+ <SidebarLink to={`${base}/settings/secrets`} icon={<Lock size={15} />}>
581+ Secrets and variables
582+ </SidebarLink>
583+ <SidebarLink to={`${base}/settings/webhooks`} icon={<Webhook size={15} />}>
584+ Webhooks
585+ </SidebarLink>
586+ </SidebarGroup>
587+ </nav>
588+ );
589+}
590+
529591 /** Your own settings, as the sidebar shows them on the settings page. */
530592 function AccountSettingsMenu({ open }: { open: boolean }) {
531593 const { hash } = useLocation();
576638 const repoPath = /^\/([^/]+)\/([^/-][^/]*)(\/|$)/.exec(target);
577639 const reserved = new Set(["settings", "explore", "search", "new", "u", "pricing", "avatars", "workspaces", "login", "logout", "register", "verify", "forgot", "reset", "device", "oauth"]);
578640 const inRepo = repoPath != null && !reserved.has(repoPath[1]) && repoPath[2] !== "-";
641+ // A project's settings take its menu over in turn.
642+ const inRepoSettings = inRepo && REPO_SETTINGS_PAGE.test(target);
579643 const away = inSettings || inAccount || inRepo;
580644 // The repository the menu is for: the one loaded if it is the one being
581645 // gone to, else what the address says, at once.
590654 : null;
591655 // What sits on the far side of the track. Kept while sliding back, so it
592656 // does not vanish on the way out.
593− const side = useRef<"workspace" | "account" | "repo">("workspace");
657+ const side = useRef<"workspace" | "account" | "repo" | "repo-settings">("workspace");
594658 if (inAccount) side.current = "account";
595659 else if (inSettings) side.current = "workspace";
660+ else if (inRepoSettings) side.current = "repo-settings";
596661 else if (inRepo) side.current = "repo";
597662 // The repository last shown, kept for the slide back.
598663 const shown = useRef<MenuRepo | null>(menuRepo);
599664 if (menuRepo) shown.current = menuRepo;
600665 // Moving between two of the far-side menus (settings to a repository,
601666 // one repository to another) crossfades in place instead of sliding.
602− const detailKey = side.current === "repo" ? `repo:${shown.current?.namespace}/${shown.current?.name}` : side.current;
667+ const detailKey =
668+ side.current === "repo" || side.current === "repo-settings"
669+ ? `${side.current}:${shown.current?.namespace}/${shown.current?.name}`
670+ : side.current;
603671 const lastAway = useRef(away);
604672 const lastKey = useRef(detailKey);
605673 const swapped = lastAway.current && away && lastKey.current !== detailKey;
713781 </div>
714782 <div className={`${LAYER} ${away ? "translate-x-0 opacity-100" : "pointer-events-none translate-x-full opacity-0"}`}>
715783 <div key={detailKey} className={`h-full ${swapped ? "animate-[g1t-swap_220ms_ease-out]" : ""}`}>
716− {side.current === "repo" && shown.current ? (
784+ {side.current === "repo-settings" && shown.current ? (
785+ <RepoSettingsMenu repo={shown.current} open={inRepoSettings} />
786+ ) : side.current === "repo" && shown.current ? (
717787 <RepoMenu
718788 repo={shown.current}
719789 isPrivate={shell.repos.some((repo) => sameRepo(repo, shown.current) && repo.isPrivate)}
+2−2
11 import type { Route } from "./+types/secrets";
22 import { page } from "../../lib/meta";
3−import { RepoSettingsTabs } from "../../components/repo-settings-tabs";
3+import { RepoSettingsHeading } from "../../components/repo-settings-heading";
44 import { SecretsPanel } from "../../components/secrets";
55 import { actOnSecrets, loadSecrets } from "../../lib/secrets.server";
66 import { assertSameOrigin, getViewer, requireUser, roleIn } from "../../lib/session.server";
2727 export default function RepoSecrets({ loaderData, actionData, params }: Route.ComponentProps) {
2828 return (
2929 <div>
30− <RepoSettingsTabs base={`/${params.owner}/${params.repo}`} />
30+ <RepoSettingsHeading base={`/${params.owner}/${params.repo}`} />
3131 <SecretsPanel data={loaderData} action={actionData} scope="project" manage />
3232 </div>
3333 );
+2−2
66
77 import type { Route } from "./+types/settings-agents";
88 import { page } from "../../lib/meta";
9−import { RepoSettingsTabs } from "../../components/repo-settings-tabs";
9+import { RepoSettingsHeading } from "../../components/repo-settings-heading";
1010 import { Button, ErrorText, Field, Input, TimeAgo } from "../../components/ui";
1111 import { work } from "../../lib/services.server";
1212 import { assertSameOrigin, getViewer, requireUser, roleIn, unwrap } from "../../lib/session.server";
4848 const base = `/${params.owner}/${params.repo}`;
4949 return (
5050 <div className="max-w-4xl">
51− <RepoSettingsTabs base={base} />
51+ <RepoSettingsHeading base={base} />
5252 <div className="min-h-6">
5353 {actionData?.notice && <p className="text-sm text-accent">{actionData.notice}</p>}
5454 <ErrorText>{actionData?.error ?? null}</ErrorText>
+2−2
55
66 import type { Route } from "./+types/settings-dependencies";
77 import { page } from "../../lib/meta";
8−import { RepoSettingsTabs } from "../../components/repo-settings-tabs";
8+import { RepoSettingsHeading } from "../../components/repo-settings-heading";
99 import { Avatar, Button, EmptyState, ErrorText, Field, Input } from "../../components/ui";
1010 import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../components/ui/select";
1111 import { projects } from "../../lib/services.server";
8181 const choices = all.filter((project) => !taken.has(project.slug));
8282 return (
8383 <div className="max-w-4xl">
84− <RepoSettingsTabs base={base} />
84+ <RepoSettingsHeading base={base} />
8585 <header>
8686 <h2 className="flex items-center gap-2 text-sm font-medium">
8787 <Network size={15} className="text-accent" />
+2−2
22
33 import type { Route } from "./+types/settings-deployments";
44 import { page } from "../../lib/meta";
5−import { RepoSettingsTabs } from "../../components/repo-settings-tabs";
5+import { RepoSettingsHeading } from "../../components/repo-settings-heading";
66 import { Button, ErrorText, Field, Input } from "../../components/ui";
77 import { SwitchCard } from "../../components/ui/switch";
88 import { deployments } from "../../lib/services.server";
5656 const base = `/${params.owner}/${params.repo}`;
5757 return (
5858 <div className="max-w-4xl">
59− <RepoSettingsTabs base={base} />
59+ <RepoSettingsHeading base={base} />
6060 <div className="min-h-6">
6161 {actionData && "notice" in actionData && <p className="text-sm text-accent">{actionData.notice}</p>}
6262 <ErrorText>{actionData && "error" in actionData ? actionData.error : null}</ErrorText>
+2−2
66
77 import type { Route } from "./+types/settings-domains";
88 import { page } from "../../lib/meta";
9−import { RepoSettingsTabs } from "../../components/repo-settings-tabs";
9+import { RepoSettingsHeading } from "../../components/repo-settings-heading";
1010 import { Button, CopyLine, EmptyState, ErrorText, Field, Input } from "../../components/ui";
1111 import {
1212 AlertDialog,
113113
114114 return (
115115 <div className="max-w-4xl">
116− <RepoSettingsTabs base={base} />
116+ <RepoSettingsHeading base={base} />
117117 <div className="min-h-6">
118118 {actionData && "notice" in actionData && <p className="text-sm text-accent">{actionData.notice}</p>}
119119 <ErrorText>{actionData && "error" in actionData ? actionData.error : null}</ErrorText>
+2−2
44
55 import type { Route } from "./+types/settings-guardrails";
66 import { GuardrailsForm } from "../../components/guardrails";
7−import { RepoSettingsTabs } from "../../components/repo-settings-tabs";
7+import { RepoSettingsHeading } from "../../components/repo-settings-heading";
88 import { settingsFromForm } from "../../lib/guardrails";
99 import { page } from "../../lib/meta";
1010 import { guardrails } from "../../lib/services.server";
4242 const base = `/${params.owner}/${params.repo}`;
4343 return (
4444 <>
45− <RepoSettingsTabs base={base} />
45+ <RepoSettingsHeading base={base} />
4646 <p className="mb-8 max-w-3xl text-sm text-muted">
4747 What g1t's agents, checks and merge queue may do in this project's sandboxes. Anything left as the
4848 workspace's follows the{" "}
+2−2
22 import type { ReactNode } from "react";
33 import { Form, data, useNavigation } from "react-router";
44
5−import { RepoSettingsTabs } from "../../components/repo-settings-tabs";
5+import { RepoSettingsHeading } from "../../components/repo-settings-heading";
66 import type { Route } from "./+types/settings-repository";
77 import { page } from "../../lib/meta";
88 import { Button, ErrorText, Field, Input, TimeAgo } from "../../components/ui";
155155 const branch = repo.defaultBranch;
156156 return (
157157 <>
158− <RepoSettingsTabs base={`/${repo.namespace}/${repo.name}`} />
158+ <RepoSettingsHeading base={`/${repo.namespace}/${repo.name}`} />
159159 <Form method="post" className="max-w-4xl space-y-8">
160160 <Section title="General" about="What the repository is and who can see it.">
161161 <Field label="Description">
+2−2
33
44 import type { Route } from "./+types/settings";
55 import { page } from "../../lib/meta";
6−import { RepoSettingsTabs } from "../../components/repo-settings-tabs";
6+import { RepoSettingsHeading } from "../../components/repo-settings-heading";
77 import { Button, ErrorText, Field, Input, TimeAgo } from "../../components/ui";
88 import { projects } from "../../lib/services.server";
99 import { assertSameOrigin, getViewer, requireUser, roleIn, unwrap } from "../../lib/session.server";
3838 const source = project.source.kind === "hosted" ? project.source : null;
3939 return (
4040 <div className="max-w-4xl">
41− <RepoSettingsTabs base={base} />
41+ <RepoSettingsHeading base={base} />
4242 <Form method="post" className="space-y-8">
4343 <section className="space-y-4">
4444 <h2 className="flex items-center gap-2 text-sm font-medium">
+2−2
11 import type { Route } from "./+types/webhooks";
22 import { page } from "../../lib/meta";
3−import { RepoSettingsTabs } from "../../components/repo-settings-tabs";
3+import { RepoSettingsHeading } from "../../components/repo-settings-heading";
44 import { WebhooksPanel } from "../../components/webhooks";
55 import { actOnWebhooks, loadWebhooks } from "../../lib/webhooks.server";
66 import { assertSameOrigin, getViewer, requireUser, roleIn } from "../../lib/session.server";
2929 export default function RepoWebhooks({ loaderData, actionData, params }: Route.ComponentProps) {
3030 return (
3131 <div>
32− <RepoSettingsTabs base={`/${params.owner}/${params.repo}`} />
32+ <RepoSettingsHeading base={`/${params.owner}/${params.repo}`} />
3333 <WebhooksPanel data={loaderData} action={actionData} manage scope={`${params.owner}/${params.repo}`} />
3434 </div>
3535 );