Skip to content

g1t/apps/web/app/routes/workspace/security-patterns.tsx

121 lines5,689 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.

Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1import { Plus } from "lucide-react";
2import { useState } from "react";
3import { data } from "react-router";
4
5import type { CustomPattern } from "@g1t/contracts";
6
7import type { Route } from "./+types/security-patterns";
8import { page } from "../../lib/meta";
9import { CARD, PatternEditor, patternFields } from "../../components/security-suite";
10import { WorkspaceSecurityHeading, WorkspaceSecurityTabs } from "../../components/workspace-security-tabs";
11import { Badge } from "../../components/ui/badge";
12import { securitySuite } from "../../lib/services.server";
Merge membership: owners, org roles, GitHub's repo roles, privileges, 2FA13import { assertSameOrigin, getViewer, managesSecurity, requireUser, roleIn, unwrap } from "../../lib/session.server";
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar14
15export function meta({ params, ...args }: Route.MetaArgs) {
16 return page(args, { title: `Custom patterns · ${params.owner} · g1t` });
17}
18
19export async function loader({ params, context }: Route.LoaderArgs) {
20 const viewer = getViewer(context);
21 const role = roleIn(viewer, params.owner);
22 if (!role) throw data(null, { status: 404 });
Merge membership: owners, org roles, GitHub's repo roles, privileges, 2FA23 return { list: unwrap(await securitySuite.patterns(params.owner, null, viewer)), owner: managesSecurity(viewer, params.owner) };
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar24}
25
26export async function action({ params, context, request }: Route.ActionArgs) {
27 assertSameOrigin(request);
28 const user = requireUser(context, request);
29 const form = await request.formData();
30 const intent = String(form.get("intent") ?? "");
31 if (intent === "save_pattern") {
32 const saved = await securitySuite.savePattern(user, params.owner, null, patternFields(form));
33 return saved.ok ? { ok: true, saved: saved.value } : { ok: false, error: saved.error.message };
34 }
35 if (intent === "dry_run") {
36 const ran = await securitySuite.dryRun(user, params.owner, null, patternFields(form));
37 return ran.ok ? { ok: true, dryRun: ran.value } : { ok: false, error: ran.error.message };
38 }
39 if (intent === "delete_pattern") {
40 const deleted = await securitySuite.deletePattern(user, params.owner, null, String(form.get("id") ?? ""));
41 return deleted.ok ? { ok: true } : { ok: false, error: deleted.error.message };
42 }
43 return { ok: false, error: "Unknown action." };
44}
45
46function draftOf(pattern: CustomPattern | null) {
47 return {
48 id: pattern?.id,
49 name: pattern?.name ?? "",
50 pattern: pattern?.pattern ?? "",
51 before: pattern?.before ?? "",
52 after: pattern?.after ?? "",
53 testStrings: (pattern?.testStrings ?? []).join("\n"),
54 published: pattern?.state === "published",
55 };
56}
57
58export default function WorkspacePatterns({ loaderData, params }: Route.ComponentProps) {
59 const { list, owner } = loaderData;
60 const [editing, setEditing] = useState<CustomPattern | "new" | null>(null);
61 return (
62 <div>
63 <WorkspaceSecurityHeading
64 title="Custom patterns"
65 about="Secret formats of the workspace's own, found in every repository's pushes and history alongside the built-in ones. Without the Security and quality activation they cover public repositories only."
66 />
67 <WorkspaceSecurityTabs owner={params.owner} />
68 {!list.entitled && (
69 <p className="mb-4 rounded-md border border-warn/30 bg-warn/5 px-3 py-2 text-sm text-warn">
70 This workspace has no Security and quality activation: its patterns run on public repositories only. An owner can turn it
71 on in Billing.
72 </p>
73 )}
74 {owner && editing === null && (
75 <button
76 type="button"
77 onClick={() => setEditing("new")}
78 className="mb-4 inline-flex items-center gap-1.5 rounded-md border border-line px-3 py-1.5 text-sm hover:border-line-strong"
79 >
80 <Plus size={14} /> New pattern
81 </button>
82 )}
83 {editing !== null && (
84 <section className={`${CARD} mb-6 p-4`}>
85 <div className="mb-3 flex items-center justify-between">
86 <h2 className="text-base font-semibold">{editing === "new" ? "New pattern" : editing.name}</h2>
87 <button type="button" onClick={() => setEditing(null)} className="text-sm text-muted hover:text-fg">
88 Close
89 </button>
90 </div>
91 <PatternEditor key={editing === "new" ? "new" : editing.id} action={`/${params.owner}/-/security/patterns`} draft={draftOf(editing === "new" ? null : editing)} />
92 </section>
93 )}
94 {list.patterns.length === 0 ? (
95 <p className="rounded-xl border border-dashed border-line px-4 py-6 text-sm text-muted">No custom patterns yet.</p>
96 ) : (
97 <ul className={`${CARD} divide-y divide-line`}>
98 {list.patterns.map((pattern) => (
99 <li key={pattern.id} className="flex flex-col gap-1 px-4 py-3 sm:flex-row sm:items-center sm:gap-3">
100 <div className="min-w-0 grow">
101 <div className="flex flex-wrap items-center gap-2">
102 <span className="text-sm font-medium">{pattern.name}</span>
103 <Badge tone={pattern.state === "published" ? "accent" : "neutral"}>{pattern.state === "published" ? "Published" : "Draft"}</Badge>
104 </div>
105 <p className="mt-0.5 truncate font-mono text-xs text-muted">{pattern.pattern}</p>
106 </div>
107 <span className="shrink-0 text-xs text-faint">
108 {pattern.openAlerts} open {pattern.openAlerts === 1 ? "alert" : "alerts"}
109 </span>
110 {owner && (
111 <button type="button" onClick={() => setEditing(pattern)} className="shrink-0 rounded-md border border-line px-2.5 py-1 text-xs text-muted hover:text-fg">
112 Edit
113 </button>
114 )}
115 </li>
116 ))}
117 </ul>
118 )}
119 </div>
120 );
121}

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