g1t/apps/web/app/routes/repo/overview.tsx

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