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.

g1t.sh
97 linesCodeBlameRaw
1/**
2 * Guardrails: the form a workspace sets its defaults with and a project its
3 * overrides: how long its runs may take.
4 */
5import type { ReactNode } from "react";
6import { Form } from "react-router";
7
8import { type GuardrailSettings, type Guardrails, type GuardrailsView, RUN_KIND_LABEL } from "@g1t/contracts";
9
10import { SANDBOX_RUN_KINDS } from "../lib/guardrails";
11import { ErrorText, Input, SubmitButton, TimeAgo } from "./ui";
12import { Card } from "./ui/card";
13
14function Section({ title, about, children }: { title: string; about: ReactNode; children: ReactNode }) {
15 return (
16 <section className="grid gap-x-10 gap-y-4 border-t border-line pt-8 first:border-t-0 first:pt-0 lg:grid-cols-[16rem_1fr]">
17 <div>
18 <h2 className="font-medium">{title}</h2>
19 <div className="mt-1 text-sm text-muted">{about}</div>
20 </div>
21 <div className="min-w-0 space-y-3">{children}</div>
22 </section>
23 );
24}
25
26/**
27 * One level's guardrails. `level` says which: the workspace's defaults,
28 * over g1t's, or a project's overrides, over its workspace's.
29 */
30export function GuardrailsForm({
31 view,
32 level,
33 editable,
34 saving,
35 saved,
36 error,
37}: {
38 view: GuardrailsView;
39 level: "workspace" | "project";
40 editable: boolean;
41 /** Working for a reason the Save button cannot see; it follows its own submission by itself. */
42 saving?: boolean;
43 saved: boolean;
44 error: string | null | undefined;
45}) {
46 const own: GuardrailSettings = (level === "project" ? view.project : view.workspace) ?? {};
47 // What this level inherits, and what it is called.
48 const base: Guardrails = level === "project" ? view.inherited : view.defaults;
49 const parent = level === "project" ? "the workspace" : "g1t's default";
50 return (
51 // Keyed to the last change, so after a save the fields show what was kept, as it was tidied.
52 <Form key={own.updatedAt ?? "unset"} method="post" className="max-w-4xl space-y-8">
53 <fieldset disabled={!editable} className="min-w-0 space-y-8">
54 <Section title="Time limits" about="How long one run may take. A run that reaches it is stopped.">
55 <Card className="p-4">
56 <p className="text-sm font-medium">Time per run, in minutes</p>
57 <p className="mt-1 text-sm text-muted">Empty: as {parent}, shown faded.</p>
58 <div className="mt-4 grid grid-cols-2 gap-3 sm:grid-cols-3">
59 {SANDBOX_RUN_KINDS.map((kind) => (
60 <label key={kind} className="flex flex-col gap-1 text-xs text-muted">
61 {RUN_KIND_LABEL[kind]}
62 <Input
63 name={`minutes:${kind}`}
64 inputMode="numeric"
65 defaultValue={own.minutes?.[kind] == null ? "" : String(own.minutes[kind])}
66 placeholder={String(base.minutes[kind] ?? "")}
67 />
68 </label>
69 ))}
70 </div>
71 </Card>
72 </Section>
73 </fieldset>
74
75 {editable ? (
76 <div className="sticky bottom-(--tabbar-h) in-data-[keyboard=open]:bottom-0 -mx-4 flex flex-wrap items-center gap-4 border-t border-line bg-bg/90 px-4 py-4 backdrop-blur">
77 <SubmitButton pending="Saving…" busy={saving}>
78 Save guardrails
79 </SubmitButton>
80 {saved && <span className="text-sm text-muted">Saved. Runs that start from now on get these.</span>}
81 <ErrorText>{error}</ErrorText>
82 {own.updatedBy && own.updatedAt && !saved && !error && (
83 <span className="text-xs text-faint">
84 Last changed by <span className="font-mono">{own.updatedBy}</span> <TimeAgo at={own.updatedAt} />
85 </span>
86 )}
87 </div>
88 ) : (
89 <p className="text-sm text-muted">
90 {level === "workspace"
91 ? "Only the workspace's owners can change its defaults."
92 : "Only members of the workspace can change a project's guardrails."}
93 </p>
94 )}
95 </Form>
96 );
97}