Skip to content
149 linesCodeBlameRaw
1import { ChevronRight, KeyRound } from "lucide-react";
2import type { ReactNode } from "react";
3import { Link } from "react-router";
4
5import type { AccessToken } from "@g1t/contracts";
6
7import { cn } from "../lib/cn";
8import { permissionChips, reachSummary, statusBadge } from "../lib/access-tokens";
9import { describeExpiry } from "../lib/token-scopes";
10import { ButtonLink, CopyLine, TimeAgo } from "./ui";
11import { Badge } from "./ui/badge";
12import { Card } from "./ui/card";
13
14// One access token in a line, and the list of them: the same on your
15// settings, a workspace's tokens and a workspace's view of its members'
16// tokens.
17
18/** What a token reaches and may do, under its name. */
19export function TokenFacts({ token, className }: { token: AccessToken; className?: string }) {
20 const chips = permissionChips(token);
21 return (
22 <div className={cn("space-y-1.5", className)}>
23 <p className="text-xs text-muted">{reachSummary(token)}</p>
24 <div className="flex flex-wrap gap-1.5">
25 {chips.length === 0 && <span className="text-xs text-faint">No permissions</span>}
26 {chips.map((chip) => (
27 <span
28 key={chip.label}
29 className={cn(
30 "rounded border px-1.5 py-px text-[0.6875rem]",
31 chip.dangerous ? "border-danger/40 text-danger" : "border-line text-muted",
32 )}
33 >
34 {chip.label}
35 </span>
36 ))}
37 </div>
38 {token.repositorySelection === "selected" && (token.repositories?.length ?? 0) > 0 && (
39 <p className="truncate font-mono text-[0.6875rem] text-faint">{token.repositories!.join(", ")}</p>
40 )}
41 {token.reviewReason && <p className="text-xs text-faint">Owner's note: {token.reviewReason}</p>}
42 </div>
43 );
44}
45
46/** When it was made, by whom, last used, and when it expires. */
47export function TokenMeta({ token }: { token: AccessToken }) {
48 const expiry = describeExpiry(token.expiresAt);
49 return (
50 <p className="text-xs text-faint">
51 Created <TimeAgo at={token.createdAt} />
52 {token.workspaceOwned &&
53 (token.createdBy ? (
54 <>
55 {" "}
56 by <span className="font-mono">{token.createdBy}</span>
57 </>
58 ) : (
59 " by someone who has since left g1t"
60 ))}{" "}
61 ·{" "}
62 {token.lastUsedAt ? (
63 <>
64 last used <TimeAgo at={token.lastUsedAt} />
65 </>
66 ) : (
67 "never used"
68 )}{" "}
69 · <span className={expiry === "Expired" ? "text-danger" : expiry === "No expiry" ? "text-warn" : undefined}>{expiry}</span>
70 </p>
71 );
72}
73
74/** Its badges: waiting, denied or revoked; Admin on a workspace's token. */
75export function TokenBadges({ token }: { token: AccessToken }) {
76 const badge = statusBadge(token.status);
77 return (
78 <>
79 {badge && <Badge tone={badge.tone}>{badge.label}</Badge>}
80 {token.workspaceOwned && <Badge tone={token.admin ? "danger" : "neutral"}>{token.admin ? "Admin" : "Write"}</Badge>}
81 {token.website && <Badge tone="warn">Uses the website</Badge>}
82 </>
83 );
84}
85
86/** A list of tokens, each a link to its page. */
87export function TokenList({
88 tokens,
89 href,
90 lead,
91 aside,
92}: {
93 tokens: AccessToken[];
94 href: (token: AccessToken) => string;
95 /** Before a token's name, such as whose it is. */
96 lead?: (token: AccessToken) => ReactNode;
97 /** After it, such as an action. */
98 aside?: (token: AccessToken) => ReactNode;
99}) {
100 return (
101 <Card asChild tone="plain" divided>
102 <ul>
103 {tokens.map((token) => (
104 <li key={token.id} className="flex flex-col gap-3 px-4 py-3 sm:flex-row sm:items-start">
105 <div className="min-w-0 grow space-y-1">
106 <p className="flex min-w-0 flex-wrap items-center gap-2 text-sm">
107 {lead?.(token)}
108 <Link to={href(token)} className="flex min-w-0 items-center gap-1.5 font-medium text-fg hover:underline">
109 <KeyRound size={13} className="shrink-0 text-faint" />
110 <span className="truncate">{token.name}</span>
111 </Link>
112 <TokenBadges token={token} />
113 </p>
114 {token.description && <p className="truncate text-xs text-muted">{token.description}</p>}
115 <TokenMeta token={token} />
116 <TokenFacts token={token} />
117 </div>
118 <div className="flex shrink-0 items-center gap-2">
119 {aside?.(token)}
120 <Link to={href(token)} aria-label={`Open ${token.name}`} className="hidden text-faint hover:text-fg sm:block">
121 <ChevronRight size={16} />
122 </Link>
123 </div>
124 </li>
125 ))}
126 </ul>
127 </Card>
128 );
129}
130
131/** A token just made: the secret, once, and what it is. */
132export function TokenCreated({ secret, token, back }: { secret: string; token: AccessToken; back: string }) {
133 return (
134 <div className="space-y-4 rounded-xl border border-accent/40 bg-surface p-4 sm:p-5">
135 <p className="text-sm">
136 <span className="font-medium">{token.name}</span> is ready. Copy it now: it will not be shown again.
137 </p>
138 <CopyLine text={secret} />
139 <div className="flex flex-wrap items-center gap-2">
140 <TokenBadges token={token} />
141 </div>
142 <TokenMeta token={token} />
143 <TokenFacts token={token} />
144 <ButtonLink variant="outline" to={back}>
145 Back to tokens
146 </ButtonLink>
147 </div>
148 );
149}