Access requests inbox
Requests for access to a resource, with approve and deny buttons for the owner. It loads its data on the server through the block's client.
pnpm dev:example) to see it.Add the components
terminal npx shadcn add alert badge button card empty skeleton tableInstall the glue
Install the block with
npx better-supabase add access-requests. The glue items are copied into your app, so edit them as you need.None: the page calls the block's client on the server.
Write the screen
React and shadcn
View in the exampleaccess-requests-inbox.tsx /** * Requests for a temporary role in the active organization. Members ask * for one; members with `members.update_role` approve or deny it, and the * role goes back to the previous one when the duration runs out. */ export async function AccessRequestsInbox() { if (!can(await getSession(), "access_requests.create")) { forbidden(); } const [t, format, result] = await Promise.all([ getExtracted("accessRequests"), getFormatter(), loadInbox(), ]); if (result.kind === "error") { return ( <Alert variant="destructive" data-recipe="access-requests-inbox"> <TriangleAlertIcon /> <AlertTitle>{t("Access requests could not be loaded")}</AlertTitle> <AlertDescription>{t("Try again in a moment.")}</AlertDescription> <AlertAction> <form action={refreshAccessRequests}> <Button size="sm" variant="outline" type="submit"> {t("Retry")} </Button> </form> </AlertAction> </Alert> ); } if (result.data === null) { return ( <p className="text-sm text-muted-foreground" data-recipe="access-requests-inbox" > {t("Pick an organization to see its access requests.")} </p> ); } const { requests, role, userId, canDecide } = result.data; const canRequest = role !== undefined && role !== "owner"; const statusLabel = (status: AccessRequestStatus) => { switch (status) { case "pending": { return t("Pending"); } case "granted": { return t("Granted"); } case "rejected": { return t("Denied"); } case "cancelled": { return t("Withdrawn"); } case "expired": { return t("Expired"); } case "ended": { return t("Ended"); } default: { const unknown: never = status; return unknown; } } }; const roleLabel = (value: string) => value === "admin" ? t("Admin") : value === "member" ? t("Member") : value; const durationLabel = (value: string | null) => { switch (value) { case null: { return t("Until revoked"); } case "PT1H": { return t("1 hour"); } case "P1D": { return t("1 day"); } case "P7D": { return t("7 days"); } default: { return value; } } }; const date = (value: string) => format.dateTime(new Date(value), { dateStyle: "medium", timeStyle: "short", }); return ( <div className="flex flex-col gap-6" data-recipe="access-requests-inbox"> {canRequest ? ( <Card id="request-access"> <CardHeader> <CardTitle>{t("Request access")}</CardTitle> <CardDescription> {t( "Ask for a higher role for a limited time. An admin or owner approves it.", )} </CardDescription> </CardHeader> <CardContent> <RequestAccessForm currentRole={role} /> </CardContent> </Card> ) : null} <Card> <CardHeader> <CardTitle>{t("Requests")}</CardTitle> <CardDescription> {canDecide ? t("Approve or deny what members asked for.") : t("The requests you sent and what happened to them.")} </CardDescription> </CardHeader> <CardContent> {requests.length === 0 ? ( <Empty> <EmptyHeader> <EmptyMedia variant="icon"> <InboxIcon /> </EmptyMedia> <EmptyTitle>{t("No access requests")}</EmptyTitle> <EmptyDescription> {canRequest ? t("Requests you and other members send show up here.") : t( "Requests from members show up here for you to decide.", )} </EmptyDescription> </EmptyHeader> <EmptyContent> {canRequest ? ( <a href="#request-access" className={buttonVariants({ variant: "outline" })} > {t("Request a role")} </a> ) : ( <Link href="/settings/members" className={buttonVariants({ variant: "outline" })} > {t("View members")} </Link> )} </EmptyContent> </Empty> ) : ( <Table> <TableHeader> <TableRow> <TableHead>{t("Member")}</TableHead> <TableHead>{t("Role")}</TableHead> <TableHead className="hidden md:table-cell"> {t("Duration")} </TableHead> <TableHead>{t("Status")}</TableHead> <TableHead className="text-right"> <span className="sr-only">{t("Actions")}</span> </TableHead> </TableRow> </TableHeader> <TableBody> {requests.map((request) => ( <TableRow key={request.id}> <TableCell> <div className="flex flex-col gap-1"> <span className="font-medium"> {request.userId === userId ? t("You") : request.requester} </span> <span className="text-xs text-muted-foreground"> {request.reason ?? date(request.createdAt)} </span> </div> </TableCell> <TableCell>{roleLabel(request.role)}</TableCell> <TableCell className="hidden text-muted-foreground md:table-cell"> {request.status === "granted" && request.expiresAt ? t("Until {date}", { date: date(request.expiresAt) }) : durationLabel(request.duration)} </TableCell> <TableCell> <Badge variant={STATUS_VARIANT[request.status]}> {statusLabel(request.status)} </Badge> </TableCell> <TableCell className="text-right"> <AccessRequestActions request={request} mine={request.userId === userId} canDecide={canDecide} /> </TableCell> </TableRow> ))} </TableBody> </Table> )} </CardContent> </Card> </div> ); }