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

195 lines8,919 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.

A project is an app or a library: libraries show their package and how to ship a release, not production1import { Box, Code2, GitBranch, Rocket } from "lucide-react";
2import { useState } from "react";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look3import { Form, Link, useNavigation } from "react-router";
Agents as a team: lifecycle, merge queue, billing and a new shell4
A project is an app or a library: libraries show their package and how to ship a release, not production5import type { DeploysSetting } from "@g1t/contracts";
6
Agents as a team: lifecycle, merge queue, billing and a new shell7import type { Route } from "./+types/settings";
Social cards for every page: og.g1t.sh8import { page } from "../../lib/meta";
Project settings move into the sidebar, grouped, with a heading per page9import { RepoSettingsHeading } from "../../components/repo-settings-heading";
Agents as a team: lifecycle, merge queue, billing and a new shell10import { Button, ErrorText, Field, Input, TimeAgo } from "../../components/ui";
A project is an app or a library: libraries show their package and how to ship a release, not production11import { RadioGroup, RadioOption } from "../../components/ui/radio-group";
12import { DEPLOYS_CHOICES } from "../../lib/project-kind";
13import { deployments, projects } from "../../lib/services.server";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look14import { assertSameOrigin, getViewer, requireUser, unwrap } from "../../lib/session.server";
15import { requireCapability, requireInsider } from "../../lib/access.server";
Agents as a team: lifecycle, merge queue, billing and a new shell16
Social cards for every page: og.g1t.sh17export function meta({ params, ...args }: Route.MetaArgs) {
18 return page(args, { title: `Settings · ${params.owner}/${params.repo} · g1t` });
Agents as a team: lifecycle, merge queue, billing and a new shell19}
20
21export async function loader({ params, context }: Route.LoaderArgs) {
22 const viewer = getViewer(context);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look23 // Maintain and up; to anyone without a role here the page does not exist.
24 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 production25 const [project, deploys] = await Promise.all([
26 projects.get(params.owner, params.repo, viewer),
27 // Whether Deployments are on: needs Admin, so unknown (null) below it.
28 deployments.settings({ workspace: params.owner, slug: params.repo }, viewer).catch(() => null),
29 ]);
Project settings no longer print the creator's raw id30 const found = unwrap(project);
31 // Who made it is kept as an id; name it only when it was the viewer.
32 return { project: found, deploymentsOn: deploys?.ok ? deploys.value.enabled : null, mine: viewer?.id === found.createdBy };
Agents as a team: lifecycle, merge queue, billing and a new shell33}
34
35export async function action({ request, params, context }: Route.ActionArgs) {
36 assertSameOrigin(request);
37 const user = requireUser(context, request);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look38 await requireCapability(context, params, "manage_settings");
Agents as a team: lifecycle, merge queue, billing and a new shell39 const form = await request.formData();
Projects: what a workspace builds and runs, first on every page40 const saved = await projects.update(user, params.owner, params.repo, {
41 name: String(form.get("name") ?? ""),
Fast pages, required checks on the branch, self-hosted runners, honest incidents42 // Blank, or the reset, and it follows the repository's description again.
43 description: form.get("inherit") === "description" ? null : String(form.get("description") ?? ""),
Projects: what a workspace builds and runs, first on every page44 rootDir: String(form.get("rootDir") ?? ""),
A project is an app or a library: libraries show their package and how to ship a release, not production45 deploys: (form.get("deploys") as DeploysSetting | null) ?? undefined,
Agents as a team: lifecycle, merge queue, billing and a new shell46 });
Projects: what a workspace builds and runs, first on every page47 return saved.ok ? { saved: true as const } : { error: saved.error.message };
Agents as a team: lifecycle, merge queue, billing and a new shell48}
49
Projects: what a workspace builds and runs, first on every page50export default function ProjectSettings({ loaderData, actionData, params }: Route.ComponentProps) {
Project settings no longer print the creator's raw id51 const { project, deploymentsOn, mine } = loaderData;
A project is an app or a library: libraries show their package and how to ship a release, not production52 const [deploys, setDeploys] = useState<DeploysSetting>(project.deploys);
53 // Not deploying while Deployments are on is refused: they are turned off first.
54 const blocked = deploys === "no" && project.deploys !== "no" && deploymentsOn === true;
Agents as a team: lifecycle, merge queue, billing and a new shell55 const saving = useNavigation().state === "submitting";
Projects: what a workspace builds and runs, first on every page56 const base = `/${params.owner}/${params.repo}`;
57 const source = project.source.kind === "hosted" ? project.source : null;
Agents as a team: lifecycle, merge queue, billing and a new shell58 return (
Projects: what a workspace builds and runs, first on every page59 <div className="max-w-4xl">
Project settings move into the sidebar, grouped, with a heading per page60 <RepoSettingsHeading base={base} />
Projects: what a workspace builds and runs, first on every page61 <Form method="post" className="space-y-8">
62 <section className="space-y-4">
63 <h2 className="flex items-center gap-2 text-sm font-medium">
64 <Box size={15} className="text-accent" />
65 Project
66 </h2>
67 <div className="grid gap-4 md:grid-cols-2">
68 <Field label="Name" hint={`Its address stays g1t.sh/${project.workspace}/${project.slug}.`}>
69 <Input name="name" defaultValue={project.name} required maxLength={100} />
70 </Field>
Fast pages, required checks on the branch, self-hosted runners, honest incidents71 <div>
72 <Field
73 label="Description"
74 hint={
75 project.descriptionInherited
76 ? "Follows the repository's description. Write one to give the project its own."
77 : "The project's own. Empty follows the repository's description."
78 }
79 >
80 <Input
81 key={`${project.descriptionInherited}:${project.description ?? ""}`}
82 name="description"
83 defaultValue={project.descriptionInherited ? "" : (project.description ?? "")}
84 maxLength={200}
85 placeholder={(project.descriptionInherited && project.description) || "What it is, in a line"}
86 />
87 </Field>
88 {!project.descriptionInherited && (
89 <button
90 type="submit"
91 name="inherit"
92 value="description"
93 disabled={saving}
94 className="mt-1.5 text-xs text-muted hover:text-fg disabled:opacity-50"
95 >
96 Use the repository's description
97 </button>
98 )}
99 </div>
Projects: what a workspace builds and runs, first on every page100 </div>
101 </section>
Agents as a team: lifecycle, merge queue, billing and a new shell102
Projects: what a workspace builds and runs, first on every page103 <section className="space-y-4">
104 <h2 className="flex items-center gap-2 text-sm font-medium">
105 <Code2 size={15} className="text-accent" />
106 Source
107 </h2>
108 {source && (
109 <div className="rounded-xl border border-line bg-surface p-4 text-sm">
110 <div className="flex flex-wrap items-center justify-between gap-3">
111 <div>
112 <Link to={`${base}/code`} className="font-mono hover:text-accent">
113 {source.repo.namespace}/{source.repo.name}
114 </Link>
115 <p className="mt-0.5 text-xs text-muted">
116 Hosted on g1t · <GitBranch size={11} className="inline" /> {source.defaultBranch}
117 </p>
118 </div>
119 <Link to={`${base}/settings/repository`} className="text-xs text-muted hover:text-fg">
120 Repository settings
121 </Link>
122 </div>
123 <p className="mt-3 text-xs text-faint">
124 Mirroring a repository from GitHub, GitLab or Bitbucket as a project's source is coming next.
125 </p>
126 </div>
127 )}
128 <Field
129 label="Root directory"
130 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 retried131 >
Projects: what a workspace builds and runs, first on every page132 <Input name="rootDir" defaultValue={source?.rootDir ?? ""} placeholder="apps/web" className="font-mono" />
133 </Field>
134 </section>
Agents as a team: lifecycle, merge queue, billing and a new shell135
A project is an app or a library: libraries show their package and how to ship a release, not production136 <section className="space-y-4" id="deploys">
137 <h2 className="flex items-center gap-2 text-sm font-medium">
138 <Rocket size={15} className="text-accent" />
139 Deployments for this project
140 </h2>
141 <RadioGroup
142 name="deploys"
143 value={deploys}
144 onValueChange={(value) => setDeploys(value as DeploysSetting)}
145 aria-label="Deployments for this project"
146 className="gap-3"
147 >
148 {DEPLOYS_CHOICES.map((choice) => (
149 <RadioOption
150 key={choice.value}
151 value={choice.value}
152 label={choice.label}
153 description={
154 choice.value === "auto" ? (
155 <>
156 {project.detected.kind === "library" ? "Detected: a library, so it doesn't deploy." : "Detected: an app, so it deploys."}{" "}
157 {project.detected.reason.detail}
158 </>
159 ) : (
160 choice.hint
161 )
162 }
163 />
164 ))}
165 </RadioGroup>
166 {blocked && (
167 <p className="rounded-lg border border-warn/40 bg-warn/5 px-3.5 py-2.5 text-sm text-fg-soft">
168 Deployments are on for {project.name}. Turn them off in{" "}
169 <Link to={`${base}/settings/deployments`} className="text-fg underline underline-offset-4">
170 Deployments settings
171 </Link>{" "}
172 first, then choose Doesn't deploy here.
173 </p>
174 )}
175 <p className="text-xs text-faint">
176 A project that doesn't deploy shows its packages and releases on its overview instead of production. Its
177 Deployments page stays in the sidebar.
178 </p>
179 </section>
180
Projects: what a workspace builds and runs, first on every page181 <div className="flex items-center gap-3">
A project is an app or a library: libraries show their package and how to ship a release, not production182 <Button type="submit" disabled={saving || blocked}>
Projects: what a workspace builds and runs, first on every page183 Save
Webhooks: every event, to your own addresses, signed and retried184 </Button>
Projects: what a workspace builds and runs, first on every page185 {actionData && "saved" in actionData && <span className="text-sm text-accent">Saved.</span>}
186 <span className="ml-auto text-xs text-faint">
Project settings no longer print the creator's raw id187 Created {mine && "by you "}
188 <TimeAgo at={project.createdAt} />
Projects: what a workspace builds and runs, first on every page189 </span>
Webhooks: every event, to your own addresses, signed and retried190 </div>
Projects: what a workspace builds and runs, first on every page191 <ErrorText>{actionData && "error" in actionData ? actionData.error : null}</ErrorText>
Webhooks: every event, to your own addresses, signed and retried192 </Form>
Projects: what a workspace builds and runs, first on every page193 </div>
Agents as a team: lifecycle, merge queue, billing and a new shell194 );
195}