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.`
+ : ""}
+
+
+
+
+ Cancel
+
+ {
+ if (confirmRevoke) {
+ void handleRevoke(confirmRevoke);
+ setConfirmRevoke(null);
+ }
+ }}
+ >
+ Revoke key
+
+
+
+
);
}
@@ -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
+
+
+
+
);
}