| 1 | /** |
| 2 | * Guardrails: the form a workspace sets its defaults with and a project its |
| 3 | * overrides: how long its runs may take. |
| 4 | */ |
| 5 | import type { ReactNode } from "react"; |
| 6 | import { Form } from "react-router"; |
| 7 | |
| 8 | import { type GuardrailSettings, type Guardrails, type GuardrailsView, RUN_KIND_LABEL } from "@g1t/contracts"; |
| 9 | |
| 10 | import { SANDBOX_RUN_KINDS } from "../lib/guardrails"; |
| 11 | import { ErrorText, Input, SubmitButton, TimeAgo } from "./ui"; |
| 12 | import { Card } from "./ui/card"; |
| 13 | |
| 14 | function 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 | */ |
| 30 | export 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 | } |