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

175 lines7,657 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";
2import { Form, Link, data, useNavigation } from "react-router";
3
4import type { DependencyLink } from "@g1t/contracts";
5
6import type { Route } from "./+types/settings-dependencies";
7import { RepoSettingsTabs } from "../../components/repo-settings-tabs";
8import { Button, EmptyState, ErrorText, Field, Input } from "../../components/ui";
9import { projects } from "../../lib/services.server";
10import { assertSameOrigin, getViewer, requireUser, roleIn, unwrap } from "../../lib/session.server";
11
12export function meta({ params }: Route.MetaArgs) {
13 return [{ title: `Dependencies · ${params.owner}/${params.repo} · g1t` }];
14}
15
16export async function loader({ params, context }: Route.LoaderArgs) {
17 const viewer = getViewer(context);
18 if (!roleIn(viewer, params.owner)) throw data(null, { status: 404 });
19 const [deps, all] = await Promise.all([
20 projects.dependencies(params.owner, params.repo, viewer),
21 projects.list(params.owner, viewer),
22 ]);
23 return { dependencies: unwrap(deps), projects: all.ok ? all.value : [] };
24}
25
26export async function action({ request, params, context }: Route.ActionArgs) {
27 assertSameOrigin(request);
28 const user = requireUser(context, request);
29 const form = await request.formData();
30 const on = String(form.get("on") ?? "");
31 if (form.get("intent") === "remove") {
32 const done = await projects.removeDependency(user, params.owner, params.repo, on);
33 return done.ok ? { notice: `No longer depends on ${on}.` } : { error: done.error.message };
34 }
35 const added = await projects.addDependency(user, params.owner, params.repo, on, String(form.get("as") ?? "") || null);
36 return added.ok ? { notice: `Depends on ${on}.` } : { error: added.error.message };
37}
38
39function Row({ link, base, removable, busy }: { link: DependencyLink; base: string; removable: boolean; busy: boolean }) {
40 return (
41 <li className="flex items-center gap-3 px-4 py-3 text-sm">
42 <Link to={`/${base.split("/")[1]}/${link.slug}`} className="font-medium hover:underline">
43 {link.name}
44 </Link>
45 {link.as ? (
46 <code className="rounded bg-raised px-1.5 py-0.5 font-mono text-xs text-muted">{link.as}</code>
47 ) : (
48 <span className="text-xs text-faint">no variable</span>
49 )}
50 {link.source === "file" && (
51 <span className="inline-flex items-center gap-1 text-xs text-faint" title="Declared in .g1t/project.yml">
52 <FileCode2 size={12} />
53 project.yml
54 </span>
55 )}
56 {removable && link.source === "ui" && (
57 <Form method="post" className="ml-auto">
58 <input type="hidden" name="intent" value="remove" />
59 <input type="hidden" name="on" value={link.slug} />
60 <button
61 type="submit"
62 disabled={busy}
63 aria-label={`Stop depending on ${link.name}`}
64 className="rounded-md p-1.5 text-faint transition-colors hover:bg-raised hover:text-danger"
65 >
66 <Trash2 size={14} />
67 </button>
68 </Form>
69 )}
70 </li>
71 );
72}
73
74export default function DependencySettings({ loaderData, actionData, params }: Route.ComponentProps) {
75 const { dependencies, projects: all } = loaderData;
76 const busy = useNavigation().state === "submitting";
77 const base = `/${params.owner}/${params.repo}`;
78 const taken = new Set([params.repo.toLowerCase(), ...dependencies.dependsOn.map((d) => d.slug)]);
79 const choices = all.filter((project) => !taken.has(project.slug));
80 return (
81 <div className="max-w-4xl">
82 <RepoSettingsTabs base={base} />
83 <header>
84 <h2 className="flex items-center gap-2 text-sm font-medium">
85 <Network size={15} className="text-accent" />
86 Dependencies
87 </h2>
88 <p className="mt-1 max-w-2xl text-sm text-muted">
89 The projects this one uses: calls their API, consumes their package. Its builds and its running app get each
90 one's address for the same environment under the variable you name, a preview pointing at the same branch's
91 preview when there is one. Agents working here are told what uses this project.{" "}
92 <a href="https://docs.g1t.sh/guides/projects/#dependencies" className="text-fg hover:underline">
93 How dependencies work
94 </a>
95 </p>
96 </header>
97
98 <div className="mt-4 min-h-6">
99 {actionData && "notice" in actionData && <p className="text-sm text-accent">{actionData.notice}</p>}
100 <ErrorText>{actionData && "error" in actionData ? actionData.error : null}</ErrorText>
101 </div>
102
103 <section>
104 <h3 className="flex items-center gap-1.5 text-xs font-medium tracking-wide text-muted uppercase">
105 <ArrowUpRight size={13} />
106 Depends on
107 </h3>
108 <div className="mt-2">
109 {dependencies.dependsOn.length === 0 ? (
110 <EmptyState title="Uses no other project" />
111 ) : (
112 <ul className="divide-y divide-line overflow-hidden rounded-xl border border-line bg-surface">
113 {dependencies.dependsOn.map((link) => (
114 <Row key={link.slug} link={link} base={base} removable busy={busy} />
115 ))}
116 </ul>
117 )}
118 </div>
119 {choices.length > 0 && (
120 <Form method="post" className="mt-4 grid items-end gap-3 rounded-xl border border-line bg-surface p-4 sm:grid-cols-[1fr_1fr_auto]">
121 <input type="hidden" name="intent" value="add" />
122 <Field label="Project">
123 <select name="on" required className="w-full rounded-md border border-line bg-bg px-3 py-2 text-sm">
124 {choices.map((project) => (
125 <option key={project.slug} value={project.slug}>
126 {project.name}
127 </option>
128 ))}
129 </select>
130 </Field>
131 <Field label="Its address as">
132 <Input name="as" placeholder="API_URL" className="font-mono" />
133 </Field>
134 <Button type="submit" disabled={busy}>
135 Add dependency
136 </Button>
137 </Form>
138 )}
139 </section>
140
141 <section className="mt-10">
142 <h3 className="flex items-center gap-1.5 text-xs font-medium tracking-wide text-muted uppercase">
143 <ArrowDownLeft size={13} />
144 Used by
145 </h3>
146 <div className="mt-2">
147 {dependencies.usedBy.length === 0 ? (
148 <EmptyState title="No project uses this one yet" />
149 ) : (
150 <ul className="divide-y divide-line overflow-hidden rounded-xl border border-line bg-surface">
151 {dependencies.usedBy.map((link) => (
152 <Row key={link.slug} link={link} base={base} removable={false} busy={busy} />
153 ))}
154 </ul>
155 )}
156 </div>
157 </section>
158
159 <section className="mt-10 rounded-xl border border-line p-5 text-sm">
160 <h3 className="flex items-center gap-2 font-medium">
161 <FileCode2 size={15} className="text-faint" />
162 Or declare them in the code
163 </h3>
164 <p className="mt-1 text-muted">
165 A <code className="text-fg">.g1t/project.yml</code> in the project's root directory is read on every push to
166 the default branch, and its dependencies replace the ones it declared before:
167 </p>
168 <pre className="mt-3 overflow-x-auto rounded-lg border border-line bg-bg p-3 font-mono text-xs text-muted">{`dependsOn:
169 - project: api
170 as: API_URL
171 - project: ui-kit`}</pre>
172 </section>
173 </div>
174 );
175}