Skip to content

g1t/apps/web/app/routes/repo/settings.tsx

208 lines10,024 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Merge branch 'projects-kind-and-links'1import { Box, Code2, GitBranch, Link2, Rocket } from "lucide-react";
A project is an app or a library: libraries show their package and how to ship a release, not production2import { useState } from "react";
Merge branch 'worktree-agent-a985c01f43538af01'3import { Form, Link } from "react-router";
Agents as a team: lifecycle, merge queue, billing and a new shell4
5import type { Route } from "./+types/settings";
Social cards for every page: og.g1t.sh6import { page } from "../../lib/meta";
Merge branch 'projects-kind-and-links'7import { KindFields, LinkFields } from "../../components/project-about";
Project settings move into the sidebar, grouped, with a heading per page8import { RepoSettingsHeading } from "../../components/repo-settings-heading";
Merge branch 'worktree-agent-a985c01f43538af01'9import { ErrorText, Field, Input, SubmitButton, TimeAgo } from "../../components/ui";
Merge branch 'projects-kind-and-links'10import { KIND_CHOICE_SETS, type KindChoice, choiceOf, linksFromForm, neverDeploys } from "../../lib/project-kind";
Project settings say who created the project, by name11import { deployments, identity, projects } from "../../lib/services.server";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look12import { assertSameOrigin, getViewer, requireUser, unwrap } from "../../lib/session.server";
13import { requireCapability, requireInsider } from "../../lib/access.server";
Agents as a team: lifecycle, merge queue, billing and a new shell14
Social cards for every page: og.g1t.sh15export function meta({ params, ...args }: Route.MetaArgs) {
16 return page(args, { title: `Settings · ${params.owner}/${params.repo} · g1t` });
Agents as a team: lifecycle, merge queue, billing and a new shell17}
18
19export async function loader({ params, context }: Route.LoaderArgs) {
20 const viewer = getViewer(context);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look21 // Maintain and up; to anyone without a role here the page does not exist.
22 await requireInsider(context, params, "manage_settings");
A project is an app or a library: libraries show their package and how to ship a release, not production23 const [project, deploys] = await Promise.all([
24 projects.get(params.owner, params.repo, viewer),
25 // Whether Deployments are on: needs Admin, so unknown (null) below it.
26 deployments.settings({ workspace: params.owner, slug: params.repo }, viewer).catch(() => null),
27 ]);
Project settings no longer print the creator's raw id28 const found = unwrap(project);
Project settings say who created the project, by name29 // Who made it is kept as an id: named by identity, or left out if it cannot say.
30 const names = await identity.usernames([found.createdBy]).catch(() => ({}) as Record<string, string>);
31 return {
32 project: found,
33 deploymentsOn: deploys?.ok ? deploys.value.enabled : null,
34 mine: viewer?.id === found.createdBy,
35 creator: names[found.createdBy] ?? null,
36 };
Agents as a team: lifecycle, merge queue, billing and a new shell37}
38
39export async function action({ request, params, context }: Route.ActionArgs) {
40 assertSameOrigin(request);
41 const user = requireUser(context, request);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look42 await requireCapability(context, params, "manage_settings");
Agents as a team: lifecycle, merge queue, billing and a new shell43 const form = await request.formData();
Merge branch 'projects-kind-and-links'44 const choice = form.get("choice") as KindChoice | null;
45 const set = choice && choice in KIND_CHOICE_SETS ? KIND_CHOICE_SETS[choice] : null;
Projects: what a workspace builds and runs, first on every page46 const saved = await projects.update(user, params.owner, params.repo, {
47 name: String(form.get("name") ?? ""),
Fast pages, required checks on the branch, self-hosted runners, honest incidents48 // Blank, or the reset, and it follows the repository's description again.
49 description: form.get("inherit") === "description" ? null : String(form.get("description") ?? ""),
Projects: what a workspace builds and runs, first on every page50 rootDir: String(form.get("rootDir") ?? ""),
Merge branch 'projects-kind-and-links'51 ...(set ?? {}),
52 // Production's address is asked for beside Deployed elsewhere, and kept otherwise.
53 ...(form.has("productionUrl") ? { productionUrl: String(form.get("productionUrl")) } : {}),
54 homepage: String(form.get("homepage") ?? ""),
55 docsUrl: String(form.get("docsUrl") ?? ""),
56 links: linksFromForm(form),
Agents as a team: lifecycle, merge queue, billing and a new shell57 });
Projects: what a workspace builds and runs, first on every page58 return saved.ok ? { saved: true as const } : { error: saved.error.message };
Agents as a team: lifecycle, merge queue, billing and a new shell59}
60
Projects: what a workspace builds and runs, first on every page61export default function ProjectSettings({ loaderData, actionData, params }: Route.ComponentProps) {
Project settings say who created the project, by name62 const { project, deploymentsOn, mine, creator } = loaderData;
Merge branch 'projects-kind-and-links'63 const [choice, setChoice] = useState<KindChoice | null>(choiceOf(project.setting));
64 // Something that never deploys, while Deployments are on, is refused: they are turned off first.
65 const stops = choice === "library" || choice === "tool" || choice === "other";
66 const blocked = stops && !neverDeploys(project) && deploymentsOn === true;
Projects: what a workspace builds and runs, first on every page67 const base = `/${params.owner}/${params.repo}`;
68 const source = project.source.kind === "hosted" ? project.source : null;
Agents as a team: lifecycle, merge queue, billing and a new shell69 return (
Projects: what a workspace builds and runs, first on every page70 <div className="max-w-4xl">
Project settings move into the sidebar, grouped, with a heading per page71 <RepoSettingsHeading base={base} />
Projects: what a workspace builds and runs, first on every page72 <Form method="post" className="space-y-8">
Merge branch 'worktree-agent-a985c01f43538af01'73 {/* Enter in a field saves: the form's first submit button is the one
74 Enter presses, and otherwise it would be the description reset. */}
75 <button type="submit" name="intent" value="save" tabIndex={-1} aria-hidden="true" className="absolute size-0 overflow-hidden opacity-0" />
Projects: what a workspace builds and runs, first on every page76 <section className="space-y-4">
77 <h2 className="flex items-center gap-2 text-sm font-medium">
78 <Box size={15} className="text-accent" />
79 Project
80 </h2>
81 <div className="grid gap-4 md:grid-cols-2">
82 <Field label="Name" hint={`Its address stays g1t.sh/${project.workspace}/${project.slug}.`}>
83 <Input name="name" defaultValue={project.name} required maxLength={100} />
84 </Field>
Fast pages, required checks on the branch, self-hosted runners, honest incidents85 <div>
86 <Field
87 label="Description"
88 hint={
89 project.descriptionInherited
90 ? "Follows the repository's description. Write one to give the project its own."
91 : "The project's own. Empty follows the repository's description."
92 }
93 >
94 <Input
95 key={`${project.descriptionInherited}:${project.description ?? ""}`}
96 name="description"
97 defaultValue={project.descriptionInherited ? "" : (project.description ?? "")}
98 maxLength={200}
99 placeholder={(project.descriptionInherited && project.description) || "What it is, in a line"}
100 />
101 </Field>
102 {!project.descriptionInherited && (
Merge branch 'worktree-agent-a985c01f43538af01'103 <SubmitButton
Fast pages, required checks on the branch, self-hosted runners, honest incidents104 name="inherit"
105 value="description"
Merge branch 'worktree-agent-a985c01f43538af01'106 pending="Resetting…"
107 className="mt-1.5 inline-flex items-center gap-1.5 text-xs text-muted hover:text-fg disabled:opacity-50"
Fast pages, required checks on the branch, self-hosted runners, honest incidents108 >
109 Use the repository's description
Merge branch 'worktree-agent-a985c01f43538af01'110 </SubmitButton>
Fast pages, required checks on the branch, self-hosted runners, honest incidents111 )}
112 </div>
Projects: what a workspace builds and runs, first on every page113 </div>
114 </section>
Agents as a team: lifecycle, merge queue, billing and a new shell115
Projects: what a workspace builds and runs, first on every page116 <section className="space-y-4">
117 <h2 className="flex items-center gap-2 text-sm font-medium">
118 <Code2 size={15} className="text-accent" />
119 Source
120 </h2>
121 {source && (
122 <div className="rounded-xl border border-line bg-surface p-4 text-sm">
123 <div className="flex flex-wrap items-center justify-between gap-3">
124 <div>
125 <Link to={`${base}/code`} className="font-mono hover:text-accent">
126 {source.repo.namespace}/{source.repo.name}
127 </Link>
128 <p className="mt-0.5 text-xs text-muted">
129 Hosted on g1t · <GitBranch size={11} className="inline" /> {source.defaultBranch}
130 </p>
131 </div>
132 <Link to={`${base}/settings/repository`} className="text-xs text-muted hover:text-fg">
133 Repository settings
134 </Link>
135 </div>
136 <p className="mt-3 text-xs text-faint">
137 Mirroring a repository from GitHub, GitLab or Bitbucket as a project's source is coming next.
138 </p>
139 </div>
140 )}
141 <Field
142 label="Root directory"
143 hint="Where in the repository this project lives, such as apps/web. Builds run there. Empty for the whole repository."
Webhooks: every event, to your own addresses, signed and retried144 >
Projects: what a workspace builds and runs, first on every page145 <Input name="rootDir" defaultValue={source?.rootDir ?? ""} placeholder="apps/web" className="font-mono" />
146 </Field>
147 </section>
Agents as a team: lifecycle, merge queue, billing and a new shell148
Merge branch 'projects-kind-and-links'149 <section className="scroll-mt-20 space-y-4" id="kind">
150 {/* Old links to #deploys land here too. */}
151 <span id="deploys" />
A project is an app or a library: libraries show their package and how to ship a release, not production152 <h2 className="flex items-center gap-2 text-sm font-medium">
153 <Rocket size={15} className="text-accent" />
Merge branch 'projects-kind-and-links'154 What it is
A project is an app or a library: libraries show their package and how to ship a release, not production155 </h2>
Merge branch 'projects-kind-and-links'156 <p className="max-w-2xl text-xs text-muted">
157 Its overview follows: production for what is deployed, packages and releases for a library or a tool, and
158 the steps that apply to it. Only an app or site deployed on g1t is asked to turn on Deployments.
159 </p>
160 <KindFields project={project} choice={choice} onChoice={setChoice} />
A project is an app or a library: libraries show their package and how to ship a release, not production161 {blocked && (
162 <p className="rounded-lg border border-warn/40 bg-warn/5 px-3.5 py-2.5 text-sm text-fg-soft">
163 Deployments are on for {project.name}. Turn them off in{" "}
164 <Link to={`${base}/settings/deployments`} className="text-fg underline underline-offset-4">
165 Deployments settings
166 </Link>{" "}
Merge branch 'projects-kind-and-links'167 first, then choose this here.
A project is an app or a library: libraries show their package and how to ship a release, not production168 </p>
169 )}
Merge branch 'projects-kind-and-links'170 </section>
171
172 <section className="scroll-mt-20 space-y-4" id="links">
173 <h2 className="flex items-center gap-2 text-sm font-medium">
174 <Link2 size={15} className="text-accent" />
175 Links
176 </h2>
177 <p className="max-w-2xl text-xs text-muted">
178 Shown on its overview, its card, and its workspace's Projects. Each is an http or https address;
179 https:// is added when you leave it out.
A project is an app or a library: libraries show their package and how to ship a release, not production180 </p>
Merge branch 'projects-kind-and-links'181 <LinkFields links={project.links} />
A project is an app or a library: libraries show their package and how to ship a release, not production182 </section>
183
Projects: what a workspace builds and runs, first on every page184 <div className="flex items-center gap-3">
Merge branch 'worktree-agent-a985c01f43538af01'185 <SubmitButton name="intent" value="save" pending="Saving…" disabled={blocked}>
Projects: what a workspace builds and runs, first on every page186 Save
Merge branch 'worktree-agent-a985c01f43538af01'187 </SubmitButton>
Merge branch 'worktree-agent-ad7c6d88d93adc817'188 {actionData && "saved" in actionData && <span className="text-sm text-success">Saved.</span>}
Projects: what a workspace builds and runs, first on every page189 <span className="ml-auto text-xs text-faint">
Project settings say who created the project, by name190 Created{" "}
191 {mine ? (
192 "by you "
193 ) : creator ? (
194 <>
195 by{" "}
196 <Link to={`/${creator}`} className="text-muted hover:text-fg">
197 {creator}
198 </Link>{" "}
199 </>
200 ) : null}
Project settings no longer print the creator's raw id201 <TimeAgo at={project.createdAt} />
Projects: what a workspace builds and runs, first on every page202 </span>
Webhooks: every event, to your own addresses, signed and retried203 </div>
Projects: what a workspace builds and runs, first on every page204 <ErrorText>{actionData && "error" in actionData ? actionData.error : null}</ErrorText>
Webhooks: every event, to your own addresses, signed and retried205 </Form>
Projects: what a workspace builds and runs, first on every page206 </div>
Agents as a team: lifecycle, merge queue, billing and a new shell207 );
208}

This file's history is long; its oldest lines are credited to the oldest commit read.