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 bar | 1 | import { Plus } from "lucide-react"; |
| 2 | import { useState } from "react"; | |
| 3 | import { data } from "react-router"; | |
| 4 | ||
| 5 | import type { CustomPattern } from "@g1t/contracts"; | |
| 6 | ||
| 7 | import type { Route } from "./+types/security-patterns"; | |
| 8 | import { page } from "../../lib/meta"; | |
| 9 | import { CARD, PatternEditor, patternFields } from "../../components/security-suite"; | |
| 10 | import { WorkspaceSecurityHeading, WorkspaceSecurityTabs } from "../../components/workspace-security-tabs"; | |
| 11 | import { Badge } from "../../components/ui/badge"; | |
| 12 | import { securitySuite } from "../../lib/services.server"; | |
| 13 | import { assertSameOrigin, getViewer, requireUser, roleIn, unwrap } from "../../lib/session.server"; | |
| 14 | ||
| 15 | export function meta({ params, ...args }: Route.MetaArgs) { | |
| 16 | return page(args, { title: `Custom patterns · ${params.owner} · g1t` }); | |
| 17 | } | |
| 18 | ||
| 19 | export 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 }); | |
| 23 | return { list: unwrap(await securitySuite.patterns(params.owner, null, viewer)), owner: role === "owner" }; | |
| 24 | } | |
| 25 | ||
| 26 | export 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 | ||
| 46 | function 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 | ||
| 58 | export 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 | } |