flagon-io/g1t

public

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/apps/web/app/components/access.tsx

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