g1t/apps/web/app/routes/workspace/packages.tsx

156 lines6,754 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.

Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member1import { Box, Lock, Package, Search } from "lucide-react";
2import { Form, Link, data } from "react-router";
3
4import { ECOSYSTEMS, type Ecosystem, type PackageSummary } from "@g1t/contracts";
5
6import type { Route } from "./+types/packages";
7import { CopyLine, EmptyState, TimeAgo } from "../../components/ui";
8import { Badge } from "../../components/ui/badge";
9import { cn } from "../../lib/cn";
10import { page } from "../../lib/meta";
11import { ECOSYSTEM_LABEL, formatBytes } from "../../lib/packages";
12import { packages } from "../../lib/services.server";
13import { getViewer, roleIn, unwrap } from "../../lib/session.server";
14
15export function meta({ params, ...args }: Route.MetaArgs) {
16 return page(args, { title: `Packages · ${params.owner} · g1t` });
17}
18
19export async function loader({ params, context, request }: Route.LoaderArgs) {
20 const viewer = getViewer(context);
21 // Members only for now; a workspace's public packages are found through
22 // their repositories and search.
23 if (!roleIn(viewer, params.owner)) throw data(null, { status: 404 });
24 const url = new URL(request.url);
25 const asked = url.searchParams.get("type");
26 const ecosystem = asked && (ECOSYSTEMS as readonly string[]).includes(asked) ? (asked as Ecosystem) : null;
27 const query = url.searchParams.get("q")?.trim() || null;
28 const list = unwrap(await packages.list(params.owner, viewer, { ecosystem, query }));
29 return { list, ecosystem, query, workspace: params.owner.toLowerCase(), username: viewer?.username ?? "you" };
30}
31
32/** The packages a workspace publishes, by registry, with how to start one. */
33export default function Packages({ loaderData }: Route.ComponentProps) {
34 const { list, ecosystem, query, workspace, username } = loaderData;
35 const filtered = ecosystem != null || query != null;
36 return (
37 <div className="space-y-6">
38 <div className="flex justify-end">
39 <a href="https://docs.g1t.sh/guides/packages/" className="text-sm text-muted hover:text-fg">
40 How packages work
41 </a>
42 </div>
43
44 <Form method="get" className="flex flex-wrap items-center gap-2">
45 <label className="relative min-w-0 grow">
46 <span className="sr-only">Search packages</span>
47 <Search size={14} className="pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-faint" />
48 <input
49 name="q"
50 defaultValue={query ?? ""}
51 placeholder="Find a package"
52 className="h-9 w-full rounded-lg border border-line bg-surface pr-3 pl-8 text-sm placeholder:text-faint focus:border-line-strong focus:outline-none"
53 />
54 </label>
55 {ecosystem && <input type="hidden" name="type" value={ecosystem} />}
56 <nav aria-label="Registry" className="flex flex-wrap gap-1 text-sm">
57 {[null, ...ECOSYSTEMS].map((option) => {
58 const params = new URLSearchParams();
59 if (option) params.set("type", option);
60 if (query) params.set("q", query);
61 const href = `/${workspace}/-/packages${params.size ? `?${params}` : ""}`;
62 return (
63 <Link
64 key={option ?? "all"}
65 to={href}
66 aria-current={ecosystem === option ? "page" : undefined}
67 className={cn(
68 "rounded-md px-2.5 py-1.5 transition-colors",
69 ecosystem === option ? "bg-raised text-fg" : "text-muted hover:text-fg",
70 )}
71 >
72 {option ? ECOSYSTEM_LABEL[option] : "All"}
73 </Link>
74 );
75 })}
76 </nav>
77 </Form>
78
79 {list.length === 0 ? (
80 filtered ? (
81 <EmptyState title="No packages match">Try another registry, or clear the search.</EmptyState>
82 ) : (
83 <FirstPackage workspace={workspace} username={username} />
84 )
85 ) : (
86 <ul className="divide-y divide-line overflow-hidden rounded-xl border border-line bg-surface">
87 {list.map((pkg) => (
88 <PackageRow key={pkg.id} pkg={pkg} />
89 ))}
90 </ul>
91 )}
92 </div>
93 );
94}
95
96function PackageRow({ pkg }: { pkg: PackageSummary }) {
97 return (
98 <li>
99 <Link
100 to={`/${pkg.workspace}/-/packages/${pkg.ecosystem}/${pkg.name}`}
101 className="flex flex-wrap items-center gap-x-4 gap-y-2 px-4 py-3.5 transition-colors hover:bg-raised/50"
102 >
103 <span className="text-faint">{pkg.visibility === "private" ? <Lock size={15} /> : <Package size={15} />}</span>
104 <span className="min-w-0 grow">
105 <span className="flex flex-wrap items-center gap-2">
106 <span className="truncate font-medium">{pkg.name}</span>
107 <Badge>{ECOSYSTEM_LABEL[pkg.ecosystem]}</Badge>
108 {pkg.visibility === "private" && <Badge>Private</Badge>}
109 </span>
110 <span className="mt-0.5 block truncate font-mono text-xs text-faint">{pkg.address}</span>
111 </span>
112 <span className="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted tabular-nums">
113 {pkg.repo && (
114 <span className="inline-flex items-center gap-1">
115 <Box size={12} />
116 {pkg.repo.name}
117 </span>
118 )}
119 {pkg.latest && <span className="font-mono">{pkg.latest}</span>}
120 <span>
121 {pkg.versions} {pkg.versions === 1 ? "version" : "versions"}
122 </span>
123 <span>{formatBytes(pkg.size)}</span>
124 <span>
125 Updated <TimeAgo at={pkg.updated_at} />
126 </span>
127 </span>
128 </Link>
129 </li>
130 );
131}
132
133/** With no packages yet: how to push the first image. */
134function FirstPackage({ workspace, username }: { workspace: string; username: string }) {
135 return (
136 <section className="rounded-xl border border-line bg-surface p-6">
137 <h2 className="font-medium">Push the workspace's first image</h2>
138 <p className="mt-1 text-sm text-muted">
139 Log in with a token that may write packages, tag an image under the workspace, and push it. An image named after one of the
140 workspace's repositories is linked to it and has its access.
141 </p>
142 <div className="mt-4 space-y-2">
143 <CopyLine prompt text={`docker login g1t.sh -u ${username}`} />
144 <CopyLine prompt text={`docker tag web g1t.sh/${workspace}/web:latest`} />
145 <CopyLine prompt text={`docker push g1t.sh/${workspace}/web:latest`} />
146 </div>
147 <p className="mt-4 text-xs text-faint">
148 In a workflow, log in with <code className="font-mono">G1T_TOKEN</code>. See{" "}
149 <a href="https://docs.g1t.sh/guides/containers/" className="underline hover:text-fg">
150 Containers
151 </a>
152 .
153 </p>
154 </section>
155 );
156}