flagon-io/g1t

public

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

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

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