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

194 lines8,624 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.

Project dependencies: addresses, preview stacks, Affects, and agents who know1import { ArrowDownLeft, ArrowUpRight, FileCode2, Network, Trash2 } from "lucide-react";
Merge branch 'worktree-agent-a985c01f43538af01'2import { Form, Link } from "react-router";
Project dependencies: addresses, preview stacks, Affects, and agents who know3
4import type { DependencyLink } from "@g1t/contracts";
5
6import type { Route } from "./+types/settings-dependencies";
Social cards for every page: og.g1t.sh7import { page } from "../../lib/meta";
Project settings move into the sidebar, grouped, with a heading per page8import { RepoSettingsHeading } from "../../components/repo-settings-heading";
Merge branch 'worktree-agent-a985c01f43538af01'9import { Avatar, EmptyState, ErrorText, Field, Input, SubmitButton } from "../../components/ui";
Workspace names and icons, and a component kit for every control10import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../components/ui/select";
Project dependencies: addresses, preview stacks, Affects, and agents who know11import { 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";
Project dependencies: addresses, preview stacks, Affects, and agents who know14
Social cards for every page: og.g1t.sh15export function meta({ params, ...args }: Route.MetaArgs) {
16 return page(args, { title: `Dependencies · ${params.owner}/${params.repo} · g1t` });
Project dependencies: addresses, preview stacks, Affects, and agents who know17}
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 // Admins; to anyone without a role here the page does not exist.
22 await requireInsider(context, params, "manage_settings");
Project dependencies: addresses, preview stacks, Affects, and agents who know23 const [deps, all] = await Promise.all([
24 projects.dependencies(params.owner, params.repo, viewer),
25 projects.list(params.owner, viewer),
26 ]);
27 return { dependencies: unwrap(deps), projects: all.ok ? all.value : [] };
28}
29
30export async function action({ request, params, context }: Route.ActionArgs) {
31 assertSameOrigin(request);
32 const user = requireUser(context, request);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look33 await requireCapability(context, params, "manage_settings");
Project dependencies: addresses, preview stacks, Affects, and agents who know34 const form = await request.formData();
35 const on = String(form.get("on") ?? "");
36 if (form.get("intent") === "remove") {
37 const done = await projects.removeDependency(user, params.owner, params.repo, on);
38 return done.ok ? { notice: `No longer depends on ${on}.` } : { error: done.error.message };
39 }
40 const added = await projects.addDependency(user, params.owner, params.repo, on, String(form.get("as") ?? "") || null);
41 return added.ok ? { notice: `Depends on ${on}.` } : { error: added.error.message };
42}
43
Merge branch 'worktree-agent-a985c01f43538af01'44function Row({ link, base, removable }: { link: DependencyLink; base: string; removable: boolean }) {
Project dependencies: addresses, preview stacks, Affects, and agents who know45 return (
46 <li className="flex items-center gap-3 px-4 py-3 text-sm">
47 <Link to={`/${base.split("/")[1]}/${link.slug}`} className="font-medium hover:underline">
48 {link.name}
49 </Link>
50 {link.as ? (
51 <code className="rounded bg-raised px-1.5 py-0.5 font-mono text-xs text-muted">{link.as}</code>
52 ) : (
53 <span className="text-xs text-faint">no variable</span>
54 )}
55 {link.source === "file" && (
56 <span className="inline-flex items-center gap-1 text-xs text-faint" title="Declared in .g1t/project.yml">
57 <FileCode2 size={12} />
58 project.yml
59 </span>
60 )}
61 {removable && link.source === "ui" && (
62 <Form method="post" className="ml-auto">
63 <input type="hidden" name="intent" value="remove" />
64 <input type="hidden" name="on" value={link.slug} />
Merge branch 'worktree-agent-a985c01f43538af01'65 <SubmitButton
66 icon
67 match={{ intent: "remove", on: link.slug }}
Project dependencies: addresses, preview stacks, Affects, and agents who know68 aria-label={`Stop depending on ${link.name}`}
Merge branch 'worktree-agent-a985c01f43538af01'69 className="rounded-md p-1.5 text-faint transition-colors hover:bg-raised hover:text-danger disabled:opacity-50"
Project dependencies: addresses, preview stacks, Affects, and agents who know70 >
71 <Trash2 size={14} />
Merge branch 'worktree-agent-a985c01f43538af01'72 </SubmitButton>
Project dependencies: addresses, preview stacks, Affects, and agents who know73 </Form>
74 )}
75 </li>
76 );
77}
78
79export default function DependencySettings({ loaderData, actionData, params }: Route.ComponentProps) {
80 const { dependencies, projects: all } = loaderData;
81 const base = `/${params.owner}/${params.repo}`;
82 const taken = new Set([params.repo.toLowerCase(), ...dependencies.dependsOn.map((d) => d.slug)]);
83 const choices = all.filter((project) => !taken.has(project.slug));
84 return (
85 <div className="max-w-4xl">
Project settings move into the sidebar, grouped, with a heading per page86 <RepoSettingsHeading base={base} />
Project dependencies: addresses, preview stacks, Affects, and agents who know87 <header>
88 <h2 className="flex items-center gap-2 text-sm font-medium">
89 <Network size={15} className="text-accent" />
90 Dependencies
91 </h2>
92 <p className="mt-1 max-w-2xl text-sm text-muted">
93 The projects this one uses: calls their API, consumes their package. Its builds and its running app get each
94 one's address for the same environment under the variable you name, a preview pointing at the same branch's
95 preview when there is one. Agents working here are told what uses this project.{" "}
96 <a href="https://docs.g1t.sh/guides/projects/#dependencies" className="text-fg hover:underline">
97 How dependencies work
98 </a>
99 </p>
100 </header>
101
102 <div className="mt-4 min-h-6">
103 {actionData && "notice" in actionData && <p className="text-sm text-accent">{actionData.notice}</p>}
104 <ErrorText>{actionData && "error" in actionData ? actionData.error : null}</ErrorText>
105 </div>
106
107 <section>
108 <h3 className="flex items-center gap-1.5 text-xs font-medium tracking-wide text-muted uppercase">
109 <ArrowUpRight size={13} />
110 Depends on
111 </h3>
112 <div className="mt-2">
113 {dependencies.dependsOn.length === 0 ? (
114 <EmptyState title="Uses no other project" />
115 ) : (
116 <ul className="divide-y divide-line overflow-hidden rounded-xl border border-line bg-surface">
117 {dependencies.dependsOn.map((link) => (
Merge branch 'worktree-agent-a985c01f43538af01'118 <Row key={link.slug} link={link} base={base} removable />
Project dependencies: addresses, preview stacks, Affects, and agents who know119 ))}
120 </ul>
121 )}
122 </div>
123 {choices.length > 0 && (
Merge branch 'worktree-agent-a985c01f43538af01'124 // Keyed to what it depends on, so once one is added the form starts over on what is left.
125 <Form
126 key={dependencies.dependsOn.map((link) => link.slug).join(" ")}
127 method="post"
128 className="mt-4 grid items-end gap-3 rounded-xl border border-line bg-surface p-4 sm:grid-cols-[1fr_1fr_auto]"
129 >
Project dependencies: addresses, preview stacks, Affects, and agents who know130 <input type="hidden" name="intent" value="add" />
131 <Field label="Project">
Workspace names and icons, and a component kit for every control132 <Select name="on" required defaultValue={choices[0].slug}>
133 <SelectTrigger aria-label="Project" className="h-auto py-2">
134 <SelectValue />
135 </SelectTrigger>
136 <SelectContent>
137 {choices.map((project) => (
138 <SelectItem
139 key={project.slug}
140 value={project.slug}
141 icon={<Avatar name={project.name} size={16} square />}
142 description={project.slug !== project.name ? project.slug : undefined}
143 >
144 {project.name}
145 </SelectItem>
146 ))}
147 </SelectContent>
148 </Select>
Project dependencies: addresses, preview stacks, Affects, and agents who know149 </Field>
150 <Field label="Its address as">
151 <Input name="as" placeholder="API_URL" className="font-mono" />
152 </Field>
Merge branch 'worktree-agent-a985c01f43538af01'153 <SubmitButton match={{ intent: "add" }} pending="Adding…">
Project dependencies: addresses, preview stacks, Affects, and agents who know154 Add dependency
Merge branch 'worktree-agent-a985c01f43538af01'155 </SubmitButton>
Project dependencies: addresses, preview stacks, Affects, and agents who know156 </Form>
157 )}
158 </section>
159
160 <section className="mt-10">
161 <h3 className="flex items-center gap-1.5 text-xs font-medium tracking-wide text-muted uppercase">
162 <ArrowDownLeft size={13} />
163 Used by
164 </h3>
165 <div className="mt-2">
166 {dependencies.usedBy.length === 0 ? (
167 <EmptyState title="No project uses this one yet" />
168 ) : (
169 <ul className="divide-y divide-line overflow-hidden rounded-xl border border-line bg-surface">
170 {dependencies.usedBy.map((link) => (
Merge branch 'worktree-agent-a985c01f43538af01'171 <Row key={link.slug} link={link} base={base} removable={false} />
Project dependencies: addresses, preview stacks, Affects, and agents who know172 ))}
173 </ul>
174 )}
175 </div>
176 </section>
177
178 <section className="mt-10 rounded-xl border border-line p-5 text-sm">
179 <h3 className="flex items-center gap-2 font-medium">
180 <FileCode2 size={15} className="text-faint" />
181 Or declare them in the code
182 </h3>
183 <p className="mt-1 text-muted">
184 A <code className="text-fg">.g1t/project.yml</code> in the project's root directory is read on every push to
185 the default branch, and its dependencies replace the ones it declared before:
186 </p>
187 <pre className="mt-3 overflow-x-auto rounded-lg border border-line bg-bg p-3 font-mono text-xs text-muted">{`dependsOn:
188 - project: api
189 as: API_URL
190 - project: ui-kit`}</pre>
191 </section>
192 </div>
193 );
194}

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