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.
| Merge packages: roles, Actions access, source label, soft delete, API | 1 | import { Box, Lock, RotateCcw, Trash2, User, UserPlus, Users } from "lucide-react"; |
| 2 | import { useEffect, useState } from "react"; | |
| 3 | import { Form, Link, useFetcher } from "react-router"; | |
| 4 | ||
| 5 | import { | |
| 6 | type ActionsAccess, | |
| 7 | PACKAGE_ROLES, | |
| 8 | type PackageAccess, | |
| 9 | type PackageRole, | |
| 10 | type PackageSettings, | |
| 11 | type PackageVersion, | |
| 12 | PACKAGE_RESTORE_DAYS, | |
| 13 | } from "@g1t/contracts"; | |
| 14 | ||
| 15 | import { ConfirmDialog } from "./repo-lifecycle"; | |
| 16 | import { SettingsSection as Section } from "./settings-section"; | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 17 | import { ErrorText, SubmitButton, TimeAgo } from "./ui"; |
| Merge packages: roles, Actions access, source label, soft delete, API | 18 | import { Badge } from "./ui/badge"; |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 19 | import { Button } from "./ui/button"; |
| 20 | import { Card } from "./ui/card"; | |
| Merge packages: roles, Actions access, source label, soft delete, API | 21 | import { Hint } from "./ui/hint"; |
| 22 | import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"; | |
| 23 | import { Skeleton, SkeletonLine, SkeletonRows } from "./ui/skeleton"; | |
| 24 | import { SwitchCard } from "./ui/switch"; | |
| 25 | import { formatBytes, shortDigest } from "../lib/packages"; | |
| 26 | ||
| 27 | /** A one-line text field the height of the small selects beside it. */ | |
| 28 | const FIELD = | |
| 29 | "h-8 w-full min-w-0 rounded-md border border-line bg-bg px-2.5 font-mono text-sm placeholder:font-sans placeholder:text-faint focus:border-line-strong focus:outline-none"; | |
| 30 | ||
| 31 | /** What an action on the Settings tab answers. */ | |
| 32 | export type SettingsOutcome = { error: string | null; message: string | null }; | |
| 33 | ||
| 34 | export const PACKAGE_ROLE_LABEL: Record<PackageRole, string> = { read: "Read", write: "Write", admin: "Admin" }; | |
| 35 | const PACKAGE_ROLE_SUMMARY: Record<PackageRole, string> = { | |
| 36 | read: "Pull and install it.", | |
| 37 | write: "Also publish new versions and tags.", | |
| 38 | admin: "Also delete and restore it, and change these settings.", | |
| 39 | }; | |
| 40 | const ACTIONS_ROLE_SUMMARY: Record<"read" | "write", string> = { | |
| 41 | read: "Its workflows pull or install the package.", | |
| 42 | write: "Its workflows publish it too.", | |
| 43 | }; | |
| 44 | ||
| 45 | /** A role picker: read, write and admin, or read and write for a repository. */ | |
| 46 | function RoleSelect({ | |
| 47 | value, | |
| 48 | roles, | |
| 49 | label, | |
| 50 | name, | |
| 51 | disabled, | |
| 52 | onValueChange, | |
| 53 | className = "w-28", | |
| 54 | }: { | |
| 55 | value: string; | |
| 56 | roles: readonly ("read" | "write" | "admin")[]; | |
| 57 | label: string; | |
| 58 | name?: string; | |
| 59 | disabled?: boolean; | |
| 60 | onValueChange?: (role: string) => void; | |
| 61 | className?: string; | |
| 62 | }) { | |
| 63 | return ( | |
| 64 | <Select name={name} value={value} onValueChange={onValueChange} disabled={disabled}> | |
| 65 | <SelectTrigger size="sm" aria-label={label} className={className}> | |
| 66 | <SelectValue /> | |
| 67 | </SelectTrigger> | |
| 68 | <SelectContent align="end" className="w-64"> | |
| 69 | {roles.map((role) => ( | |
| 70 | <SelectItem | |
| 71 | key={role} | |
| 72 | value={role} | |
| 73 | description={role === "admin" ? PACKAGE_ROLE_SUMMARY.admin : roles.length === 2 ? ACTIONS_ROLE_SUMMARY[role] : PACKAGE_ROLE_SUMMARY[role]} | |
| 74 | > | |
| 75 | {PACKAGE_ROLE_LABEL[role]} | |
| 76 | </SelectItem> | |
| 77 | ))} | |
| 78 | </SelectContent> | |
| 79 | </Select> | |
| 80 | ); | |
| 81 | } | |
| 82 | ||
| 83 | /** The Settings tab of a package: who may use it, from where, and what happens to it. Admins only. */ | |
| 84 | export function PackageSettingsTab({ settings, outcome }: { settings: PackageSettings; outcome: SettingsOutcome | undefined }) { | |
| 85 | const { package: pkg, access, actions_access: actions, deleted_versions: deleted, permissions } = settings; | |
| 86 | const repo = pkg.repo ? `${pkg.repo.namespace}/${pkg.repo.name}` : null; | |
| 87 | const npm = pkg.ecosystem !== "container"; | |
| 88 | return ( | |
| 89 | <div className="max-w-4xl space-y-8"> | |
| 90 | {outcome?.error && <ErrorText>{outcome.error}</ErrorText>} | |
| 91 | {outcome?.message && ( | |
| 92 | <p className="text-sm text-success" role="status"> | |
| 93 | {outcome.message} | |
| 94 | </p> | |
| 95 | )} | |
| 96 | ||
| 97 | <Section | |
| 98 | title="Manage access" | |
| 99 | about={ | |
| 100 | repo | |
| 101 | ? `People and teams with a role on the package itself. The workspace's owners administer every package.` | |
| 102 | : `People and teams with a role on the package itself, beyond what members of ${pkg.workspace} get from its base permission. Owners administer every package.` | |
| 103 | } | |
| 104 | > | |
| 105 | {repo && <InheritToggle on={pkg.inherit_access !== false} repo={repo} />} | |
| 106 | {access.length === 0 ? ( | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 107 | <Card asChild tone="plain" className="border-dashed px-4 py-6 text-center text-sm text-muted"> |
| 108 | <p> | |
| 109 | No one has a role on the package itself. | |
| 110 | </p> | |
| 111 | </Card> | |
| Merge packages: roles, Actions access, source label, soft delete, API | 112 | ) : ( |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 113 | <Card asChild tone="plain" divided> |
| 114 | <ul> | |
| 115 | {access.map((entry) => ( | |
| 116 | <AccessRow key={`${entry.kind}:${entry.id}`} entry={entry} workspace={pkg.workspace} /> | |
| 117 | ))} | |
| 118 | </ul> | |
| 119 | </Card> | |
| Merge packages: roles, Actions access, source label, soft delete, API | 120 | )} |
| 121 | <AddAccessForm /> | |
| 122 | </Section> | |
| 123 | ||
| 124 | <Section | |
| 125 | title="Manage Actions access" | |
| 126 | about="Which repositories' workflows may use the package with their job token, G1T_TOKEN. A workflow in any other repository is refused." | |
| 127 | > | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 128 | <Card asChild tone="plain" divided> |
| 129 | <ul> | |
| 130 | {actions.length === 0 && ( | |
| 131 | <li className="px-4 py-6 text-center text-sm text-muted">No repository's workflows may use it yet.</li> | |
| 132 | )} | |
| 133 | {actions.map((entry) => ( | |
| 134 | <ActionsRow key={entry.repo_id} entry={entry} /> | |
| 135 | ))} | |
| 136 | </ul> | |
| 137 | </Card> | |
| Merge packages: roles, Actions access, source label, soft delete, API | 138 | <AddActionsForm workspace={pkg.workspace} /> |
| 139 | </Section> | |
| 140 | ||
| 141 | <Section | |
| 142 | title="Visibility" | |
| 143 | about="Who may pull it. Public packages need no sign-in to pull; pushing always needs the Write role." | |
| 144 | > | |
| 145 | {repo ? ( | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 146 | <Card asChild className="p-4 text-sm text-muted"> |
| 147 | <p> | |
| 148 | <span className="inline-flex items-center gap-1.5 font-medium text-fg"> | |
| 149 | {pkg.visibility === "private" && <Lock size={13} />} | |
| 150 | {pkg.visibility === "private" ? "Private" : "Public"} | |
| 151 | </span> | |
| 152 | , as{" "} | |
| 153 | <Link to={`/${repo}`} className="text-fg-soft hover:text-fg"> | |
| 154 | {repo} | |
| 155 | </Link>{" "} | |
| 156 | is. A linked package has its repository's visibility: change the repository's, or unlink the package. | |
| 157 | </p> | |
| 158 | </Card> | |
| Merge packages: roles, Actions access, source label, soft delete, API | 159 | ) : ( |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 160 | <Card asChild className="flex flex-wrap items-center gap-3 p-4"> |
| 161 | <Form method="post"> | |
| 162 | <input type="hidden" name="intent" value="visibility" /> | |
| 163 | <Select name="visibility" defaultValue={pkg.visibility}> | |
| 164 | <SelectTrigger size="sm" aria-label="Visibility" className="w-full sm:w-64"> | |
| 165 | <SelectValue /> | |
| 166 | </SelectTrigger> | |
| 167 | <SelectContent align="start"> | |
| 168 | <SelectItem value="private" description={`Members of ${pkg.workspace}, by its base permission, and those given a role here.`}> | |
| 169 | Private | |
| 170 | </SelectItem> | |
| 171 | <SelectItem value="public" description="Anyone can pull it, signed in or not."> | |
| 172 | Public | |
| 173 | </SelectItem> | |
| 174 | </SelectContent> | |
| 175 | </Select> | |
| 176 | <SubmitButton variant="outline" match={{ intent: "visibility" }} pending="Saving…"> | |
| 177 | Save | |
| 178 | </SubmitButton> | |
| 179 | </Form> | |
| 180 | </Card> | |
| Merge packages: roles, Actions access, source label, soft delete, API | 181 | )} |
| 182 | </Section> | |
| 183 | ||
| 184 | <Section | |
| 185 | title="Repository" | |
| 186 | about="A linked package shows on its repository, takes its visibility and, while inheriting access, its roles, and its repository's workflows may publish it." | |
| 187 | > | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 188 | <Card className="space-y-3 p-4"> |
| Merge packages: roles, Actions access, source label, soft delete, API | 189 | {repo ? ( |
| 190 | <div className="flex flex-wrap items-center justify-between gap-3"> | |
| 191 | <p className="flex min-w-0 items-center gap-2 text-sm"> | |
| 192 | <Box size={14} className="shrink-0 text-faint" /> | |
| 193 | <span className="text-muted">Linked to</span> | |
| 194 | <Link to={`/${repo}`} className="truncate font-mono text-fg-soft hover:text-fg"> | |
| 195 | {repo} | |
| 196 | </Link> | |
| 197 | </p> | |
| 198 | <Form method="post"> | |
| 199 | <input type="hidden" name="intent" value="unlink" /> | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 200 | <SubmitButton variant="outline" match={{ intent: "unlink" }} pending="Unlinking…"> |
| Merge packages: roles, Actions access, source label, soft delete, API | 201 | Unlink |
| 202 | </SubmitButton> | |
| 203 | </Form> | |
| 204 | </div> | |
| 205 | ) : ( | |
| 206 | <p className="text-sm text-muted">Not linked: it is the workspace's.</p> | |
| 207 | )} | |
| 208 | <Form method="post" className="flex flex-wrap items-center gap-2"> | |
| 209 | <input type="hidden" name="intent" value="link" /> | |
| 210 | <label className="sr-only" htmlFor="link-repo"> | |
| 211 | Repository to link | |
| 212 | </label> | |
| 213 | <input id="link-repo" name="repo" required placeholder="repository" className={`${FIELD} sm:w-56`} /> | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 214 | <SubmitButton variant="outline" match={{ intent: "link" }} pending="Linking…"> |
| Merge packages: roles, Actions access, source label, soft delete, API | 215 | {repo ? "Link to another" : "Link"} |
| 216 | </SubmitButton> | |
| 217 | </Form> | |
| 218 | <p className="text-xs text-faint">Linking needs the Admin role on the repository too.</p> | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 219 | </Card> |
| Merge packages: roles, Actions access, source label, soft delete, API | 220 | </Section> |
| 221 | ||
| 222 | {permissions.delete && ( | |
| 223 | <Section | |
| 224 | title="Deleted versions" | |
| 225 | about={`Versions deleted in the last ${PACKAGE_RESTORE_DAYS} days, which can still be restored. Their ${npm ? "versions" : "digests"} cannot be published again until they are purged.`} | |
| 226 | > | |
| 227 | {deleted.length === 0 ? ( | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 228 | <Card asChild tone="plain" className="border-dashed px-4 py-6 text-center text-sm text-muted"> |
| 229 | <p>No deleted versions.</p> | |
| 230 | </Card> | |
| Merge packages: roles, Actions access, source label, soft delete, API | 231 | ) : ( |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 232 | <Card asChild tone="plain" divided> |
| 233 | <ul> | |
| 234 | {deleted.map((version) => ( | |
| 235 | <DeletedVersionRow key={version.id} version={version} npm={npm} /> | |
| 236 | ))} | |
| 237 | </ul> | |
| 238 | </Card> | |
| Merge packages: roles, Actions access, source label, soft delete, API | 239 | )} |
| 240 | </Section> | |
| 241 | )} | |
| 242 | ||
| 243 | {permissions.delete && ( | |
| 244 | <Section title="Delete this package" about={`Every version goes from the registry at once. An admin can restore it for ${PACKAGE_RESTORE_DAYS} days.`}> | |
| 245 | <div className="flex flex-wrap items-center justify-between gap-3 rounded-xl border border-danger/30 bg-surface p-4"> | |
| 246 | <p className="text-sm text-muted"> | |
| 247 | Its name stays taken until it is purged, so nobody else can publish a package called {pkg.name} meanwhile. | |
| 248 | </p> | |
| 249 | <ConfirmDialog | |
| 250 | intent="delete-package" | |
| 251 | title={`Delete ${pkg.name}?`} | |
| 252 | description={`Anyone pulling it gets an error from then on. It can be restored from the workspace's deleted packages for ${PACKAGE_RESTORE_DAYS} days.`} | |
| 253 | confirm={pkg.name} | |
| 254 | submit="Delete package" | |
| 255 | busy="Deleting…" | |
| 256 | trigger={(open) => ( | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 257 | <Button type="button" variant="destructive" onClick={open}> |
| Merge packages: roles, Actions access, source label, soft delete, API | 258 | <Trash2 size={14} /> |
| 259 | Delete package | |
| 260 | </Button> | |
| 261 | )} | |
| 262 | > | |
| 263 | <li> | |
| 264 | {pkg.versions} {pkg.versions === 1 ? "version" : "versions"} and their tags. | |
| 265 | </li> | |
| 266 | <li>After {PACKAGE_RESTORE_DAYS} days it is purged, and files no other package uses are removed.</li> | |
| 267 | </ConfirmDialog> | |
| 268 | </div> | |
| 269 | </Section> | |
| 270 | )} | |
| 271 | </div> | |
| 272 | ); | |
| 273 | } | |
| 274 | ||
| 275 | /** Whether a linked package takes its repository's roles: saved as soon as it is flipped. */ | |
| 276 | function InheritToggle({ on, repo }: { on: boolean; repo: string }) { | |
| 277 | const fetcher = useFetcher<SettingsOutcome>(); | |
| 278 | const shown = fetcher.formData ? fetcher.formData.get("inherit") === "on" : on; | |
| 279 | return ( | |
| 280 | <> | |
| 281 | <SwitchCard | |
| 282 | title="Inherit access from the linked repository" | |
| 283 | checked={shown} | |
| 284 | disabled={fetcher.state !== "idle"} | |
| 285 | onCheckedChange={(checked) => fetcher.submit({ intent: "inherit", inherit: checked ? "on" : "off" }, { method: "post" })} | |
| 286 | > | |
| 287 | {shown | |
| 288 | ? `Everyone with a role on ${repo} has it on the package too, and the people and teams below add to it.` | |
| 289 | : `Only the people and teams below, and the workspace's owners, have access. Roles on ${repo} no longer reach the package.`} | |
| 290 | </SwitchCard> | |
| 291 | {fetcher.data?.error && <ErrorText>{fetcher.data.error}</ErrorText>} | |
| 292 | </> | |
| 293 | ); | |
| 294 | } | |
| 295 | ||
| 296 | function AccessRow({ entry, workspace }: { entry: PackageAccess; workspace: string }) { | |
| 297 | const fetcher = useFetcher<SettingsOutcome>(); | |
| 298 | const shown = fetcher.formData?.get("intent") === "access-role" ? String(fetcher.formData.get("role")) : entry.role; | |
| 299 | const removing = fetcher.formData?.get("intent") === "access-remove"; | |
| 300 | const slug = entry.name.includes("/") ? entry.name.slice(entry.name.indexOf("/") + 1) : entry.name; | |
| 301 | const who: Record<string, string> = entry.kind === "user" ? { user: entry.name } : { team: slug }; | |
| 302 | return ( | |
| 303 | <li className={`flex flex-wrap items-center gap-x-3 gap-y-2 px-4 py-3 ${removing ? "opacity-50" : ""}`}> | |
| 304 | <span className="flex size-8 shrink-0 items-center justify-center rounded-full bg-raised text-muted"> | |
| 305 | {entry.kind === "user" ? <User size={15} /> : <Users size={15} />} | |
| 306 | </span> | |
| 307 | <div className="min-w-0 grow basis-40"> | |
| 308 | <p className="flex flex-wrap items-center gap-x-2 gap-y-1"> | |
| 309 | <Link | |
| 310 | to={entry.kind === "user" ? `/u/${entry.name}` : `/${workspace}/-/teams/${slug}`} | |
| 311 | className="truncate font-mono text-sm hover:text-accent" | |
| 312 | > | |
| 313 | {entry.name} | |
| 314 | </Link> | |
| 315 | <Badge>{entry.kind === "user" ? "Person" : "Team"}</Badge> | |
| 316 | </p> | |
| 317 | <p className="mt-0.5 text-xs text-faint"> | |
| 318 | Added <TimeAgo at={entry.created_at} /> | |
| 319 | </p> | |
| 320 | {fetcher.data?.error && <ErrorText>{fetcher.data.error}</ErrorText>} | |
| 321 | </div> | |
| 322 | <div className="flex shrink-0 items-center gap-2"> | |
| 323 | <RoleSelect | |
| 324 | value={shown} | |
| 325 | roles={PACKAGE_ROLES} | |
| 326 | label={`Role of ${entry.name}`} | |
| 327 | disabled={fetcher.state !== "idle"} | |
| 328 | onValueChange={(role) => fetcher.submit({ intent: "access-role", ...who, role }, { method: "post" })} | |
| 329 | /> | |
| 330 | <Hint label={`Take ${entry.name}'s role away`}> | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 331 | <Button |
| Merge packages: roles, Actions access, source label, soft delete, API | 332 | type="button" |
| 333 | aria-label={`Remove ${entry.name}`} | |
| 334 | disabled={fetcher.state !== "idle"} | |
| 335 | onClick={() => fetcher.submit({ intent: "access-remove", ...who }, { method: "post" })} | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 336 | variant="ghost" |
| 337 | size="inline" | |
| 338 | className="p-1.5 text-faint hover:text-danger" | |
| Merge packages: roles, Actions access, source label, soft delete, API | 339 | > |
| 340 | <Trash2 size={14} /> | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 341 | </Button> |
| Merge packages: roles, Actions access, source label, soft delete, API | 342 | </Hint> |
| 343 | </div> | |
| 344 | </li> | |
| 345 | ); | |
| 346 | } | |
| 347 | ||
| 348 | /** A person by username, or a team by slug, and the role to give. */ | |
| 349 | function AddAccessForm() { | |
| 350 | const [kind, setKind] = useState<"user" | "team">("user"); | |
| 351 | const [role, setRole] = useState<PackageRole>("read"); | |
| 352 | const fetcher = useFetcher<SettingsOutcome>(); | |
| 353 | const [key, setKey] = useState(0); | |
| 354 | // Cleared once someone was added, ready for the next. | |
| 355 | useEffect(() => { | |
| 356 | if (fetcher.data && !fetcher.data.error) setKey((k) => k + 1); | |
| 357 | }, [fetcher.data]); | |
| 358 | return ( | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 359 | <Card asChild className="space-y-3 p-4"> |
| 360 | <fetcher.Form method="post"> | |
| 361 | <input type="hidden" name="intent" value="access-add" /> | |
| 362 | <input type="hidden" name="kind" value={kind} /> | |
| 363 | <input type="hidden" name="role" value={role} /> | |
| 364 | <div className="grid gap-3 sm:grid-cols-[8rem_minmax(0,1fr)_7rem_auto] sm:items-center"> | |
| 365 | <Select value={kind} onValueChange={(value) => setKind(value as "user" | "team")}> | |
| 366 | <SelectTrigger size="sm" aria-label="Person or team"> | |
| 367 | <SelectValue /> | |
| 368 | </SelectTrigger> | |
| 369 | <SelectContent align="start"> | |
| 370 | <SelectItem value="user">Person</SelectItem> | |
| 371 | <SelectItem value="team">Team</SelectItem> | |
| 372 | </SelectContent> | |
| 373 | </Select> | |
| 374 | <input | |
| 375 | key={key} | |
| 376 | name="who" | |
| 377 | required | |
| 378 | aria-label={kind === "user" ? "Username" : "Team"} | |
| 379 | placeholder={kind === "user" ? "username" : "team slug"} | |
| 380 | className={FIELD} | |
| 381 | /> | |
| 382 | <RoleSelect value={role} roles={PACKAGE_ROLES} label="Role" onValueChange={(value) => setRole(value as PackageRole)} className="w-full" /> | |
| 383 | <SubmitButton variant="outline" fetcher={fetcher} match={{ intent: "access-add" }} pending="Adding…"> | |
| 384 | <UserPlus size={14} /> | |
| 385 | Add | |
| 386 | </SubmitButton> | |
| 387 | </div> | |
| 388 | <p className="text-xs text-faint"> | |
| 389 | {PACKAGE_ROLE_LABEL[role]}: {PACKAGE_ROLE_SUMMARY[role].toLowerCase()} | |
| Merge packages: roles, Actions access, source label, soft delete, API | 390 | </p> |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 391 | {fetcher.data?.error && <ErrorText>{fetcher.data.error}</ErrorText>} |
| 392 | {fetcher.data?.message && ( | |
| 393 | <p className="text-sm text-success" role="status"> | |
| 394 | {fetcher.data.message} | |
| 395 | </p> | |
| 396 | )} | |
| 397 | </fetcher.Form> | |
| 398 | </Card> | |
| Merge packages: roles, Actions access, source label, soft delete, API | 399 | ); |
| 400 | } | |
| 401 | ||
| 402 | function ActionsRow({ entry }: { entry: ActionsAccess }) { | |
| 403 | const fetcher = useFetcher<SettingsOutcome>(); | |
| 404 | const shown = fetcher.formData?.get("intent") === "actions-role" ? String(fetcher.formData.get("role")) : entry.role; | |
| 405 | const removing = fetcher.formData?.get("intent") === "actions-remove"; | |
| 406 | return ( | |
| 407 | <li className={`flex flex-wrap items-center gap-x-3 gap-y-2 px-4 py-3 ${removing ? "opacity-50" : ""}`}> | |
| 408 | <span className="flex size-8 shrink-0 items-center justify-center rounded-md bg-raised text-muted"> | |
| 409 | <Box size={15} /> | |
| 410 | </span> | |
| 411 | <div className="min-w-0 grow basis-40"> | |
| 412 | <p className="flex flex-wrap items-center gap-x-2 gap-y-1"> | |
| 413 | <Link to={`/${entry.repo}`} className="truncate font-mono text-sm hover:text-accent"> | |
| 414 | {entry.repo} | |
| 415 | </Link> | |
| 416 | {entry.linked && <Badge tone="accent">Linked repository</Badge>} | |
| 417 | </p> | |
| 418 | <p className="mt-0.5 text-xs text-faint"> | |
| 419 | {entry.linked ? "Its workflows always publish the package it is linked to." : <>Added {entry.created_at && <TimeAgo at={entry.created_at} />}</>} | |
| 420 | </p> | |
| 421 | {fetcher.data?.error && <ErrorText>{fetcher.data.error}</ErrorText>} | |
| 422 | </div> | |
| 423 | <div className="flex shrink-0 items-center gap-2"> | |
| 424 | {entry.linked ? ( | |
| 425 | <span className="inline-flex h-8 w-28 items-center rounded-md border border-line px-2.5 text-[0.8125rem] text-muted">Write</span> | |
| 426 | ) : ( | |
| 427 | <RoleSelect | |
| 428 | value={shown} | |
| 429 | roles={["read", "write"]} | |
| 430 | label={`Role of ${entry.repo}'s workflows`} | |
| 431 | disabled={fetcher.state !== "idle"} | |
| 432 | onValueChange={(role) => fetcher.submit({ intent: "actions-role", repo: entry.repo, role }, { method: "post" })} | |
| 433 | /> | |
| 434 | )} | |
| 435 | <span className="flex w-[2.125rem] justify-end"> | |
| 436 | {!entry.linked && ( | |
| 437 | <Hint label={`Stop ${entry.repo}'s workflows using it`}> | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 438 | <Button |
| Merge packages: roles, Actions access, source label, soft delete, API | 439 | type="button" |
| 440 | aria-label={`Remove ${entry.repo}`} | |
| 441 | disabled={fetcher.state !== "idle"} | |
| 442 | onClick={() => fetcher.submit({ intent: "actions-remove", repo: entry.repo }, { method: "post" })} | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 443 | variant="ghost" |
| 444 | size="inline" | |
| 445 | className="p-1.5 text-faint hover:text-danger" | |
| Merge packages: roles, Actions access, source label, soft delete, API | 446 | > |
| 447 | <Trash2 size={14} /> | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 448 | </Button> |
| Merge packages: roles, Actions access, source label, soft delete, API | 449 | </Hint> |
| 450 | )} | |
| 451 | </span> | |
| 452 | </div> | |
| 453 | </li> | |
| 454 | ); | |
| 455 | } | |
| 456 | ||
| 457 | function AddActionsForm({ workspace }: { workspace: string }) { | |
| 458 | const [role, setRole] = useState<"read" | "write">("read"); | |
| 459 | const fetcher = useFetcher<SettingsOutcome>(); | |
| 460 | const [key, setKey] = useState(0); | |
| 461 | useEffect(() => { | |
| 462 | if (fetcher.data && !fetcher.data.error) setKey((k) => k + 1); | |
| 463 | }, [fetcher.data]); | |
| 464 | return ( | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 465 | <Card asChild className="space-y-3 p-4"> |
| 466 | <fetcher.Form method="post"> | |
| 467 | <input type="hidden" name="intent" value="actions-add" /> | |
| 468 | <input type="hidden" name="role" value={role} /> | |
| 469 | <div className="grid gap-3 sm:grid-cols-[minmax(0,1fr)_7rem_auto] sm:items-center"> | |
| 470 | <div className="flex h-8 min-w-0 items-center rounded-md border border-line bg-bg pl-2.5 font-mono text-sm focus-within:border-line-strong"> | |
| 471 | <span className="shrink-0 text-faint">{workspace}/</span> | |
| 472 | <input | |
| 473 | key={key} | |
| 474 | name="repo" | |
| 475 | required | |
| 476 | aria-label="Repository" | |
| 477 | placeholder="repository" | |
| 478 | className="h-full min-w-0 grow bg-transparent pr-2.5 placeholder:text-faint focus:outline-none" | |
| 479 | /> | |
| 480 | </div> | |
| 481 | <RoleSelect value={role} roles={["read", "write"]} label="Role" onValueChange={(value) => setRole(value as "read" | "write")} className="w-full" /> | |
| 482 | <SubmitButton variant="outline" fetcher={fetcher} match={{ intent: "actions-add" }} pending="Adding…"> | |
| 483 | Add repository | |
| 484 | </SubmitButton> | |
| Merge packages: roles, Actions access, source label, soft delete, API | 485 | </div> |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 486 | {fetcher.data?.error && <ErrorText>{fetcher.data.error}</ErrorText>} |
| 487 | {fetcher.data?.message && ( | |
| 488 | <p className="text-sm text-success" role="status"> | |
| 489 | {fetcher.data.message} | |
| 490 | </p> | |
| 491 | )} | |
| 492 | </fetcher.Form> | |
| 493 | </Card> | |
| Merge packages: roles, Actions access, source label, soft delete, API | 494 | ); |
| 495 | } | |
| 496 | ||
| 497 | function DeletedVersionRow({ version, npm }: { version: PackageVersion; npm: boolean }) { | |
| 498 | const fetcher = useFetcher<SettingsOutcome>(); | |
| 499 | return ( | |
| 500 | <li className="flex flex-wrap items-center gap-x-4 gap-y-2 px-4 py-3"> | |
| 501 | <div className="min-w-0 grow space-y-1"> | |
| 502 | <p className="flex flex-wrap items-center gap-2"> | |
| 503 | {npm ? ( | |
| 504 | <span className="font-mono text-sm font-medium">{version.version}</span> | |
| 505 | ) : ( | |
| 506 | <Hint label={version.digest}> | |
| 507 | <code className="font-mono text-sm">{shortDigest(version.digest)}</code> | |
| 508 | </Hint> | |
| 509 | )} | |
| 510 | {version.tags.map((tag) => ( | |
| 511 | <Badge key={tag} className="font-mono"> | |
| 512 | {tag} | |
| 513 | </Badge> | |
| 514 | ))} | |
| 515 | </p> | |
| 516 | <p className="flex flex-wrap gap-x-3 text-xs text-faint tabular-nums"> | |
| 517 | <span>{formatBytes(version.size)}</span> | |
| 518 | {version.deleted_at && ( | |
| 519 | <span> | |
| 520 | Deleted {version.deleted_by ? `by ${version.deleted_by} ` : ""} | |
| 521 | <TimeAgo at={version.deleted_at} /> | |
| 522 | </span> | |
| 523 | )} | |
| 524 | {version.purge_at && <span>Purged {new Date(version.purge_at).toLocaleDateString("en-US", { month: "short", day: "numeric" })}</span>} | |
| 525 | </p> | |
| 526 | {fetcher.data?.error && <ErrorText>{fetcher.data.error}</ErrorText>} | |
| 527 | </div> | |
| 528 | <fetcher.Form method="post"> | |
| 529 | <input type="hidden" name="intent" value="restore-version" /> | |
| 530 | <input type="hidden" name="version" value={version.id} /> | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 531 | <SubmitButton variant="outline" fetcher={fetcher} match={{ intent: "restore-version", version: version.id }} pending="Restoring…"> |
| Merge packages: roles, Actions access, source label, soft delete, API | 532 | <RotateCcw size={14} /> |
| 533 | Restore | |
| 534 | </SubmitButton> | |
| 535 | </fetcher.Form> | |
| 536 | </li> | |
| 537 | ); | |
| 538 | } | |
| 539 | ||
| 540 | /** The Settings tab's outline while it loads. */ | |
| 541 | export function PackageSettingsSkeleton() { | |
| 542 | return ( | |
| 543 | <div aria-hidden="true" className="max-w-4xl space-y-8"> | |
| 544 | {[3, 2, 1].map((rows, index) => ( | |
| 545 | <div key={index} 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]"> | |
| 546 | <div className="space-y-2"> | |
| 547 | <SkeletonLine className="w-32" /> | |
| 548 | <Skeleton className="h-3 w-48" /> | |
| 549 | <Skeleton className="h-3 w-40" /> | |
| 550 | </div> | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 551 | <Card tone="plain"> |
| Merge packages: roles, Actions access, source label, soft delete, API | 552 | <SkeletonRows rows={rows} rowClassName="h-14" /> |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 553 | </Card> |
| Merge packages: roles, Actions access, source label, soft delete, API | 554 | </div> |
| 555 | ))} | |
| 556 | </div> | |
| 557 | ); | |
| 558 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.