Skip to content

Commit 48ba434

Browse files
committed
feat: add revoke all buttons to agents and hosts dashboard pages
Made-with: Cursor
1 parent f01a2a1 commit 48ba434

File tree

2 files changed

+74
-0
lines changed

2 files changed

+74
-0
lines changed

examples/gmail-proxy/app/dashboard/agents/page.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,7 @@ export default function AgentsPage() {
374374
const [expanded, setExpanded] = useState<string | null>(null);
375375
const [activeTab, setActiveTab] = useState<Record<string, "details" | "activity">>({});
376376
const [revoking, setRevoking] = useState<string | null>(null);
377+
const [revokingAll, setRevokingAll] = useState(false);
377378
const [editingAgent, setEditingAgent] = useState<string | null>(null);
378379
const [availableCaps, setAvailableCaps] = useState<{ name: string; description: string }[]>([]);
379380
const [selectedCaps, setSelectedCaps] = useState<Set<string>>(new Set());
@@ -459,6 +460,31 @@ export default function AgentsPage() {
459460
}
460461
};
461462

463+
const handleRevokeAll = async () => {
464+
const activeAgents = agents.filter((a) => a.status === "active");
465+
if (activeAgents.length === 0) return;
466+
if (!window.confirm(`Revoke all ${activeAgents.length} active agent${activeAgents.length !== 1 ? "s" : ""}? This cannot be undone.`)) return;
467+
setRevokingAll(true);
468+
try {
469+
await Promise.all(
470+
activeAgents.map((a) =>
471+
fetch("/api/auth/agent/revoke", {
472+
method: "POST",
473+
headers: { "Content-Type": "application/json" },
474+
body: JSON.stringify({ agent_id: a.agent_id }),
475+
}),
476+
),
477+
);
478+
setAgents((prev) =>
479+
prev.map((a) => (a.status === "active" ? { ...a, status: "revoked" } : a)),
480+
);
481+
} catch {
482+
/* ignore */
483+
} finally {
484+
setRevokingAll(false);
485+
}
486+
};
487+
462488
const filters = ["all", "active", "pending", "expired", "revoked"];
463489
const filteredCount = agents.length;
464490

@@ -474,6 +500,16 @@ export default function AgentsPage() {
474500
: `${filteredCount} agent${filteredCount !== 1 ? "s" : ""} connected`}
475501
</p>
476502
</div>
503+
<div className="flex items-center gap-2">
504+
{agents.some((a) => a.status === "active") && (
505+
<button
506+
onClick={handleRevokeAll}
507+
disabled={revokingAll}
508+
className="cursor-pointer rounded-lg border border-red-200 px-3 py-1.5 text-[12px] font-medium text-red-600 transition-all hover:bg-red-50 disabled:opacity-50"
509+
>
510+
{revokingAll ? "Revoking..." : "Revoke All"}
511+
</button>
512+
)}
477513
<div className="flex gap-0.5 rounded-lg border border-gray-200 bg-white p-0.5">
478514
{filters.map((f) => (
479515
<button
@@ -489,6 +525,7 @@ export default function AgentsPage() {
489525
</button>
490526
))}
491527
</div>
528+
</div>
492529
</div>
493530

494531
{loading ? (

examples/gmail-proxy/app/dashboard/hosts/page.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export default function HostsPage() {
6565
const [filter, setFilter] = useState("all");
6666
const [expanded, setExpanded] = useState<string | null>(null);
6767
const [revoking, setRevoking] = useState<string | null>(null);
68+
const [revokingAll, setRevokingAll] = useState(false);
6869
const [editingHost, setEditingHost] = useState<string | null>(null);
6970
const [availableCaps, setAvailableCaps] = useState<{ name: string; description: string }[]>([]);
7071
const [selectedCaps, setSelectedCaps] = useState<Set<string>>(new Set());
@@ -137,6 +138,31 @@ export default function HostsPage() {
137138
}
138139
};
139140

141+
const handleRevokeAll = async () => {
142+
const activeHosts = hosts.filter((h) => h.status === "active");
143+
if (activeHosts.length === 0) return;
144+
if (!window.confirm(`Revoke all ${activeHosts.length} active host${activeHosts.length !== 1 ? "s" : ""} and their agents? This cannot be undone.`)) return;
145+
setRevokingAll(true);
146+
try {
147+
await Promise.all(
148+
activeHosts.map((h) =>
149+
fetch("/api/auth/host/revoke", {
150+
method: "POST",
151+
headers: { "Content-Type": "application/json" },
152+
body: JSON.stringify({ host_id: h.id }),
153+
}),
154+
),
155+
);
156+
setHosts((prev) =>
157+
prev.map((h) => (h.status === "active" ? { ...h, status: "revoked" } : h)),
158+
);
159+
} catch {
160+
/* ignore */
161+
} finally {
162+
setRevokingAll(false);
163+
}
164+
};
165+
140166
const filters = ["all", "active", "pending", "pending_enrollment", "revoked"];
141167

142168
return (
@@ -149,6 +175,16 @@ export default function HostsPage() {
149175
Agent host environments and their configurations.
150176
</p>
151177
</div>
178+
<div className="flex items-center gap-2">
179+
{hosts.some((h) => h.status === "active") && (
180+
<button
181+
onClick={handleRevokeAll}
182+
disabled={revokingAll}
183+
className="cursor-pointer rounded-full border border-gmail-red/20 px-3 py-1.5 text-xs font-medium text-gmail-red transition-colors hover:bg-gmail-red/5 disabled:opacity-50"
184+
>
185+
{revokingAll ? "Revoking..." : "Revoke All"}
186+
</button>
187+
)}
152188
<div className="flex gap-0.5 rounded-full border border-border bg-white p-0.5 shadow-sm">
153189
{filters.map((f) => (
154190
<button
@@ -164,6 +200,7 @@ export default function HostsPage() {
164200
</button>
165201
))}
166202
</div>
203+
</div>
167204
</div>
168205

169206
{loading ? (

0 commit comments

Comments
 (0)