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

325 lines13,807 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.

Projects: what a workspace builds and runs, first on every page1import { ArrowRight, ArrowUpRight, Box, CircleDot, GitBranch, GitPullRequest, KeyRound, Lock, Plus, Rocket } from "lucide-react";
Agents as a team: lifecycle, merge queue, billing and a new shell2import { Link } from "react-router";
3
Projects: what a workspace builds and runs, first on every page4import type { Project, ProjectDeploys } from "@g1t/contracts";
5
Agents as a team: lifecycle, merge queue, billing and a new shell6import type { Route } from "./+types/overview";
Projects: what a workspace builds and runs, first on every page7import { host, StatusDot } from "../../components/deploy";
Agents as a team: lifecycle, merge queue, billing and a new shell8import { Avatar, ButtonLink, CopyLine, Pill, TimeAgo } from "../../components/ui";
9import { PullIcon } from "../../components/work";
Projects: what a workspace builds and runs, first on every page10import { deployments, identity, projects as projectsApi, work } from "../../lib/services.server";
Agents as a team: lifecycle, merge queue, billing and a new shell11import { getViewer, roleIn } from "../../lib/session.server";
12
Projects: what a workspace builds and runs, first on every page13/** Projects whose open issues and pull requests are counted. */
Agents as a team: lifecycle, merge queue, billing and a new shell14const MAX_COUNTED = 30;
Projects: what a workspace builds and runs, first on every page15/** Projects whose pull requests are listed under "In progress". */
Agents as a team: lifecycle, merge queue, billing and a new shell16const MAX_LISTED = 8;
17const MAX_PULLS = 8;
18const MAX_FACES = 8;
19
20export async function loader({ params, context }: Route.LoaderArgs) {
21 const viewer = getViewer(context);
22 const slug = params.owner.toLowerCase();
23 const role = roleIn(viewer, slug);
Projects: what a workspace builds and runs, first on every page24 const [listed, members, deploys] = await Promise.all([
25 projectsApi.list(slug, viewer),
Agents as a team: lifecycle, merge queue, billing and a new shell26 role ? identity.listMembers(slug, viewer) : null,
Projects: what a workspace builds and runs, first on every page27 role ? deployments.overview(slug, viewer) : null,
Agents as a team: lifecycle, merge queue, billing and a new shell28 ]);
Projects: what a workspace builds and runs, first on every page29 const projects = listed.ok ? listed.value : [];
Agents as a team: lifecycle, merge queue, billing and a new shell30
Projects: what a workspace builds and runs, first on every page31 const pathOf = (project: Project) =>
32 project.source.kind === "hosted" ? project.source.repo : { namespace: project.workspace, name: project.slug };
33 const counted = projects.slice(0, MAX_COUNTED);
34 const counts = await Promise.all(counted.map((project) => work.counts(pathOf(project), viewer)));
Agents as a team: lifecycle, merge queue, billing and a new shell35 const open: Record<string, { issues: number; pulls: number }> = {};
Projects: what a workspace builds and runs, first on every page36 counted.forEach((project, i) => {
Agents as a team: lifecycle, merge queue, billing and a new shell37 const result = counts[i];
Projects: what a workspace builds and runs, first on every page38 if (result.ok) open[project.id] = result.value;
Agents as a team: lifecycle, merge queue, billing and a new shell39 });
40
Projects: what a workspace builds and runs, first on every page41 const busy = counted.filter((project) => (open[project.id]?.pulls ?? 0) > 0).slice(0, MAX_LISTED);
42 const lists = await Promise.all(busy.map((project) => work.listPulls(pathOf(project), viewer, "open")));
Agents as a team: lifecycle, merge queue, billing and a new shell43 const pulls = lists
Projects: what a workspace builds and runs, first on every page44 .flatMap((result, i) => (result.ok ? result.value.map((pull) => ({ pull, project: busy[i] })) : []))
Agents as a team: lifecycle, merge queue, billing and a new shell45 .sort((a, b) => b.pull.updatedAt.localeCompare(a.pull.updatedAt))
46 .slice(0, MAX_PULLS);
47
Projects: what a workspace builds and runs, first on every page48 const bySlug: Record<string, ProjectDeploys> = {};
49 for (const entry of deploys?.ok ? deploys.value : []) bySlug[entry.slug] = entry;
50
51 return {
52 slug,
53 role,
54 projects,
55 open,
56 deploys: bySlug,
57 pulls: pulls.map(({ pull, project }) => ({ pull, slug: project.slug, name: project.name })),
58 members: members?.ok ? members.value : null,
59 };
Agents as a team: lifecycle, merge queue, billing and a new shell60}
61
62function Stat({ value, label }: { value: number; label: string }) {
63 return (
64 <div className="rounded-xl border border-line bg-surface px-4 py-3">
65 <p className="text-xl font-semibold tabular-nums tracking-tight">{value}</p>
66 <p className="text-xs text-muted">{label}</p>
67 </div>
68 );
69}
70
Projects: what a workspace builds and runs, first on every page71/** What a member sees in a workspace with no projects yet. */
Agents as a team: lifecycle, merge queue, billing and a new shell72function GetStarted({ slug }: { slug: string }) {
73 return (
Projects: what a workspace builds and runs, first on every page74 <div className="rounded-2xl border border-line bg-surface p-8">
75 <span className="flex size-10 items-center justify-center rounded-xl bg-accent/10 text-accent ring-1 ring-accent/30">
76 <Box size={18} />
77 </span>
78 <h2 className="mt-4 text-lg font-semibold tracking-tight">Create your first project</h2>
Agents as a team: lifecycle, merge queue, billing and a new shell79 <p className="mt-1.5 max-w-xl text-sm text-muted">
Projects: what a workspace builds and runs, first on every page80 A project is what you build and run: its code, the issues that say what should change, the agents and
81 people who change it, and where it is deployed.
Agents as a team: lifecycle, merge queue, billing and a new shell82 </p>
Projects: what a workspace builds and runs, first on every page83 <div className="mt-6 flex flex-wrap gap-3">
84 <ButtonLink to={`/new?workspace=${slug}`} variant="accent">
85 <Plus size={14} />
86 New project
87 </ButtonLink>
Agents as a team: lifecycle, merge queue, billing and a new shell88 </div>
Projects: what a workspace builds and runs, first on every page89 <p className="mt-6 text-sm text-muted">Or push code you already have, and it becomes a project:</p>
Agents as a team: lifecycle, merge queue, billing and a new shell90 <div className="mt-2 max-w-xl">
91 <CopyLine prompt text={`git push https://g1t.sh/${slug}/my-project.git main`} />
92 </div>
93 </div>
94 );
95}
96
Projects: what a workspace builds and runs, first on every page97function ProjectCard({
98 project,
99 open,
100 deploys,
101 member,
102}: {
103 project: Project;
104 open: { issues: number; pulls: number } | undefined;
105 deploys: ProjectDeploys | undefined;
106 member: boolean;
107}) {
108 const base = `/${project.workspace}/${project.slug}`;
109 const production = deploys?.production ?? null;
110 const latest = deploys?.latest ?? null;
111 const source = project.source.kind === "hosted" ? project.source : null;
112 return (
113 <li className="group relative flex flex-col rounded-2xl border border-line bg-surface p-5 transition-colors hover:border-line-strong">
114 <div className="flex items-start gap-3">
115 <span className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-raised text-muted ring-1 ring-line">
116 {project.private ? <Lock size={15} /> : <Box size={15} />}
117 </span>
118 <div className="min-w-0 grow">
119 <Link to={base} prefetch="intent" className="font-medium after:absolute after:inset-0 hover:underline">
120 {project.name}
121 </Link>
122 {production ? (
123 <a
124 href={production.url}
125 className="relative z-10 mt-0.5 flex items-center gap-1 truncate font-mono text-xs text-muted hover:text-accent"
126 >
127 {host(production.url)}
128 <ArrowUpRight size={11} className="shrink-0" />
129 </a>
130 ) : (
131 <p className="mt-0.5 truncate text-xs text-faint">{project.description ?? "Not deployed"}</p>
132 )}
133 </div>
134 {project.private && <Pill>private</Pill>}
135 </div>
136
137 {project.description && production && <p className="mt-3 line-clamp-2 text-sm text-muted">{project.description}</p>}
138
139 <div className="mt-auto pt-5">
140 {member && latest && (
141 <p className="mb-3 flex items-center gap-2 text-xs text-muted">
142 <StatusDot status={latest.status} />
143 <span className="truncate">
144 {latest.kind === "production" ? "Production" : `Preview of ${latest.branch}`}
145 </span>
146 <span className="ml-auto shrink-0 text-faint">
147 <TimeAgo at={latest.createdAt} />
148 </span>
149 </p>
150 )}
151 <div className="flex items-center gap-4 border-t border-line pt-3 text-xs text-muted">
152 {source && (
153 <span className="flex min-w-0 items-center gap-1.5 font-mono">
154 <GitBranch size={12} className="shrink-0 text-faint" />
155 <span className="truncate">{source.defaultBranch}</span>
156 </span>
157 )}
158 <span className="flex items-center gap-1">
159 <CircleDot size={12} className="text-faint" />
160 {open?.issues ?? 0}
161 </span>
162 <span className="flex items-center gap-1">
163 <GitPullRequest size={12} className="text-faint" />
164 {open?.pulls ?? 0}
165 </span>
166 {member && deploys?.enabled && (deploys.previews ?? 0) > 0 && (
167 <span className="ml-auto flex items-center gap-1 text-accent">
168 <Rocket size={12} />
169 {deploys.previews} {deploys.previews === 1 ? "preview" : "previews"}
170 </span>
171 )}
172 </div>
173 </div>
174 </li>
175 );
176}
177
Agents as a team: lifecycle, merge queue, billing and a new shell178export default function WorkspaceOverview({ loaderData }: Route.ComponentProps) {
Projects: what a workspace builds and runs, first on every page179 const { slug, role, projects, open, deploys, pulls, members } = loaderData;
Agents as a team: lifecycle, merge queue, billing and a new shell180 const totals = Object.values(open).reduce(
Projects: what a workspace builds and runs, first on every page181 (sum, counts) => ({ issues: sum.issues + counts.issues, pulls: sum.pulls + counts.pulls }),
Agents as a team: lifecycle, merge queue, billing and a new shell182 { issues: 0, pulls: 0 },
183 );
Projects: what a workspace builds and runs, first on every page184 const liveApps = Object.values(deploys).filter((entry) => entry.production).length;
Agents as a team: lifecycle, merge queue, billing and a new shell185 return (
186 <div className="grid gap-10 lg:grid-cols-[1fr_18rem]">
187 <div className="min-w-0 space-y-10">
Projects: what a workspace builds and runs, first on every page188 {projects.length === 0 && role ? (
Agents as a team: lifecycle, merge queue, billing and a new shell189 <GetStarted slug={slug} />
190 ) : (
191 <>
Projects: what a workspace builds and runs, first on every page192 <div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
193 <Stat value={projects.length} label={projects.length === 1 ? "Project" : "Projects"} />
194 <Stat value={liveApps} label="In production" />
Agents as a team: lifecycle, merge queue, billing and a new shell195 <Stat value={totals.issues} label="Open issues" />
196 <Stat value={totals.pulls} label="Pull requests in progress" />
197 </div>
198
Projects: what a workspace builds and runs, first on every page199 <section>
200 <div className="flex items-center justify-between">
201 <h2 className="text-sm font-medium text-muted">Projects</h2>
202 {role && (
203 <Link to={`/new?workspace=${slug}`} className="inline-flex items-center gap-1 text-xs text-muted hover:text-fg">
204 <Plus size={13} />
205 New project
206 </Link>
207 )}
208 </div>
209 <ul className="mt-3 grid gap-4 sm:grid-cols-2">
210 {projects.map((project) => (
211 <ProjectCard
212 key={project.id}
213 project={project}
214 open={open[project.id]}
215 deploys={deploys[project.slug]}
216 member={role != null}
217 />
218 ))}
219 </ul>
220 </section>
221
Agents as a team: lifecycle, merge queue, billing and a new shell222 {pulls.length > 0 && (
223 <section>
224 <h2 className="text-sm font-medium text-muted">In progress</h2>
225 <ul className="mt-3 divide-y divide-line overflow-hidden rounded-xl border border-line bg-surface">
Projects: what a workspace builds and runs, first on every page226 {pulls.map(({ pull, slug: project, name }) => (
Agents as a team: lifecycle, merge queue, billing and a new shell227 <li key={pull.id}>
228 <Link
229 prefetch="intent"
Projects: what a workspace builds and runs, first on every page230 to={`/${slug}/${project}/pull/${pull.number}`}
Agents as a team: lifecycle, merge queue, billing and a new shell231 className="flex items-center gap-3 px-4 py-3 transition-colors hover:bg-raised"
232 >
233 <PullIcon status={pull.status} />
234 <span className="min-w-0 grow">
235 <span className="block truncate font-medium">{pull.title}</span>
Projects: what a workspace builds and runs, first on every page236 <span className="text-xs text-muted">
237 {name} <span className="font-mono">#{pull.number}</span>
238 {pull.issue != null && ` · for #${pull.issue}`} · {pull.author.username}
Agents as a team: lifecycle, merge queue, billing and a new shell239 </span>
240 </span>
241 <span className="hidden shrink-0 text-xs text-muted sm:block">
242 {pull.status === "draft" ? "Being worked on" : "Ready for review"}
243 </span>
244 <span className="w-14 shrink-0 text-right text-xs text-faint">
245 <TimeAgo at={pull.updatedAt} />
246 </span>
247 </Link>
248 </li>
249 ))}
250 </ul>
251 </section>
252 )}
253 </>
254 )}
255 </div>
256
257 <aside className="space-y-6">
258 {members ? (
259 <section>
260 <div className="flex items-center justify-between">
Projects: what a workspace builds and runs, first on every page261 <h2 className="text-sm font-medium">Members</h2>
Agents as a team: lifecycle, merge queue, billing and a new shell262 <Link to={`/${slug}/-/people`} className="text-xs text-muted hover:text-fg">
263 {role === "owner" ? "Manage" : "See all"}
264 </Link>
265 </div>
266 <ul className="mt-3 space-y-1.5">
267 {members.slice(0, MAX_FACES).map((member) => (
268 <li key={member.username} className="flex items-center gap-2 text-sm">
269 <Avatar name={member.username} />
270 <span className="grow truncate font-mono">{member.username}</span>
271 {member.role === "owner" && <Pill>owner</Pill>}
272 </li>
273 ))}
274 </ul>
Projects: what a workspace builds and runs, first on every page275 {members.length > MAX_FACES && <p className="mt-2 text-xs text-faint">and {members.length - MAX_FACES} more</p>}
Agents as a team: lifecycle, merge queue, billing and a new shell276 </section>
277 ) : (
278 <p className="text-sm text-muted">
Projects: what a workspace builds and runs, first on every page279 Members of this workspace build its projects. Anyone can open an issue or a pull request on a public one.
Agents as a team: lifecycle, merge queue, billing and a new shell280 </p>
281 )}
282
283 {role && (
284 <section className="rounded-xl border border-line bg-surface p-5">
285 <h2 className="flex items-center gap-2 font-medium">
Projects: what a workspace builds and runs, first on every page286 <Rocket size={15} className="text-faint" />
287 Deploy on g1t.page
288 </h2>
289 <p className="mt-1.5 text-sm text-muted">
290 Production from each project's default branch, and a live preview for every pull request. Apps cost
291 nothing while no one visits.
292 </p>
293 <Link
294 prefetch="intent"
295 to={`/${slug}/-/billing`}
296 className="mt-3 inline-flex items-center gap-1 text-sm text-accent hover:underline"
297 >
298 Billing and plans <ArrowRight size={13} />
299 </Link>
300 </section>
301 )}
302
303 {role && (
304 <section className="rounded-xl border border-line bg-surface p-5">
305 <h2 className="flex items-center gap-2 font-medium">
Agents as a team: lifecycle, merge queue, billing and a new shell306 <KeyRound size={15} className="text-faint" />
307 Automate without a service account
308 </h2>
309 <p className="mt-1.5 text-sm text-muted">
Projects: what a workspace builds and runs, first on every page310 A workspace has access tokens of its own for CI, integrations and agents. They act as the workspace,
311 not as a person.
Agents as a team: lifecycle, merge queue, billing and a new shell312 </p>
313 <Link
314 prefetch="intent"
315 to={`/${slug}/-/tokens`}
316 className="mt-3 inline-flex items-center gap-1 text-sm text-accent hover:underline"
317 >
318 Access tokens <ArrowRight size={13} />
319 </Link>
320 </section>
321 )}
322 </aside>
323 </div>
324 );
325}