diff --git a/packages/react/src/pages/api-keys.tsx b/packages/react/src/pages/api-keys.tsx index eb15084cb..b48e04856 100644 --- a/packages/react/src/pages/api-keys.tsx +++ b/packages/react/src/pages/api-keys.tsx @@ -233,6 +233,7 @@ export function ApiKeysPage(props: { readonly orgKeysSection?: ReactNode }) { // stays mounted for Radix's exit animation (see CreateKeyDialogBody). const [openCount, setOpenCount] = useState(0); const [revokingId, setRevokingId] = useState(null); + const [confirmRevoke, setConfirmRevoke] = useState(null); const handleCreate = async (name: string): Promise => { const exit = await doCreate({ payload: { name }, reactivityKeys: apiKeyWriteKeys }); @@ -312,7 +313,11 @@ export function ApiKeysPage(props: { readonly orgKeysSection?: ReactNode }) {

) : ( - + setConfirmRevoke(key)} + /> ), }) )} @@ -333,6 +338,42 @@ export function ApiKeysPage(props: { readonly orgKeysSection?: ReactNode }) { /> + + {/* A personal key revoke breaks any script or tool using it, so it asks + first — same as the org-key revoke. */} + { + if (!open) setConfirmRevoke(null); + }} + > + + + Revoke API key + + {confirmRevoke + ? `Revoke ${confirmRevoke.name}? Any script or tool authenticating with it loses access immediately. This cannot be undone.` + : ""} + + + + + + + + + + ); } @@ -492,8 +533,7 @@ function OrgApiKeysSectionBody() { - {/* Revoking an org key breaks every backend using it, so it is the one - revoke on this page that asks first. */} + {/* Revoking an org key breaks every backend using it, so it asks first. */} { diff --git a/packages/react/src/pages/org.tsx b/packages/react/src/pages/org.tsx index 8797533af..670c0845f 100644 --- a/packages/react/src/pages/org.tsx +++ b/packages/react/src/pages/org.tsx @@ -14,6 +14,16 @@ import { DialogFooter, DialogClose, } from "../components/dialog"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "../components/alert-dialog"; import { Button } from "../components/button"; import { PageContainer, PageHeader } from "../components/page"; import { Badge } from "../components/badge"; @@ -153,6 +163,7 @@ export function OrgPage(props: { const refreshMembers = useAtomRefresh(orgMembersAtom); const rolesResult = useAtomValue(orgRolesAtom); const doRemove = useAtomSet(removeMember, { mode: "promiseExit" }); + const [removingMember, setRemovingMember] = useState<{ id: string; name: string } | null>(null); const doUpdateRole = useAtomSet(updateMemberRole, { mode: "promiseExit" }); const doUpdateOrgName = useAtomSet(updateOrgName, { mode: "promiseExit" }); const [inviteOpen, setInviteOpen] = useState(false); @@ -179,6 +190,7 @@ export function OrgPage(props: { const showUpgradeOnInvite = atSeatLimit && !!props.upgradeAction; const handleRemove = async (membershipId: string, name: string) => { + setRemovingMember(null); const exit = await doRemove({ params: { membershipId }, reactivityKeys: orgMemberWriteKeys, @@ -406,7 +418,12 @@ export function OrgPage(props: { )} handleRemove(member.id, member.name ?? member.email)} + onClick={() => + setRemovingMember({ + id: member.id, + name: member.name ?? member.email, + }) + } > Remove member @@ -426,6 +443,38 @@ export function OrgPage(props: { {props.dangerZoneSection} + { + if (!open) setRemovingMember(null); + }} + > + + + + {removingMember ? `Remove ${removingMember.name}?` : "Remove member?"} + + + They lose access to this organization immediately. This cannot be undone; you would + need to invite them again. + + + + Cancel + { + if (removingMember !== null) { + void handleRemove(removingMember.id, removingMember.name); + } + }} + > + Remove member + + + + + (null); const ownerDisplay = useOwnerDisplay(); // Policies default to org/workspace. On local this is the hidden Local owner // that v1 local data migrates into. @@ -336,6 +351,7 @@ export function PoliciesPage() { }; const handleRemove = async (policy: { id: string; owner: Owner }) => { + setRemovingPolicy(null); const exit = await doRemove({ params: { policyId: PolicyId.make(policy.id) }, payload: { owner: policy.owner }, @@ -462,7 +478,9 @@ export function PoliciesPage() { isFirst={!reorderable || j === 0} isLast={!reorderable || j === committed.length - 1} showOwnerLabel={ownerDisplay.showOwnerLabels} - onRemove={() => handleRemove({ id: p.id, owner: p.owner })} + onRemove={() => + setRemovingPolicy({ id: p.id, owner: p.owner, pattern: p.pattern }) + } onChangeAction={(action) => handleUpdate({ id: p.id, owner: p.owner }, action) } @@ -490,6 +508,37 @@ export function PoliciesPage() { }, }) )} + + { + if (!open) setRemovingPolicy(null); + }} + > + + + Remove policy? + + {removingPolicy + ? `The rule for "${removingPolicy.pattern}" will be deleted. Tools it governs fall back to the default policy. This cannot be undone.` + : ""} + + + + Cancel + { + if (removingPolicy !== null) { + void handleRemove({ id: removingPolicy.id, owner: removingPolicy.owner }); + } + }} + > + Remove policy + + + + ); }