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

273 lines11,759 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 lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start1import { ArrowUpRight, Box, Check, ChevronDown, Download, Lock, Search } from "lucide-react";
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member2import { Form, Link, data } from "react-router";
3
4import { ECOSYSTEMS, type Ecosystem, type PackageSummary } from "@g1t/contracts";
5
6import type { Route } from "./+types/packages";
Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start7import { PackageIcon } from "../../components/package-icon";
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member8import { CopyLine, EmptyState, TimeAgo } from "../../components/ui";
9import { Badge } from "../../components/ui/badge";
Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start10import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../../components/ui/dropdown-menu";
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member11import { cn } from "../../lib/cn";
12import { page } from "../../lib/meta";
Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start13import {
14 ECOSYSTEM_LABEL,
15 type PackageSort,
16 REGISTRIES,
17 SORT_LABEL,
18 VISIBILITY_LABEL,
19 type VisibilityFilter,
20 arrange,
21 formatBytes,
22 shortCount,
23} from "../../lib/packages";
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member24import { packages } from "../../lib/services.server";
25import { getViewer, roleIn, unwrap } from "../../lib/session.server";
26
Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start27const DOCS = "https://docs.g1t.sh";
28
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member29export function meta({ params, ...args }: Route.MetaArgs) {
30 return page(args, { title: `Packages · ${params.owner} · g1t` });
31}
32
Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start33function pick<T extends string>(value: string | null, options: readonly T[], fallback: T): T {
34 return value && (options as readonly string[]).includes(value) ? (value as T) : fallback;
35}
36
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member37export async function loader({ params, context, request }: Route.LoaderArgs) {
38 const viewer = getViewer(context);
39 // Members only for now; a workspace's public packages are found through
40 // their repositories and search.
41 if (!roleIn(viewer, params.owner)) throw data(null, { status: 404 });
42 const url = new URL(request.url);
Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start43 const type = url.searchParams.get("type");
44 const ecosystem = type && (ECOSYSTEMS as readonly string[]).includes(type) ? (type as Ecosystem) : null;
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member45 const query = url.searchParams.get("q")?.trim() || null;
Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start46 const visibility = pick<VisibilityFilter>(url.searchParams.get("visibility"), ["all", "public", "private"], "all");
47 const sort = pick<PackageSort>(url.searchParams.get("sort"), ["updated", "downloads", "name"], "updated");
48 const found = unwrap(await packages.list(params.owner, viewer, { ecosystem, query }));
49 // Whether the workspace has any at all decides between a filtered-out
50 // list and the first-package page.
51 const any = found.length > 0 || ecosystem != null || query != null;
52 return {
53 list: arrange(found, visibility, sort),
54 any,
55 ecosystem,
56 query,
57 visibility,
58 sort,
59 workspace: params.owner.toLowerCase(),
60 };
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member61}
62
Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start63type Filters = { type: string | null; q: string | null; visibility: VisibilityFilter; sort: PackageSort };
64
65/** The page's address with one filter changed, defaults left out. */
66function hrefWith(workspace: string, current: Filters, change: Partial<Filters>): string {
67 const next = { ...current, ...change };
68 const params = new URLSearchParams();
69 if (next.type) params.set("type", next.type);
70 if (next.q) params.set("q", next.q);
71 if (next.visibility !== "all") params.set("visibility", next.visibility);
72 if (next.sort !== "updated") params.set("sort", next.sort);
73 return `/${workspace}/-/packages${params.size ? `?${params}` : ""}`;
74}
75
76/** The packages a workspace publishes: filtered, sorted, or, with none yet, how to start. */
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member77export default function Packages({ loaderData }: Route.ComponentProps) {
Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start78 const { list, any, ecosystem, query, visibility, sort, workspace } = loaderData;
79 if (!any) return <ChooseRegistry workspace={workspace} />;
80 const filters: Filters = { type: ecosystem, q: query, visibility, sort };
81 const filtered = ecosystem != null || query != null || visibility !== "all";
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member82 return (
Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start83 <div className="space-y-4">
84 <div className="flex flex-wrap items-center gap-2">
85 <FilterMenu
86 label="Type"
87 value={ecosystem ? ECOSYSTEM_LABEL[ecosystem] : "All"}
88 options={[null, ...ECOSYSTEMS].map((option) => ({
89 label: option ? ECOSYSTEM_LABEL[option] : "All",
90 to: hrefWith(workspace, filters, { type: option }),
91 current: ecosystem === option,
92 }))}
93 />
94 <Form method="get" className="relative min-w-48 grow">
95 <label className="sr-only" htmlFor="package-search">
96 Search packages
97 </label>
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member98 <Search size={14} className="pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-faint" />
99 <input
Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start100 id="package-search"
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member101 name="q"
102 defaultValue={query ?? ""}
Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start103 placeholder="Search packages"
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member104 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"
105 />
Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start106 {ecosystem && <input type="hidden" name="type" value={ecosystem} />}
107 {visibility !== "all" && <input type="hidden" name="visibility" value={visibility} />}
108 {sort !== "updated" && <input type="hidden" name="sort" value={sort} />}
109 </Form>
110 <FilterMenu
111 label="Visibility"
112 value={VISIBILITY_LABEL[visibility]}
113 options={(["all", "public", "private"] as const).map((option) => ({
114 label: VISIBILITY_LABEL[option],
115 to: hrefWith(workspace, filters, { visibility: option }),
116 current: visibility === option,
117 }))}
118 />
119 <FilterMenu
120 label="Sort"
121 value={SORT_LABEL[sort]}
122 options={(["updated", "downloads", "name"] as const).map((option) => ({
123 label: SORT_LABEL[option],
124 to: hrefWith(workspace, filters, { sort: option }),
125 current: sort === option,
126 }))}
127 />
128 </div>
129
130 <section className="overflow-hidden rounded-xl border border-line bg-surface">
131 <header className="flex items-center justify-between gap-3 border-b border-line px-4 py-2.5 text-sm">
132 <span className="font-medium">
133 {list.length} {list.length === 1 ? "package" : "packages"}
134 {filtered && (
135 <Link to={`/${workspace}/-/packages`} className="ml-3 text-xs font-normal text-muted hover:text-fg">
136 Clear filters
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member137 </Link>
Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start138 )}
139 </span>
140 <a href={`${DOCS}/guides/packages/`} className="inline-flex items-center gap-1 text-xs text-muted hover:text-fg">
141 How packages work <ArrowUpRight size={12} />
142 </a>
143 </header>
144 {list.length === 0 ? (
145 <div className="p-4">
146 <EmptyState title="No packages match">Try another type or visibility, or clear the search.</EmptyState>
147 </div>
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member148 ) : (
Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start149 <ul className="divide-y divide-line">
150 {list.map((pkg) => (
151 <PackageRow key={pkg.id} pkg={pkg} />
152 ))}
153 </ul>
154 )}
155 </section>
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member156 </div>
157 );
158}
159
Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start160/** A filter as a button that opens its choices; each choice is a link, so the address says the view. */
161function FilterMenu({ label, value, options }: { label: string; value: string; options: { label: string; to: string; current: boolean }[] }) {
162 return (
163 <DropdownMenu>
164 <DropdownMenuTrigger className="inline-flex h-9 items-center gap-1.5 rounded-lg border border-line bg-surface px-3 text-sm transition-colors hover:border-line-strong data-[state=open]:border-line-strong">
165 <span className="text-muted">{label}:</span>
166 <span className="font-medium">{value}</span>
167 <ChevronDown size={13} className="text-faint" />
168 </DropdownMenuTrigger>
169 <DropdownMenuContent align="start" className="min-w-44">
170 {options.map((option) => (
171 <DropdownMenuItem key={option.label} asChild>
172 <Link to={option.to} preventScrollReset>
173 <Check size={14} className={cn(!option.current && "invisible")} />
174 {option.label}
175 </Link>
176 </DropdownMenuItem>
177 ))}
178 </DropdownMenuContent>
179 </DropdownMenu>
180 );
181}
182
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member183function PackageRow({ pkg }: { pkg: PackageSummary }) {
184 return (
185 <li>
186 <Link
187 to={`/${pkg.workspace}/-/packages/${pkg.ecosystem}/${pkg.name}`}
Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start188 className="flex items-center gap-3.5 px-4 py-3.5 transition-colors hover:bg-raised/50"
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member189 >
Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start190 <PackageIcon ecosystem={pkg.ecosystem} />
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member191 <span className="min-w-0 grow">
192 <span className="flex flex-wrap items-center gap-2">
193 <span className="truncate font-medium">{pkg.name}</span>
Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start194 {pkg.visibility === "private" && (
195 <Badge>
196 <Lock size={10} />
197 Private
198 </Badge>
199 )}
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member200 </span>
Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start201 <span className="mt-0.5 flex flex-wrap items-center gap-x-1.5 text-xs text-muted">
202 <span>{ECOSYSTEM_LABEL[pkg.ecosystem]}</span>
203 <span className="text-faint">·</span>
204 <span>
205 Updated <TimeAgo at={pkg.updated_at} />
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member206 </span>
Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start207 {pkg.repo && (
208 <>
209 <span className="text-faint">·</span>
210 <span className="inline-flex items-center gap-1">
211 <Box size={11} />
212 {pkg.repo.name}
213 </span>
214 </>
215 )}
216 {pkg.description && (
217 <>
218 <span className="text-faint">·</span>
219 <span className="max-w-md truncate">{pkg.description}</span>
220 </>
221 )}
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member222 </span>
Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start223 </span>
224 <span className="hidden shrink-0 items-center gap-5 text-xs text-muted tabular-nums sm:flex">
225 {pkg.latest && <span className="max-w-32 truncate font-mono text-fg-soft">{pkg.latest}</span>}
226 <span className="w-16 text-right">{formatBytes(pkg.size)}</span>
227 <span className="inline-flex w-14 items-center justify-end gap-1" title={`${pkg.downloads.toLocaleString("en-US")} downloads`}>
228 <Download size={12} />
229 {shortCount(pkg.downloads)}
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member230 </span>
231 </span>
232 </Link>
233 </li>
234 );
235}
236
Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start237/** With no packages yet: every registry, what it is for, and the first step. */
238function ChooseRegistry({ workspace }: { workspace: string }) {
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member239 return (
Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start240 <div className="space-y-6">
241 <div className="max-w-2xl">
242 <h2 className="text-lg font-semibold tracking-tight">Choose a registry</h2>
243 <p className="mt-1 text-sm text-muted">
244 Packages live beside the code that makes them, with the same members, roles and tokens. Workflows publish with{" "}
245 <code className="font-mono text-xs">G1T_TOKEN</code>.
246 </p>
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member247 </div>
Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start248 <ul className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
249 {REGISTRIES.map((registry) => (
250 <li key={registry.ecosystem} className="flex flex-col rounded-xl border border-line bg-surface p-4">
251 <div className="flex items-center gap-2.5">
252 <PackageIcon ecosystem={registry.ecosystem} />
253 <span className="font-medium">{ECOSYSTEM_LABEL[registry.ecosystem]}</span>
254 {!registry.ready && <Badge>Soon</Badge>}
255 </div>
256 <p className="mt-2.5 grow text-sm text-muted">{registry.blurb}</p>
257 {registry.ready && (
258 <div className="mt-3">
259 <CopyLine prompt text={registry.start.replaceAll("<workspace>", workspace)} />
260 </div>
261 )}
262 <a
263 href={`${DOCS}${registry.guide}`}
264 className="mt-3 inline-flex items-center gap-1 self-start text-sm text-fg-soft hover:text-fg"
265 >
266 {registry.ready ? "Set it up" : "What's planned"} <ArrowUpRight size={13} />
267 </a>
268 </li>
269 ))}
270 </ul>
271 </div>
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member272 );
273}