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.

100 linesCodeBlameRaw
1import { Check, Minus } from "lucide-react";
2import type { ComponentProps } from "react";
3
4import { CAPABILITIES, OWNER_ONLY, REPO_ROLES, REPO_ROLE_LABELS, REPO_ROLE_SUMMARIES, type RepoRole, allows } from "@g1t/contracts";
5
6import { Card } from "./ui/card";
7import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
8
9/** A repository role picker, each role with what it is for. */
10export function RoleSelect({
11 roles = REPO_ROLES,
12 label,
13 size = "default",
14 className,
15 ...props
16}: Omit<ComponentProps<typeof Select>, "children"> & {
17 roles?: readonly RepoRole[];
18 label: string;
19 size?: "sm" | "default";
20 className?: string;
21}) {
22 return (
23 <Select {...props}>
24 <SelectTrigger size={size} aria-label={label} className={className}>
25 <SelectValue />
26 </SelectTrigger>
27 <SelectContent align="end" className="w-72">
28 {roles.map((role) => (
29 <SelectItem key={role} value={role} description={REPO_ROLE_SUMMARIES[role]}>
30 {REPO_ROLE_LABELS[role]}
31 </SelectItem>
32 ))}
33 </SelectContent>
34 </Select>
35 );
36}
37
38/**
39 * What each role can do, from the permission table itself: a row for each
40 * capability, a column for each role.
41 */
42export function RolesTable() {
43 return (
44 <>
45 {/* On a phone: each capability with the least role that has it. */}
46 <Card asChild tone="plain" divided className="sm:hidden">
47 <ul>
48 {CAPABILITIES.map((row) => {
49 const owners = (OWNER_ONLY as readonly string[]).includes(row.capability);
50 return (
51 <li key={row.capability} className="flex items-start justify-between gap-3 px-4 py-2.5 text-sm">
52 <span className="min-w-0 text-fg/90">{row.about}</span>
53 <span className="shrink-0 text-xs whitespace-nowrap text-muted">
54 {owners ? "Owners" : row.role === "admin" ? "Admin" : `${REPO_ROLE_LABELS[row.role]} and up`}
55 </span>
56 </li>
57 );
58 })}
59 </ul>
60 </Card>
61 <Card tone="plain" className="hidden overflow-x-auto sm:block">
62 <table className="w-full min-w-136 text-sm">
63 <thead>
64 <tr className="border-b border-line bg-surface text-left text-xs text-muted">
65 <th scope="col" className="px-4 py-2.5 font-medium">
66 Can
67 </th>
68 {REPO_ROLES.map((role) => (
69 <th key={role} scope="col" className="w-18 px-1 py-2.5 text-center font-medium">
70 {REPO_ROLE_LABELS[role]}
71 </th>
72 ))}
73 </tr>
74 </thead>
75 <tbody className="divide-y divide-line">
76 {CAPABILITIES.map((row) => {
77 const owners = (OWNER_ONLY as readonly string[]).includes(row.capability);
78 return (
79 <tr key={row.capability}>
80 <th scope="row" className="px-4 py-2 text-left font-normal text-fg/90">
81 {row.about}
82 </th>
83 {REPO_ROLES.map((role) => (
84 <td key={role} className="px-1 py-2 text-center">
85 {allows(role, row.capability) ? (
86 <Check size={15} aria-label="Yes" className={`inline ${owners ? "text-muted" : "text-success"}`} />
87 ) : (
88 <Minus size={13} aria-label="No" className="inline text-line-strong" />
89 )}
90 </td>
91 ))}
92 </tr>
93 );
94 })}
95 </tbody>
96 </table>
97 </Card>
98 </>
99 );
100}