Skip to content

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

92 lines4,060 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.

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

This file's history is long; its oldest lines are credited to the oldest commit read.