g1t/apps/web/app/components/secrets.tsx
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.
| GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs | 1 | /** |
| Projects: what a workspace builds and runs, first on every page | 2 | * A project's or a workspace's secrets and variables, as one list in the |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 3 | * way Vercel lists environment variables: each row is a key, its type |
| 4 | * (Secret or Config), the environments it applies to and who reads it. | |
| 5 | * Adding and editing happen in a side panel, opened by `?add` or | |
| 6 | * `?edit=<id>` so the page works without scripts. | |
| GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs | 7 | */ |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 8 | import { Lock, Pencil, Plus, Search, SlidersHorizontal, Trash2, X } from "lucide-react"; |
| 9 | import { useMemo, useState } from "react"; | |
| 10 | import { Form, Link, useLocation, useNavigation } from "react-router"; | |
| GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs | 11 | |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 12 | import type { Setting } from "@g1t/contracts"; |
| GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs | 13 | |
| 14 | import type { SecretsAction, SecretsData } from "../lib/secrets.server"; | |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 15 | import { Button, ButtonLink, EmptyState, ErrorText, TimeAgo } from "./ui"; |
| Workspace names and icons, and a component kit for every control | 16 | import { CheckboxOption } from "./ui/checkbox"; |
| 17 | import { RadioCard, RadioGroup, RadioOption } from "./ui/radio-group"; | |
| 18 | import { Select, SelectContent, SelectItem, SelectSeparator, SelectTrigger, SelectValue } from "./ui/select"; | |
| GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs | 19 | |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 20 | /** The environments every deployment knows; workflow jobs may name others. */ |
| 21 | const KNOWN_ENVIRONMENTS = ["production", "preview"]; | |
| 22 | ||
| 23 | const READERS: Record<string, string> = { workflows: "Workflows", deployments: "Deployments" }; | |
| 24 | ||
| 25 | function environmentsLabel(environments: string[]): string { | |
| 26 | if (environments.length === 0) return "All environments"; | |
| 27 | return environments.map((env) => env.charAt(0).toUpperCase() + env.slice(1)).join(", "); | |
| GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs | 28 | } |
| 29 | ||
| Secrets and variables: one list, rows per environment, for workflows and deployments | 30 | const SELECT = |
| 31 | "rounded-md border border-line bg-bg px-3 py-2 text-sm outline-none transition-colors hover:border-line-strong focus:border-accent-dim"; | |
| 32 | ||
| 33 | export function SecretsPanel({ | |
| 34 | data, | |
| 35 | action, | |
| GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs | 36 | scope, |
| 37 | manage, | |
| 38 | }: { | |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 39 | data: SecretsData; |
| 40 | action: SecretsAction | undefined; | |
| Projects: what a workspace builds and runs, first on every page | 41 | scope: "project" | "workspace"; |
| GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs | 42 | manage: boolean; |
| 43 | }) { | |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 44 | const location = useLocation(); |
| 45 | const params = new URLSearchParams(location.search); | |
| 46 | const editing = params.get("edit"); | |
| 47 | const adding = params.has("add"); | |
| 48 | const row = editing ? data.rows.find((r) => r.id === editing && r.scope === scope) : undefined; | |
| 49 | const [query, setQuery] = useState(""); | |
| 50 | const [type, setType] = useState("all"); | |
| 51 | const [environment, setEnvironment] = useState("all"); | |
| 52 | const environments = useMemo( | |
| 53 | () => [...new Set([...KNOWN_ENVIRONMENTS, ...data.rows.flatMap((r) => r.environments)])], | |
| 54 | [data.rows], | |
| 55 | ); | |
| 56 | const shown = data.rows.filter( | |
| 57 | (r) => | |
| 58 | (!query || r.name.toLowerCase().includes(query.toLowerCase()) || r.note?.toLowerCase().includes(query.toLowerCase())) && | |
| 59 | (type === "all" || r.kind === type) && | |
| 60 | (environment === "all" || r.environments.length === 0 || r.environments.includes(environment)), | |
| 61 | ); | |
| 62 | ||
| GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs | 63 | return ( |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 64 | <div className="max-w-5xl"> |
| 65 | <header className="flex flex-wrap items-start justify-between gap-4"> | |
| 66 | <div> | |
| 67 | <h2 className="text-lg font-semibold tracking-tight">Secrets and variables</h2> | |
| 68 | <p className="mt-1 max-w-2xl text-sm text-muted"> | |
| 69 | One list for everything that reads them. Each row says which environments it applies to and whether{" "} | |
| 70 | <strong className="font-medium text-fg">workflows</strong> (as <code className="text-fg">secrets.KEY</code>{" "} | |
| 71 | and <code className="text-fg">vars.KEY</code>), <strong className="font-medium text-fg">deployments</strong>{" "} | |
| 72 | (the build's environment and the running app's <code className="text-fg">env.KEY</code>), or both read it. | |
| 73 | {scope === "workspace" | |
| Projects: what a workspace builds and runs, first on every page | 74 | ? " Every project, or the ones you link, reads the workspace's; a project's own row of the same key wins." |
| 75 | : " Rows from the workspace are shown too; adding the same key here replaces them for this project."}{" "} | |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 76 | <a href="https://docs.g1t.sh/guides/secrets-and-variables/" className="text-fg hover:underline"> |
| 77 | How they are read | |
| 78 | </a> | |
| 79 | </p> | |
| 80 | </div> | |
| 81 | {manage && ( | |
| 82 | <ButtonLink to="?add" variant="accent"> | |
| 83 | <Plus size={14} /> | |
| 84 | Add | |
| 85 | </ButtonLink> | |
| GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs | 86 | )} |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 87 | </header> |
| 88 | ||
| 89 | <p className="mt-4 rounded-lg border border-line bg-surface px-4 py-2.5 text-xs text-muted"> | |
| 90 | Built in: workflows get <code className="text-fg">secrets.G1T_TOKEN</code>, the workspace's own token for | |
| 91 | the run, with <code className="text-fg">secrets.GITHUB_TOKEN</code> as its alias. Agents, acceptance checks | |
| 92 | and the merge queue never read secrets or variables, and runs for people outside the workspace get no secrets. | |
| GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs | 93 | </p> |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 94 | |
| 95 | <div className="mt-5 flex flex-wrap gap-2"> | |
| 96 | <label className="relative min-w-56 grow"> | |
| 97 | <Search size={14} className="pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-faint" /> | |
| 98 | <input | |
| 99 | value={query} | |
| 100 | onChange={(e) => setQuery(e.target.value)} | |
| 101 | placeholder="Search keys and notes" | |
| 102 | aria-label="Search" | |
| 103 | className={`${SELECT} w-full pl-9`} | |
| 104 | /> | |
| 105 | </label> | |
| Workspace names and icons, and a component kit for every control | 106 | <Select value={type} onValueChange={setType}> |
| 107 | <SelectTrigger aria-label="Type" className="h-auto w-auto py-2"> | |
| 108 | <SelectValue /> | |
| 109 | </SelectTrigger> | |
| 110 | <SelectContent align="end"> | |
| 111 | <SelectItem value="all">All types</SelectItem> | |
| 112 | <SelectSeparator /> | |
| 113 | <SelectItem value="secret" icon={<Lock />}>Secret</SelectItem> | |
| 114 | <SelectItem value="variable" icon={<SlidersHorizontal />}>Config</SelectItem> | |
| 115 | </SelectContent> | |
| 116 | </Select> | |
| 117 | <Select value={environment} onValueChange={setEnvironment}> | |
| 118 | <SelectTrigger aria-label="Environment" className="h-auto w-auto py-2"> | |
| 119 | <SelectValue /> | |
| 120 | </SelectTrigger> | |
| 121 | <SelectContent align="end"> | |
| 122 | <SelectItem value="all">All environments</SelectItem> | |
| 123 | {environments.length > 0 && <SelectSeparator />} | |
| 124 | {environments.map((env) => ( | |
| 125 | <SelectItem key={env} value={env}> | |
| 126 | {environmentsLabel([env])} | |
| 127 | </SelectItem> | |
| 128 | ))} | |
| 129 | </SelectContent> | |
| 130 | </Select> | |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 131 | </div> |
| 132 | ||
| 133 | <ErrorText>{data.error}</ErrorText> | |
| 134 | {!editing && !adding && <div className="mt-2"><ErrorText>{action?.error}</ErrorText></div>} | |
| 135 | ||
| GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs | 136 | <div className="mt-4"> |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 137 | {data.rows.length === 0 ? ( |
| 138 | <EmptyState title="No secrets or variables yet"> | |
| 139 | Add one, or paste a <code>.env</code> file into Add to bring many at once. | |
| 140 | </EmptyState> | |
| 141 | ) : shown.length === 0 ? ( | |
| 142 | <EmptyState title="Nothing matches" /> | |
| GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs | 143 | ) : ( |
| 144 | <ul className="overflow-hidden rounded-xl border border-line bg-surface"> | |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 145 | {shown.map((r) => ( |
| 146 | <Row key={r.id} row={r} inherited={r.scope !== scope} manage={manage} /> | |
| GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs | 147 | ))} |
| 148 | </ul> | |
| 149 | )} | |
| 150 | </div> | |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 151 | |
| 152 | {manage && (adding || row) && ( | |
| Projects: what a workspace builds and runs, first on every page | 153 | <Drawer row={row} scope={scope} projects={data.projects} error={action?.error} /> |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 154 | )} |
| 155 | </div> | |
| 156 | ); | |
| 157 | } | |
| 158 | ||
| 159 | function Row({ row, inherited, manage }: { row: Setting; inherited: boolean; manage: boolean }) { | |
| 160 | const busy = useNavigation().state === "submitting"; | |
| 161 | const secret = row.kind === "secret"; | |
| 162 | return ( | |
| 163 | <li className="grid grid-cols-[minmax(0,1.4fr)_minmax(0,1fr)_auto] items-center gap-x-4 gap-y-1 border-t border-line px-4 py-3 text-sm first:border-t-0 md:grid-cols-[minmax(0,1.4fr)_minmax(0,1fr)_minmax(0,1fr)_6rem_6rem_auto]"> | |
| 164 | <div className="min-w-0"> | |
| 165 | <p className="truncate font-mono text-[0.8125rem]">{row.name}</p> | |
| 166 | {row.note && <p className="truncate text-xs text-faint">{row.note}</p>} | |
| 167 | {!secret && row.value != null && <p className="truncate font-mono text-xs text-muted">{row.value}</p>} | |
| 168 | </div> | |
| 169 | <span className="truncate text-muted">{environmentsLabel(row.environments)}</span> | |
| 170 | <span className="hidden truncate text-xs text-muted md:block"> | |
| 171 | {row.availableTo.map((r) => READERS[r] ?? r).join(" · ")} | |
| 172 | </span> | |
| 173 | <span className="hidden items-center gap-1.5 text-xs text-muted md:flex"> | |
| 174 | {secret ? <Lock size={13} /> : <SlidersHorizontal size={13} />} | |
| 175 | {secret ? "Secret" : "Config"} | |
| 176 | </span> | |
| 177 | <span className="hidden text-xs text-faint md:block"> | |
| 178 | <TimeAgo at={row.updatedAt} /> | |
| 179 | </span> | |
| 180 | <span className="flex items-center justify-end gap-1"> | |
| 181 | {inherited ? ( | |
| 182 | <span className="rounded-full px-2 py-px text-xs text-muted ring-1 ring-line">Workspace</span> | |
| 183 | ) : ( | |
| 184 | <> | |
| Projects: what a workspace builds and runs, first on every page | 185 | {row.projects.length > 0 && ( |
| 186 | <span className="mr-1 text-xs text-faint" title={row.projects.join(", ")}> | |
| 187 | {row.projects.length} {row.projects.length === 1 ? "project" : "projects"} | |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 188 | </span> |
| 189 | )} | |
| 190 | {manage && ( | |
| 191 | <> | |
| 192 | <Link | |
| 193 | to={`?edit=${row.id}`} | |
| 194 | aria-label={`Edit ${row.name}`} | |
| 195 | className="rounded-md p-1.5 text-faint transition-colors hover:bg-raised hover:text-fg" | |
| 196 | > | |
| 197 | <Pencil size={14} /> | |
| 198 | </Link> | |
| 199 | <Form method="post"> | |
| 200 | <input type="hidden" name="intent" value="delete" /> | |
| 201 | <input type="hidden" name="id" value={row.id} /> | |
| 202 | <input type="hidden" name="name" value={row.name} /> | |
| 203 | <button | |
| 204 | type="submit" | |
| 205 | disabled={busy} | |
| 206 | aria-label={`Remove ${row.name}`} | |
| 207 | className="rounded-md p-1.5 text-faint transition-colors hover:bg-raised hover:text-danger" | |
| 208 | > | |
| 209 | <Trash2 size={14} /> | |
| 210 | </button> | |
| 211 | </Form> | |
| 212 | </> | |
| GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs | 213 | )} |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 214 | </> |
| 215 | )} | |
| 216 | </span> | |
| 217 | </li> | |
| GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs | 218 | ); |
| 219 | } | |
| 220 | ||
| Secrets and variables: one list, rows per environment, for workflows and deployments | 221 | function Drawer({ |
| 222 | row, | |
| GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs | 223 | scope, |
| Projects: what a workspace builds and runs, first on every page | 224 | projects, |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 225 | error, |
| GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs | 226 | }: { |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 227 | row: Setting | undefined; |
| Projects: what a workspace builds and runs, first on every page | 228 | scope: "project" | "workspace"; |
| 229 | projects: string[]; | |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 230 | error: string | undefined; |
| GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs | 231 | }) { |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 232 | const busy = useNavigation().state === "submitting"; |
| 233 | const editing = !!row; | |
| 234 | const [type, setType] = useState<"secret" | "config">(row?.kind === "variable" ? "config" : "secret"); | |
| 235 | const [some, setSome] = useState(!!row && row.environments.length > 0); | |
| Projects: what a workspace builds and runs, first on every page | 236 | const [reach, setReach] = useState(row && row.projects.length > 0 ? "some" : "all"); |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 237 | const custom = row?.environments.filter((env) => !KNOWN_ENVIRONMENTS.includes(env)) ?? []; |
| 238 | const field = | |
| 239 | "w-full rounded-md border border-line bg-bg px-3 py-2 text-sm outline-none transition-colors placeholder:text-faint hover:border-line-strong focus:border-accent-dim"; | |
| GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs | 240 | return ( |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 241 | <div className="fixed inset-0 z-50 flex justify-end bg-black/50" role="dialog" aria-modal="true" aria-label={editing ? "Edit" : "Add"}> |
| 242 | <Link to="?" aria-label="Close" className="grow" /> | |
| 243 | <Form method="post" className="flex h-full w-full max-w-xl flex-col border-l border-line bg-bg shadow-2xl"> | |
| 244 | <div className="flex items-center justify-between border-b border-line px-6 py-4"> | |
| 245 | <h3 className="font-semibold">{editing ? `Edit ${row.name}` : "Add a secret or variable"}</h3> | |
| 246 | <Link to="?" aria-label="Close" className="rounded-md p-1.5 text-faint hover:bg-raised hover:text-fg"> | |
| 247 | <X size={16} /> | |
| 248 | </Link> | |
| 249 | </div> | |
| 250 | <div className="grow space-y-6 overflow-y-auto px-6 py-5"> | |
| 251 | <input type="hidden" name="intent" value="save" /> | |
| 252 | {row && <input type="hidden" name="id" value={row.id} />} | |
| 253 | ||
| 254 | <fieldset> | |
| 255 | <legend className="mb-2 text-sm font-medium text-muted">Type</legend> | |
| Workspace names and icons, and a component kit for every control | 256 | <RadioGroup |
| 257 | name="type" | |
| 258 | value={type} | |
| 259 | onValueChange={(value) => setType(value as typeof type)} | |
| 260 | aria-label="Type" | |
| 261 | className="gap-3 sm:grid-cols-2" | |
| 262 | > | |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 263 | {( |
| 264 | [ | |
| Workspace names and icons, and a component kit for every control | 265 | ["secret", "Secret", "You can't read it again after saving. For passwords, API keys and tokens.", <Lock key="i" />], |
| 266 | ["config", "Config", "Readable by members after saving. For values that are not sensitive.", <SlidersHorizontal key="i" />], | |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 267 | ] as const |
| Workspace names and icons, and a component kit for every control | 268 | ).map(([value, title, text, icon]) => ( |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 269 | // A secret's value is sealed: it can never become config. |
| Workspace names and icons, and a component kit for every control | 270 | <RadioCard |
| 271 | key={value} | |
| 272 | value={value} | |
| 273 | title={title} | |
| 274 | description={text} | |
| 275 | icon={icon} | |
| 276 | disabled={value === "config" && row?.kind === "secret"} | |
| 277 | /> | |
| 278 | ))} | |
| 279 | </RadioGroup> | |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 280 | {row?.kind === "variable" && ( |
| 281 | <p className="mt-2 text-xs text-faint">Config can become a secret; a secret cannot become config.</p> | |
| 282 | )} | |
| 283 | </fieldset> | |
| 284 | ||
| 285 | <label className="block"> | |
| 286 | <span className="mb-1.5 block text-sm font-medium text-muted">Key</span> | |
| 287 | {editing ? ( | |
| 288 | <input name="key" value={row.name} readOnly className={`${field} font-mono text-muted`} /> | |
| 289 | ) : ( | |
| 290 | <textarea | |
| 291 | name="key" | |
| 292 | required | |
| 293 | rows={1} | |
| 294 | placeholder="CLIENT_KEY, or paste a .env file" | |
| 295 | autoComplete="off" | |
| 296 | spellCheck={false} | |
| 297 | className={`${field} min-h-10 font-mono`} | |
| 298 | /> | |
| 299 | )} | |
| 300 | </label> | |
| 301 | ||
| 302 | <label className="block"> | |
| 303 | <span className="mb-1.5 block text-sm font-medium text-muted">Value</span> | |
| 304 | <textarea | |
| 305 | name="value" | |
| 306 | rows={type === "secret" ? 3 : 2} | |
| 307 | defaultValue={row?.kind === "variable" ? (row.value ?? "") : ""} | |
| 308 | placeholder={ | |
| 309 | editing && row.kind === "secret" ? "Leave empty to keep the current value" : "Enter a value" | |
| 310 | } | |
| 311 | autoComplete="off" | |
| 312 | spellCheck={false} | |
| 313 | className={`${field} font-mono`} | |
| 314 | /> | |
| 315 | </label> | |
| 316 | ||
| 317 | <label className="block"> | |
| 318 | <span className="mb-1.5 block text-sm font-medium text-muted">Note (optional)</span> | |
| 319 | <input name="note" defaultValue={row?.note ?? ""} placeholder="Where to rotate it, or who to ask" className={field} /> | |
| 320 | </label> | |
| 321 | ||
| 322 | <fieldset> | |
| 323 | <legend className="mb-2 text-sm font-medium text-muted">Environments</legend> | |
| 324 | <div className="space-y-2 text-sm"> | |
| Workspace names and icons, and a component kit for every control | 325 | <RadioGroup name="scope" value={some ? "some" : "all"} onValueChange={(value) => setSome(value === "some")} aria-label="Environments"> |
| 326 | <RadioOption value="all" label="All environments" /> | |
| 327 | <RadioOption value="some" label="Only some" /> | |
| 328 | </RadioGroup> | |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 329 | {some && ( |
| 330 | <div className="ml-6 space-y-2"> | |
| 331 | {KNOWN_ENVIRONMENTS.map((env) => ( | |
| Workspace names and icons, and a component kit for every control | 332 | <CheckboxOption |
| 333 | key={env} | |
| 334 | name="env" | |
| 335 | value={env} | |
| 336 | defaultChecked={row?.environments.includes(env)} | |
| 337 | label={environmentsLabel([env])} | |
| 338 | /> | |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 339 | ))} |
| 340 | <input | |
| 341 | name="envCustom" | |
| 342 | defaultValue={custom.join(", ")} | |
| 343 | placeholder="Others, comma-separated: staging, qa" | |
| 344 | className={field} | |
| 345 | /> | |
| 346 | <p className="text-xs text-faint"> | |
| 347 | Deployments are production and preview; a workflow job reads the row for its{" "} | |
| 348 | <code>environment:</code>, and rows for all environments otherwise. | |
| 349 | </p> | |
| 350 | </div> | |
| 351 | )} | |
| 352 | </div> | |
| 353 | </fieldset> | |
| 354 | ||
| 355 | <fieldset> | |
| 356 | <legend className="mb-2 text-sm font-medium text-muted">Available to</legend> | |
| 357 | <div className="space-y-2 text-sm"> | |
| 358 | {( | |
| 359 | [ | |
| 360 | ["workflows", "Workflows", "secrets.KEY or vars.KEY in GitHub Actions workflows"], | |
| 361 | ["deployments", "Deployments", "The build's environment, and env.KEY in the running app"], | |
| 362 | ] as const | |
| 363 | ).map(([value, title, text]) => ( | |
| Workspace names and icons, and a component kit for every control | 364 | <CheckboxOption |
| 365 | key={value} | |
| 366 | name="availableTo" | |
| 367 | value={value} | |
| 368 | defaultChecked={row ? row.availableTo.includes(value) : true} | |
| 369 | label={title} | |
| 370 | description={text} | |
| 371 | /> | |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 372 | ))} |
| 373 | </div> | |
| 374 | </fieldset> | |
| 375 | ||
| 376 | {scope === "workspace" && ( | |
| 377 | <fieldset> | |
| Projects: what a workspace builds and runs, first on every page | 378 | <legend className="mb-2 text-sm font-medium text-muted">Projects</legend> |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 379 | <div className="space-y-2 text-sm"> |
| Workspace names and icons, and a component kit for every control | 380 | <RadioGroup name="reach" value={reach} onValueChange={(value) => setReach(value as typeof reach)} aria-label="Projects"> |
| 381 | <RadioOption value="all" label="Every project" /> | |
| 382 | <RadioOption value="some" label="Only these" /> | |
| 383 | </RadioGroup> | |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 384 | {reach === "some" && ( |
| Workspace names and icons, and a component kit for every control | 385 | <div className="ml-6 grid max-h-48 gap-1.5 overflow-y-auto p-0.5 sm:grid-cols-2"> |
| Projects: what a workspace builds and runs, first on every page | 386 | {projects.map((name) => ( |
| Workspace names and icons, and a component kit for every control | 387 | <CheckboxOption |
| 388 | key={name} | |
| 389 | name="project" | |
| 390 | value={name} | |
| 391 | defaultChecked={row?.projects.includes(name)} | |
| 392 | label={name} | |
| 393 | className="items-center" | |
| 394 | labelClassName="font-mono text-xs" | |
| 395 | /> | |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 396 | ))} |
| 397 | </div> | |
| 398 | )} | |
| 399 | </div> | |
| 400 | </fieldset> | |
| 401 | )} | |
| 402 | <ErrorText>{error}</ErrorText> | |
| 403 | </div> | |
| 404 | <div className="flex items-center justify-between gap-4 border-t border-line px-6 py-4"> | |
| 405 | <p className="text-xs text-faint">{editing ? "" : "Paste .env contents into Key to add many."}</p> | |
| 406 | <Button type="submit" disabled={busy}> | |
| 407 | Save | |
| 408 | </Button> | |
| 409 | </div> | |
| 410 | </Form> | |
| GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs | 411 | </div> |
| 412 | ); | |
| 413 | } |