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

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