Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

94 linesCodeBlameRaw
1import { ChartColumn, Plus, ShieldCheck } from "lucide-react";
2import { Link, data, redirect, useSearchParams } from "react-router";
3
4import type { Route } from "./+types/rules";
5import { page } from "../../lib/meta";
6import { InsightsView, RulesetList } from "../../components/rules";
7import { ButtonLink } from "../../components/ui";
8import { work } from "../../lib/services.server";
9import { getViewer, roleIn, unwrap } from "../../lib/session.server";
10import { movedTab, rulesAt, rulesPath } from "../../lib/tab-paths";
11
12export function meta({ params, ...args }: Route.MetaArgs) {
13 return page(args, { title: `Rules · ${params.owner} · g1t` });
14}
15
16export async function loader({ params, context, request }: Route.LoaderArgs) {
17 const viewer = getViewer(context);
18 const role = roleIn(viewer, params.owner);
19 if (!role) throw data(null, { status: 404 });
20 const url = new URL(request.url);
21 // Insights is a path: `?tab=insights`, the address before, is `/-/rules/insights`.
22 const moved = movedTab(url, (asked) => rulesPath(`/${params.owner}/-/rules`, asked));
23 if (moved) throw redirect(moved, 301);
24 const tab = rulesAt(url.pathname);
25 const owner = { workspace: params.owner };
26 const [rulesets, evaluations] = await Promise.all([
27 work.listRulesets(owner, viewer),
28 tab === "insights" ? work.ruleEvaluations(owner, viewer, { before: url.searchParams.get("before") ?? undefined }) : Promise.resolve(null),
29 ]);
30 return {
31 tab,
32 rulesets: unwrap(rulesets),
33 evaluations: evaluations?.ok ? evaluations.value : null,
34 editable: role === "owner",
35 };
36}
37
38export default function WorkspaceRules({ loaderData, params }: Route.ComponentProps) {
39 const { tab, rulesets, evaluations, editable } = loaderData;
40 const base = `/${params.owner}/-/rules`;
41 const [search] = useSearchParams();
42 const tabLink = (name: "rulesets" | "insights") => {
43 const next = new URLSearchParams(search);
44 next.delete("before");
45 const query = next.toString();
46 return `${rulesPath(base, name)}${query ? `?${query}` : ""}`;
47 };
48 return (
49 <div className="mx-auto max-w-4xl">
50 <header className="mb-6 border-b border-line pb-5">
51 <h1 className="text-lg font-semibold tracking-tight">Rules</h1>
52 <p className="mt-1 text-sm text-muted">
53 Rulesets that hold across {params.owner}'s repositories: which branches and tags they cover, who may bypass them, and what a pull
54 request needs before it merges. Each repository's own rulesets stack with these.
55 </p>
56 </header>
57 <nav className="mb-6 flex gap-1 border-b border-line" aria-label="Rules">
58 {(["rulesets", "insights"] as const).map((name) => (
59 <Link
60 key={name}
61 to={tabLink(name)}
62 aria-current={tab === name ? "page" : undefined}
63 className={`-mb-px flex items-center gap-1.5 border-b-2 px-3 py-2 text-sm ${tab === name ? "border-accent text-fg" : "border-transparent text-muted hover:text-fg"}`}
64 >
65 {name === "rulesets" ? <ShieldCheck size={14} /> : <ChartColumn size={14} />}
66 {name === "rulesets" ? "Rulesets" : "Insights"}
67 </Link>
68 ))}
69 </nav>
70 {tab === "rulesets" ? (
71 <section>
72 <div className="mb-3 flex flex-wrap items-center justify-between gap-3">
73 <p className="text-sm text-muted">{editable ? "Owners create and change them." : "Owners create and change them; you can read them."}</p>
74 {editable && (
75 <ButtonLink to={`${base}/new`}>
76 <Plus size={15} /> New ruleset
77 </ButtonLink>
78 )}
79 </div>
80 <RulesetList
81 rulesets={rulesets}
82 level="workspace"
83 hrefFor={(ruleset) => `${base}/${ruleset.id}`}
84 empty="No workspace rulesets yet. Each repository's own rulesets still hold."
85 />
86 </section>
87 ) : evaluations ? (
88 <InsightsView page={evaluations} showRepository olderHref={evaluations.next ? `${rulesPath(base, "insights")}?before=${encodeURIComponent(evaluations.next)}` : null} />
89 ) : (
90 <p className="text-sm text-muted">Insights could not be read just now.</p>
91 )}
92 </div>
93 );
94}