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 production | 1 | import { Box, Code2, GitBranch, Rocket } from "lucide-react"; |
| 2 | import { useState } from "react"; | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 3 | import { Form, Link, useNavigation } from "react-router"; |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 4 | |
| A project is an app or a library: libraries show their package and how to ship a release, not production | 5 | import type { DeploysSetting } from "@g1t/contracts"; |
| 6 | ||
| Agents as a team: lifecycle, merge queue, billing and a new shell | 7 | import type { Route } from "./+types/settings"; |
| Social cards for every page: og.g1t.sh | 8 | import { page } from "../../lib/meta"; |
| Project settings move into the sidebar, grouped, with a heading per page | 9 | import { RepoSettingsHeading } from "../../components/repo-settings-heading"; |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 10 | import { 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 production | 11 | import { RadioGroup, RadioOption } from "../../components/ui/radio-group"; |
| 12 | import { DEPLOYS_CHOICES } from "../../lib/project-kind"; | |
| 13 | import { deployments, projects } from "../../lib/services.server"; | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 14 | import { assertSameOrigin, getViewer, requireUser, unwrap } from "../../lib/session.server"; |
| 15 | import { requireCapability, requireInsider } from "../../lib/access.server"; | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 16 | |
| Social cards for every page: og.g1t.sh | 17 | export 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 shell | 19 | } |
| 20 | ||
| 21 | export 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 look | 23 | // 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 production | 25 | 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 | ]); | |
| 30 | return { project: unwrap(project), deploymentsOn: deploys?.ok ? deploys.value.enabled : null }; | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 31 | } |
| 32 | ||
| 33 | export async function action({ request, params, context }: Route.ActionArgs) { | |
| 34 | assertSameOrigin(request); | |
| 35 | const user = requireUser(context, request); | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 36 | await requireCapability(context, params, "manage_settings"); |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 37 | const form = await request.formData(); |
| Projects: what a workspace builds and runs, first on every page | 38 | const saved = await projects.update(user, params.owner, params.repo, { |
| 39 | name: String(form.get("name") ?? ""), | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 40 | // Blank, or the reset, and it follows the repository's description again. |
| 41 | description: form.get("inherit") === "description" ? null : String(form.get("description") ?? ""), | |
| Projects: what a workspace builds and runs, first on every page | 42 | rootDir: String(form.get("rootDir") ?? ""), |
| A project is an app or a library: libraries show their package and how to ship a release, not production | 43 | deploys: (form.get("deploys") as DeploysSetting | null) ?? undefined, |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 44 | }); |
| Projects: what a workspace builds and runs, first on every page | 45 | return saved.ok ? { saved: true as const } : { error: saved.error.message }; |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 46 | } |
| 47 | ||
| Projects: what a workspace builds and runs, first on every page | 48 | export default function ProjectSettings({ loaderData, actionData, params }: Route.ComponentProps) { |
| A project is an app or a library: libraries show their package and how to ship a release, not production | 49 | const { project, deploymentsOn } = loaderData; |
| 50 | const [deploys, setDeploys] = useState<DeploysSetting>(project.deploys); | |
| 51 | // Not deploying while Deployments are on is refused: they are turned off first. | |
| 52 | const blocked = deploys === "no" && project.deploys !== "no" && deploymentsOn === true; | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 53 | const saving = useNavigation().state === "submitting"; |
| Projects: what a workspace builds and runs, first on every page | 54 | const base = `/${params.owner}/${params.repo}`; |
| 55 | const source = project.source.kind === "hosted" ? project.source : null; | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 56 | return ( |
| Projects: what a workspace builds and runs, first on every page | 57 | <div className="max-w-4xl"> |
| Project settings move into the sidebar, grouped, with a heading per page | 58 | <RepoSettingsHeading base={base} /> |
| Projects: what a workspace builds and runs, first on every page | 59 | <Form method="post" className="space-y-8"> |
| 60 | <section className="space-y-4"> | |
| 61 | <h2 className="flex items-center gap-2 text-sm font-medium"> | |
| 62 | <Box size={15} className="text-accent" /> | |
| 63 | Project | |
| 64 | </h2> | |
| 65 | <div className="grid gap-4 md:grid-cols-2"> | |
| 66 | <Field label="Name" hint={`Its address stays g1t.sh/${project.workspace}/${project.slug}.`}> | |
| 67 | <Input name="name" defaultValue={project.name} required maxLength={100} /> | |
| 68 | </Field> | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 69 | <div> |
| 70 | <Field | |
| 71 | label="Description" | |
| 72 | hint={ | |
| 73 | project.descriptionInherited | |
| 74 | ? "Follows the repository's description. Write one to give the project its own." | |
| 75 | : "The project's own. Empty follows the repository's description." | |
| 76 | } | |
| 77 | > | |
| 78 | <Input | |
| 79 | key={`${project.descriptionInherited}:${project.description ?? ""}`} | |
| 80 | name="description" | |
| 81 | defaultValue={project.descriptionInherited ? "" : (project.description ?? "")} | |
| 82 | maxLength={200} | |
| 83 | placeholder={(project.descriptionInherited && project.description) || "What it is, in a line"} | |
| 84 | /> | |
| 85 | </Field> | |
| 86 | {!project.descriptionInherited && ( | |
| 87 | <button | |
| 88 | type="submit" | |
| 89 | name="inherit" | |
| 90 | value="description" | |
| 91 | disabled={saving} | |
| 92 | className="mt-1.5 text-xs text-muted hover:text-fg disabled:opacity-50" | |
| 93 | > | |
| 94 | Use the repository's description | |
| 95 | </button> | |
| 96 | )} | |
| 97 | </div> | |
| Projects: what a workspace builds and runs, first on every page | 98 | </div> |
| 99 | </section> | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 100 | |
| Projects: what a workspace builds and runs, first on every page | 101 | <section className="space-y-4"> |
| 102 | <h2 className="flex items-center gap-2 text-sm font-medium"> | |
| 103 | <Code2 size={15} className="text-accent" /> | |
| 104 | Source | |
| 105 | </h2> | |
| 106 | {source && ( | |
| 107 | <div className="rounded-xl border border-line bg-surface p-4 text-sm"> | |
| 108 | <div className="flex flex-wrap items-center justify-between gap-3"> | |
| 109 | <div> | |
| 110 | <Link to={`${base}/code`} className="font-mono hover:text-accent"> | |
| 111 | {source.repo.namespace}/{source.repo.name} | |
| 112 | </Link> | |
| 113 | <p className="mt-0.5 text-xs text-muted"> | |
| 114 | Hosted on g1t · <GitBranch size={11} className="inline" /> {source.defaultBranch} | |
| 115 | </p> | |
| 116 | </div> | |
| 117 | <Link to={`${base}/settings/repository`} className="text-xs text-muted hover:text-fg"> | |
| 118 | Repository settings | |
| 119 | </Link> | |
| 120 | </div> | |
| 121 | <p className="mt-3 text-xs text-faint"> | |
| 122 | Mirroring a repository from GitHub, GitLab or Bitbucket as a project's source is coming next. | |
| 123 | </p> | |
| 124 | </div> | |
| 125 | )} | |
| 126 | <Field | |
| 127 | label="Root directory" | |
| 128 | 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 retried | 129 | > |
| Projects: what a workspace builds and runs, first on every page | 130 | <Input name="rootDir" defaultValue={source?.rootDir ?? ""} placeholder="apps/web" className="font-mono" /> |
| 131 | </Field> | |
| 132 | </section> | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 133 | |
| A project is an app or a library: libraries show their package and how to ship a release, not production | 134 | <section className="space-y-4" id="deploys"> |
| 135 | <h2 className="flex items-center gap-2 text-sm font-medium"> | |
| 136 | <Rocket size={15} className="text-accent" /> | |
| 137 | Deployments for this project | |
| 138 | </h2> | |
| 139 | <RadioGroup | |
| 140 | name="deploys" | |
| 141 | value={deploys} | |
| 142 | onValueChange={(value) => setDeploys(value as DeploysSetting)} | |
| 143 | aria-label="Deployments for this project" | |
| 144 | className="gap-3" | |
| 145 | > | |
| 146 | {DEPLOYS_CHOICES.map((choice) => ( | |
| 147 | <RadioOption | |
| 148 | key={choice.value} | |
| 149 | value={choice.value} | |
| 150 | label={choice.label} | |
| 151 | description={ | |
| 152 | choice.value === "auto" ? ( | |
| 153 | <> | |
| 154 | {project.detected.kind === "library" ? "Detected: a library, so it doesn't deploy." : "Detected: an app, so it deploys."}{" "} | |
| 155 | {project.detected.reason.detail} | |
| 156 | </> | |
| 157 | ) : ( | |
| 158 | choice.hint | |
| 159 | ) | |
| 160 | } | |
| 161 | /> | |
| 162 | ))} | |
| 163 | </RadioGroup> | |
| 164 | {blocked && ( | |
| 165 | <p className="rounded-lg border border-warn/40 bg-warn/5 px-3.5 py-2.5 text-sm text-fg-soft"> | |
| 166 | Deployments are on for {project.name}. Turn them off in{" "} | |
| 167 | <Link to={`${base}/settings/deployments`} className="text-fg underline underline-offset-4"> | |
| 168 | Deployments settings | |
| 169 | </Link>{" "} | |
| 170 | first, then choose Doesn't deploy here. | |
| 171 | </p> | |
| 172 | )} | |
| 173 | <p className="text-xs text-faint"> | |
| 174 | A project that doesn't deploy shows its packages and releases on its overview instead of production. Its | |
| 175 | Deployments page stays in the sidebar. | |
| 176 | </p> | |
| 177 | </section> | |
| 178 | ||
| Projects: what a workspace builds and runs, first on every page | 179 | <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 production | 180 | <Button type="submit" disabled={saving || blocked}> |
| Projects: what a workspace builds and runs, first on every page | 181 | Save |
| Webhooks: every event, to your own addresses, signed and retried | 182 | </Button> |
| Projects: what a workspace builds and runs, first on every page | 183 | {actionData && "saved" in actionData && <span className="text-sm text-accent">Saved.</span>} |
| 184 | <span className="ml-auto text-xs text-faint"> | |
| 185 | Created by {project.createdBy} <TimeAgo at={project.createdAt} /> | |
| 186 | </span> | |
| Webhooks: every event, to your own addresses, signed and retried | 187 | </div> |
| Projects: what a workspace builds and runs, first on every page | 188 | <ErrorText>{actionData && "error" in actionData ? actionData.error : null}</ErrorText> |
| Webhooks: every event, to your own addresses, signed and retried | 189 | </Form> |
| Projects: what a workspace builds and runs, first on every page | 190 | </div> |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 191 | ); |
| 192 | } |