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/overview.tsx

383 lines18,527 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, Box, CircleDot, Code2, GitBranch, GitCommitHorizontal, GitPullRequest, Lock, Network, Rocket, RotateCw } from "lucide-react";
Projects: what a workspace builds and runs, first on every page2import { Form, Link, useNavigation } from "react-router";
3
4import type { Route } from "./+types/overview";
5import { host, StatusDot } from "../../components/deploy";
6import { Button, ButtonLink, CopyLine, EmptyState, TimeAgo } from "../../components/ui";
7import { PullIcon } from "../../components/work";
8import { deployments, projects, repos, work } from "../../lib/services.server";
9import { assertSameOrigin, getViewer, requireUser, roleIn } from "../../lib/session.server";
10
11const MAX_PULLS = 5;
12
13export async function loader({ params, context }: Route.LoaderArgs) {
14 const viewer = getViewer(context);
15 const member = roleIn(viewer, params.owner) != null;
16 const path = { namespace: params.owner, name: params.repo };
17 const ref = { workspace: params.owner, slug: params.repo };
Project dependencies: addresses, preview stacks, Affects, and agents who know18 const [project, settings, list, pulls, log, counts, deps] = await Promise.all([
Projects: what a workspace builds and runs, first on every page19 projects.get(params.owner, params.repo, viewer),
20 member ? deployments.settings(ref, viewer) : null,
21 member ? deployments.list(ref, viewer) : null,
22 work.listPulls(path, viewer, "open"),
23 repos.log(path, viewer, null, 1),
24 work.counts(path, viewer),
Project dependencies: addresses, preview stacks, Affects, and agents who know25 projects.dependencies(params.owner, params.repo, viewer),
Projects: what a workspace builds and runs, first on every page26 ]);
27 return {
28 member,
29 project: project.ok ? project.value : null,
30 settings: settings?.ok ? settings.value : null,
31 builds: list?.ok ? list.value.deployments : [],
32 live: list?.ok ? list.value.live : [],
33 pulls: pulls.ok ? pulls.value.slice(0, MAX_PULLS) : [],
34 commit: log.ok ? (log.value[0] ?? null) : null,
35 open: counts.ok ? counts.value : { issues: 0, pulls: 0 },
Project dependencies: addresses, preview stacks, Affects, and agents who know36 dependencies: deps.ok ? deps.value : { dependsOn: [], usedBy: [] },
Projects: what a workspace builds and runs, first on every page37 };
38}
39
40export async function action({ request, params, context }: Route.ActionArgs) {
41 assertSameOrigin(request);
42 const user = requireUser(context, request);
43 const started = await deployments.redeploy(user, { workspace: params.owner, slug: params.repo }, null);
44 return started.ok ? { notice: "Production is building." } : { error: started.error.message };
45}
46
47export default function ProjectOverview({ loaderData, actionData, params }: Route.ComponentProps) {
Project dependencies: addresses, preview stacks, Affects, and agents who know48 const { member, project, settings, builds, live, pulls, commit, open, dependencies } = loaderData;
Projects: what a workspace builds and runs, first on every page49 const base = `/${params.owner}/${params.repo}`;
50 const production = live.find((app) => app.kind === "production") ?? null;
Agents and memory, checks and conflicts, profiles, slug renames, custom domains51 // The project's own domain, once it is active, is where production is visited.
52 const productionUrl = production ? (settings?.primaryDomain ? `https://${settings.primaryDomain}` : production.url) : null;
Projects: what a workspace builds and runs, first on every page53 const previews = live.filter((app) => app.kind === "preview");
54 const latestProduction = builds.find((build) => build.kind === "production") ?? null;
55 const busy = useNavigation().state === "submitting";
56 const source = project?.source.kind === "hosted" ? project.source : null;
57
58 return (
59 <div className="grid gap-6 lg:grid-cols-[minmax(0,1fr)_20rem]">
60 <div className="min-w-0 space-y-6">
61 {/* Production: what the project is, running. */}
62 <section className="overflow-hidden rounded-2xl border border-line bg-surface">
63 <div className="flex flex-wrap items-start justify-between gap-4 p-6">
64 <div className="min-w-0">
65 <p className="flex items-center gap-2 text-xs font-medium tracking-wide text-muted uppercase">
66 <Rocket size={13} className="text-accent" />
67 Production
68 </p>
69 {production ? (
70 <>
71 <a
Agents and memory, checks and conflicts, profiles, slug renames, custom domains72 href={productionUrl ?? production.url}
Projects: what a workspace builds and runs, first on every page73 className="mt-2 flex items-center gap-1.5 truncate font-mono text-lg font-medium hover:text-accent"
74 >
Agents and memory, checks and conflicts, profiles, slug renames, custom domains75 {host(productionUrl ?? production.url)}
Projects: what a workspace builds and runs, first on every page76 <ArrowUpRight size={16} className="shrink-0 text-faint" />
77 </a>
78 <p className="mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted">
79 {latestProduction && <StatusDot status={latestProduction.status} />}
80 <span className="inline-flex items-center gap-1 font-mono">
81 <GitCommitHorizontal size={13} className="text-faint" />
82 {production.commit.slice(0, 7)}
83 </span>
84 <span>
85 deployed <TimeAgo at={production.deployedAt} />
86 </span>
87 </p>
88 </>
89 ) : settings?.enabled ? (
90 <>
91 <p className="mt-2 font-mono text-lg text-muted">{host(settings.productionUrl)}</p>
92 <p className="mt-1.5 text-xs text-muted">
93 {latestProduction ? (
94 <StatusDot status={latestProduction.status} label={latestProduction.status === "failed" ? "The last build failed" : undefined} />
95 ) : (
96 "Not deployed yet. Push to the default branch, or deploy it now."
97 )}
98 </p>
99 </>
100 ) : (
101 <>
102 <p className="mt-2 text-lg font-medium">Not deployed</p>
103 <p className="mt-1 max-w-lg text-sm text-muted">
104 Put {project?.name ?? params.repo} on g1t.page: production from the default branch, and a live
105 preview for every pull request. It runs only while someone visits.
106 </p>
107 </>
108 )}
109 </div>
110 {member && (
111 <div className="flex shrink-0 items-center gap-2">
112 {production && (
Agents and memory, checks and conflicts, profiles, slug renames, custom domains113 <ButtonLink to={productionUrl ?? production.url} variant="accent" reloadDocument>
Projects: what a workspace builds and runs, first on every page114 Visit
115 <ArrowUpRight size={14} />
116 </ButtonLink>
117 )}
118 {settings?.enabled ? (
119 <Form method="post">
120 <Button type="submit" variant="quiet" disabled={busy} title="Build production again from the default branch">
121 <RotateCw size={14} />
122 Redeploy
123 </Button>
124 </Form>
125 ) : (
126 <ButtonLink to={`${base}/settings/deployments`} variant="accent">
127 <Rocket size={14} />
128 Deploy
129 </ButtonLink>
130 )}
131 </div>
132 )}
133 </div>
134 {(actionData && "notice" in actionData) || (actionData && "error" in actionData) ? (
135 <p className={`border-t border-line px-6 py-2.5 text-sm ${"error" in actionData ? "text-danger" : "text-accent"}`}>
136 {"error" in actionData ? actionData.error : actionData.notice}
137 </p>
138 ) : null}
139 {member && settings?.enabled && (
140 <div className="grid border-t border-line text-sm sm:grid-cols-3 sm:divide-x sm:divide-line">
141 <Link to={`${base}/deployments`} className="px-6 py-3 hover:bg-raised/50">
142 <span className="block text-xs text-muted">Previews up</span>
143 <span className="font-medium tabular-nums">{previews.length}</span>
144 </Link>
145 <Link to={`${base}/deployments`} className="px-6 py-3 hover:bg-raised/50">
146 <span className="block text-xs text-muted">Builds</span>
147 <span className="font-medium tabular-nums">{builds.length}</span>
148 </Link>
149 <Link to={`${base}/settings/secrets`} className="px-6 py-3 hover:bg-raised/50">
150 <span className="block text-xs text-muted">Secrets and variables</span>
151 <span className="font-medium">Manage</span>
152 </Link>
153 </div>
154 )}
155 </section>
156
157 {previews.length > 0 && (
158 <section>
159 <h2 className="text-sm font-medium text-muted">Previews</h2>
160 <ul className="mt-3 divide-y divide-line overflow-hidden rounded-xl border border-line bg-surface">
161 {previews.map((app) => (
162 <li key={app.url} className="flex items-center gap-3 px-4 py-3 text-sm">
163 <GitBranch size={14} className="shrink-0 text-faint" />
164 <span className="min-w-0 grow">
165 <a href={app.url} className="block truncate font-mono text-[0.8125rem] hover:text-accent">
166 {host(app.url)}
167 </a>
168 <span className="text-xs text-muted">
169 {app.branch}
170 {app.number != null && (
171 <>
172 {" · "}
173 <Link to={`${base}/pull/${app.number}`} className="hover:underline">
174 #{app.number}
175 </Link>
176 </>
177 )}
178 </span>
179 </span>
180 <span className="shrink-0 text-xs text-faint">
181 <TimeAgo at={app.deployedAt} />
182 </span>
183 </li>
184 ))}
185 </ul>
186 </section>
187 )}
188
189 <section>
190 <div className="flex items-center justify-between">
191 <h2 className="text-sm font-medium text-muted">In progress</h2>
192 <Link to={`${base}/pulls`} className="text-xs text-muted hover:text-fg">
193 All pull requests
194 </Link>
195 </div>
196 <div className="mt-3">
197 {pulls.length === 0 ? (
198 <EmptyState title="Nothing in progress">
199 Open an issue and assign it to g1t-agent, or push a branch and open a pull request.
200 </EmptyState>
201 ) : (
202 <ul className="divide-y divide-line overflow-hidden rounded-xl border border-line bg-surface">
203 {pulls.map((pull) => {
204 const preview = previews.find((app) => app.number === pull.number);
205 return (
206 <li key={pull.id}>
207 <Link
208 prefetch="intent"
209 to={`${base}/pull/${pull.number}`}
210 className="flex items-center gap-3 px-4 py-3 transition-colors hover:bg-raised"
211 >
212 <PullIcon status={pull.status} />
213 <span className="min-w-0 grow">
214 <span className="block truncate font-medium">{pull.title}</span>
215 <span className="font-mono text-xs text-muted">
216 #{pull.number} · {pull.author.username}
217 </span>
218 </span>
219 {preview && <span className="hidden text-xs text-accent sm:block">Preview live</span>}
220 <span className="w-14 shrink-0 text-right text-xs text-faint">
221 <TimeAgo at={pull.updatedAt} />
222 </span>
223 </Link>
224 </li>
225 );
226 })}
227 </ul>
228 )}
229 </div>
230 </section>
231
232 {member && builds.length > 0 && (
233 <section>
234 <div className="flex items-center justify-between">
235 <h2 className="text-sm font-medium text-muted">Recent builds</h2>
236 <Link to={`${base}/deployments`} className="text-xs text-muted hover:text-fg">
237 All deployments
238 </Link>
239 </div>
240 <ul className="mt-3 divide-y divide-line overflow-hidden rounded-xl border border-line bg-surface">
241 {builds.slice(0, 4).map((build) => (
242 <li key={build.id}>
243 <Link to={`${base}/deployments/${build.id}`} className="flex items-center gap-4 px-4 py-3 text-sm hover:bg-raised">
244 <span className="w-20 shrink-0">
245 <StatusDot status={build.status} />
246 </span>
247 <span className="min-w-0 grow truncate">
248 {build.kind === "production" ? "Production" : `Preview of ${build.branch}`}
249 <span className="ml-2 font-mono text-xs text-faint">{build.commit.slice(0, 7)}</span>
250 </span>
251 <span className="shrink-0 text-xs text-faint">
252 <TimeAgo at={build.createdAt} />
253 </span>
254 </Link>
255 </li>
256 ))}
257 </ul>
258 </section>
259 )}
260 </div>
261
262 <aside className="space-y-4 text-sm">
263 <section className="rounded-xl border border-line bg-surface p-5">
264 <h2 className="text-xs font-medium tracking-wide text-muted uppercase">Source</h2>
265 {source && (
266 <>
267 <Link to={`${base}/code`} className="mt-3 flex items-center gap-2 font-mono text-[0.8125rem] hover:text-accent">
268 {project?.private ? <Lock size={14} className="text-faint" /> : <Code2 size={14} className="text-faint" />}
269 {source.repo.namespace}/{source.repo.name}
270 </Link>
271 <p className="mt-1 text-xs text-muted">Hosted on g1t{source.rootDir ? ` · in ${source.rootDir}/` : ""}</p>
272 <dl className="mt-4 space-y-2 text-xs">
273 <div className="flex justify-between gap-3">
274 <dt className="text-muted">Default branch</dt>
275 <dd className="font-mono">{source.defaultBranch}</dd>
276 </div>
277 {commit && (
278 <div className="flex justify-between gap-3">
279 <dt className="text-muted">Latest commit</dt>
280 <dd className="min-w-0 truncate text-right">
281 <Link to={`${base}/commit/${commit.hash}`} className="font-mono hover:underline">
282 {commit.hash.slice(0, 7)}
283 </Link>
284 </dd>
285 </div>
286 )}
287 </dl>
288 {commit && <p className="mt-2 line-clamp-2 text-xs text-muted">{commit.message.split("\n")[0]}</p>}
289 <div className="mt-4">
290 <CopyLine text={`git clone https://g1t.sh/${source.repo.namespace}/${source.repo.name}.git`} />
291 </div>
292 </>
293 )}
294 </section>
295
296 <section className="grid grid-cols-2 gap-3">
297 <Link to={`${base}/issues`} className="rounded-xl border border-line bg-surface p-4 hover:border-line-strong">
298 <CircleDot size={14} className="text-faint" />
299 <p className="mt-2 text-xl font-semibold tabular-nums">{open.issues}</p>
300 <p className="text-xs text-muted">Open issues</p>
301 </Link>
302 <Link to={`${base}/pulls`} className="rounded-xl border border-line bg-surface p-4 hover:border-line-strong">
303 <GitPullRequest size={14} className="text-faint" />
304 <p className="mt-2 text-xl font-semibold tabular-nums">{open.pulls}</p>
305 <p className="text-xs text-muted">Pull requests</p>
306 </Link>
307 </section>
308
Project dependencies: addresses, preview stacks, Affects, and agents who know309 <section className="rounded-xl border border-line bg-surface p-5">
310 <div className="flex items-center justify-between">
311 <h2 className="flex items-center gap-1.5 text-xs font-medium tracking-wide text-muted uppercase">
312 <Network size={13} />
313 Dependencies
314 </h2>
315 {member && (
316 <Link to={`${base}/settings/dependencies`} className="text-xs text-muted hover:text-fg">
317 Manage
318 </Link>
319 )}
320 </div>
321 {dependencies.dependsOn.length === 0 && dependencies.usedBy.length === 0 ? (
322 <p className="mt-3 text-xs text-muted">
323 Uses no other project, and none uses it. Declare one, and builds get its address and agents know what
324 depends on what.
325 </p>
326 ) : (
327 <div className="mt-3 space-y-3 text-xs">
328 {dependencies.dependsOn.length > 0 && (
329 <div>
330 <p className="flex items-center gap-1 text-muted">
331 <ArrowUpRight size={12} /> Depends on
332 </p>
333 <ul className="mt-1.5 space-y-1">
334 {dependencies.dependsOn.map((d) => (
335 <li key={d.slug} className="flex items-center justify-between gap-2">
336 <Link to={`/${params.owner}/${d.slug}`} className="font-medium hover:underline">
337 {d.name}
338 </Link>
339 {d.as && <code className="font-mono text-faint">{d.as}</code>}
340 </li>
341 ))}
342 </ul>
343 </div>
344 )}
345 {dependencies.usedBy.length > 0 && (
346 <div>
347 <p className="flex items-center gap-1 text-muted">
348 <ArrowDownLeft size={12} /> Used by
349 </p>
350 <ul className="mt-1.5 space-y-1">
351 {dependencies.usedBy.map((d) => (
352 <li key={d.slug}>
353 <Link to={`/${params.owner}/${d.slug}`} className="font-medium hover:underline">
354 {d.name}
355 </Link>
356 </li>
357 ))}
358 </ul>
359 </div>
360 )}
361 </div>
362 )}
363 </section>
364
Projects: what a workspace builds and runs, first on every page365 {project && (
366 <section className="rounded-xl border border-line p-5 text-xs text-muted">
367 <p className="flex items-center gap-2 font-medium text-fg">
368 <Box size={14} className="text-faint" />
369 About projects
370 </p>
371 <p className="mt-1.5">
372 A project is what you build and run. Its deployments, environments, secrets and variables belong to it;
373 its code lives in its source.{" "}
374 <a href="https://docs.g1t.sh/guides/projects/" className="text-fg hover:underline">
375 Learn more
376 </a>
377 </p>
378 </section>
379 )}
380 </aside>
381 </div>
382 );
383}