import { createFileRoute } from "@tanstack/react-router"; import { useState } from "react"; import { Exit } from "effect"; import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"; import { useAtom, useAtomValue } from "@effect/atom-react"; import { toast } from "@executor-js/react/components/sonner"; import { Button } from "@executor-js/react/components/button"; import { CopyButton } from "@executor-js/react/components/copy-button"; import { Input } from "@executor-js/react/components/input"; import { Label } from "@executor-js/react/components/label"; import { NativeSelect, NativeSelectOption } from "@executor-js/react/components/native-select"; import { useExecutorDocumentTitle } from "@executor-js/react/lib/document-title"; import { orgMembersAtom, removeMember, updateMemberRole, } from "@executor-js/react/api/account-atoms"; import { orgMemberWriteKeys } from "@executor-js/react/api/reactivity-keys"; import { createInvite, invitesAtom, inviteWriteKeys, revokeInvite } from "../../admin-atoms"; export const Route = createFileRoute("/{-$orgSlug}/admin")({ component: AdminPage, }); const ROLES = ["member", "admin"] as const; // Instance admin console. Members reuse the shared account atoms; invite codes // are the self-host join mechanism. The API gates to owner/admin, so a // non-admin who opens this just sees load failures. function AdminPage() { useExecutorDocumentTitle("Admin"); return (

Admin

Manage members and invite links for this instance.

); } function MembersSection() { const result = useAtomValue(orgMembersAtom); const [roleState, doUpdateRole] = useAtom(updateMemberRole, { mode: "promiseExit" }); const [removeState, doRemove] = useAtom(removeMember, { mode: "promiseExit" }); // The mutation atoms carry their own in-flight state — no manual busy tracking. const busy = AsyncResult.isWaiting(roleState) || AsyncResult.isWaiting(removeState); const changeRole = async (membershipId: string, roleSlug: string) => { const exit = await doUpdateRole({ params: { membershipId }, payload: { roleSlug }, reactivityKeys: orgMemberWriteKeys, }); toast[Exit.isSuccess(exit) ? "success" : "error"]( Exit.isSuccess(exit) ? "Role updated" : "Failed to update role", ); }; const remove = async (membershipId: string, label: string) => { const exit = await doRemove({ params: { membershipId }, reactivityKeys: orgMemberWriteKeys }); toast[Exit.isSuccess(exit) ? "success" : "error"]( Exit.isSuccess(exit) ? `Removed ${label}` : "Failed to remove member", ); }; return (

Members

{AsyncResult.match(result, { onInitial: () => Loading members…, onFailure: () => Admin access required., onSuccess: ({ value }) => (
{value.members.map((member) => { const isOwner = member.role === "owner"; return (

{member.name ?? member.email} {member.isCurrentUser ? " (you)" : ""}

{member.email}

{isOwner || member.isCurrentUser ? ( {member.role} ) : ( <> changeRole(member.id, e.target.value)} > {ROLES.map((role) => ( {role} ))} )}
); })}
), })}
); } function InvitesSection() { const result = useAtomValue(invitesAtom); const [createState, doCreate] = useAtom(createInvite, { mode: "promiseExit" }); const [, doRevoke] = useAtom(revokeInvite, { mode: "promiseExit" }); const [role, setRole] = useState("member"); const [label, setLabel] = useState(""); const creating = AsyncResult.isWaiting(createState); const create = async () => { const exit = await doCreate({ payload: { role, label: label.trim() || undefined }, reactivityKeys: inviteWriteKeys, }); if (Exit.isSuccess(exit)) { setLabel(""); setRole("member"); toast.success("Invite link created"); return; } toast.error("Failed to create invite"); }; const revoke = async (inviteId: string) => { const exit = await doRevoke({ params: { inviteId }, reactivityKeys: inviteWriteKeys }); toast[Exit.isSuccess(exit) ? "success" : "error"]( Exit.isSuccess(exit) ? "Invite revoked" : "Failed to revoke invite", ); }; return (

Invite links

setLabel(e.target.value)} />
setRole(e.target.value)}> {ROLES.map((r) => ( {r} ))}
{AsyncResult.match(result, { onInitial: () => Loading invites…, onFailure: () => Admin access required., onSuccess: ({ value }) => { const pending = value.invites.filter((i) => !i.usedAt); const used = value.invites.filter((i) => i.usedAt); return (
{pending.length > 0 && (
{pending.map((invite) => (
{invite.code} {invite.label ? `${invite.label} · ` : ""} {invite.role}
))}
)} {used.length > 0 && (

Redeemed

{used.map((invite) => (
{invite.code} {invite.label ? `${invite.label} · ` : ""} used by {invite.usedByEmail}
))}
)} {pending.length === 0 && used.length === 0 && ( No invite links yet — create one to add someone. )}
); }, })}
); } function Notice({ children, tone }: { children: React.ReactNode; tone?: "destructive" }) { return (
{children}
); }