feat: replace Activate All with Deactivate All button and remove Assign Access modal

This commit is contained in:
2026-05-30 06:34:20 +00:00
parent ff2beae9d0
commit 087b8f002f
3 changed files with 32 additions and 127 deletions
+7 -11
View File
@@ -1584,17 +1584,6 @@ export const api = {
{ method: "POST" }, true, requestConfig,
),
assignAccess: (orgId: string, data: {
target_user_id: string;
portal_network_id: string;
justification?: string;
}, requestConfig?: RequestConfig) =>
request<{ approval: UserNetworkApproval }>(
`/organizations/${orgId}/approvals/assign`,
{ method: "POST", body: JSON.stringify(data) },
true, requestConfig,
),
// ── Memberships ────────────────────────────────────────────────────────────
listMemberships: (orgId: string, requestConfig?: RequestConfig) =>
request<{ memberships: DeviceNetworkMembership[]; count: number }>(
@@ -1694,6 +1683,13 @@ export const api = {
true, requestConfig,
),
networkKillSwitch: (orgId: string, networkId: string, data?: { reason?: string }, requestConfig?: RequestConfig) =>
request<{ message: string; count: number }>(
`/organizations/${orgId}/networks/${networkId}/kill-switch`,
{ method: "POST", body: data ? JSON.stringify(data) : "{}" },
true, requestConfig,
),
// ── ZeroTier Controller (org-scoped admin) ─────────────────────────────────
getZtStatus: (orgId: string, requestConfig?: RequestConfig) =>
request<{ status: Record<string, unknown> }>(
-86
View File
@@ -11,7 +11,6 @@ import {
Loader2,
Search,
MoreHorizontal,
UserPlus,
Trash2,
RefreshCw,
Skull,
@@ -131,13 +130,6 @@ export default function AccessPage() {
const [isApproving, setIsApproving] = useState(false);
const [rejectConfirmId, setRejectConfirmId] = useState<string | null>(null);
const [showAssign, setShowAssign] = useState(false);
const [assignUserId, setAssignUserId] = useState("");
const [assignNetworkId, setAssignNetworkId] = useState("");
const [assignJustification, setAssignJustification] = useState("");
const [isAssigning, setIsAssigning] = useState(false);
const [assignError, setAssignError] = useState<string | null>(null);
const [showKillSwitch, setShowKillSwitch] = useState(false);
const [killTargetUserId, setKillTargetUserId] = useState("");
const [killScope, setKillScope] = useState<"organization" | "global">("organization");
@@ -252,29 +244,6 @@ export default function AccessPage() {
}
};
const handleAssign = async () => {
if (!orgId) return;
setAssignError(null);
if (!assignUserId) { setAssignError("Please select a user."); return; }
if (!assignNetworkId) { setAssignError("Please select a network."); return; }
setIsAssigning(true);
try {
await api.zerotier.assignAccess(orgId, {
target_user_id: assignUserId,
portal_network_id: assignNetworkId,
justification: assignJustification.trim() || undefined,
});
toast({ title: "Access assigned", description: "The user can now register devices for this network." });
setShowAssign(false);
setAssignUserId(""); setAssignNetworkId(""); setAssignJustification("");
fetchData();
} catch (err) {
setAssignError(err instanceof ApiError ? err.message : "Failed to assign access.");
} finally {
setIsAssigning(false);
}
};
const handleKillSwitch = async () => {
if (!orgId) return;
setKillError(null);
@@ -436,9 +405,6 @@ export default function AccessPage() {
{networks.map((n) => <SelectItem key={n.id} value={n.id}>{n.name}</SelectItem>)}
</SelectContent>
</Select>
<Button variant="outline" onClick={() => setShowAssign(true)} className="gap-2">
<UserPlus className="w-4 h-4" /> Assign Access
</Button>
<Button variant="destructive" onClick={() => setShowKillSwitch(true)} className="gap-2">
<Skull className="w-4 h-4" /> Kill Switch
</Button>
@@ -906,58 +872,6 @@ export default function AccessPage() {
</TabsContent>
</Tabs>
{/* Assign Access Dialog */}
<Dialog open={showAssign} onOpenChange={(open) => { if (!open) setShowAssign(false); }}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>Assign Network Access</DialogTitle>
<DialogDescription>Grant a user direct access to a network without a request.</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-2">
<div className="space-y-2">
<Label>User *</Label>
<Select value={assignUserId} onValueChange={setAssignUserId}>
<SelectTrigger><SelectValue placeholder="Select a user…" /></SelectTrigger>
<SelectContent>
{orgMembers.map((m) => (
<SelectItem key={m.user_id} value={m.user_id}>
{m.user?.full_name || m.user?.email}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Network *</Label>
<Select value={assignNetworkId} onValueChange={setAssignNetworkId}>
<SelectTrigger><SelectValue placeholder="Select a network…" /></SelectTrigger>
<SelectContent>
{networks.map((n) => (
<SelectItem key={n.id} value={n.id}>{n.name}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Justification (optional)</Label>
<Input
placeholder="Engineering team access"
value={assignJustification}
onChange={(e) => setAssignJustification(e.target.value)}
/>
</div>
{assignError && <p className="text-sm text-destructive">{assignError}</p>}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowAssign(false)} disabled={isAssigning}>Cancel</Button>
<Button onClick={handleAssign} disabled={isAssigning}>
{isAssigning && <Loader2 className="w-4 h-4 mr-2 animate-spin" />}
Assign Access
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Kill Switch Dialog */}
<Dialog open={showKillSwitch} onOpenChange={(open) => { if (!open) setShowKillSwitch(false); }}>
<DialogContent className="sm:max-w-lg">
+25 -30
View File
@@ -131,7 +131,8 @@ export default function NetworkManagementPage() {
const [confirmRemoveUser, setConfirmRemoveUser] = useState<string | null>(null);
const [showActivateDialog, setShowActivateDialog] = useState<string | null>(null);
const [activateLifetime, setActivateLifetime] = useState("480");
const [activatingAll, setActivatingAll] = useState(false);
const [deactivatingAll, setDeactivatingAll] = useState(false);
const [deactivateReason, setDeactivateReason] = useState("");
// Add New Membership dialog state
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
@@ -296,19 +297,19 @@ export default function NetworkManagementPage() {
}
};
const handleActivateAll = async () => {
const handleDeactivateAll = async () => {
if (!orgId) return;
setActivatingAll(true);
setDeactivatingAll(true);
try {
const lifetime = parseInt(activateLifetime);
const res = await api.zerotier.activateAllMemberships(orgId, lifetime);
toast({ title: "All memberships activated", description: `${res.count} memberships activated for ${lifetime} minutes.` });
const res = await api.zerotier.networkKillSwitch(orgId, networkId!, { reason: deactivateReason.trim() || undefined });
toast({ title: "All memberships deactivated", description: `${res.count} memberships deactivated.` });
setDeactivateReason("");
const result = await api.zerotier.getNetworkMembers(orgId, networkId!);
setMembers(result.memberships || []);
} catch (err) {
toast({ variant: "destructive", title: "Failed to activate all", description: err instanceof ApiError ? err.message : "Something went wrong." });
toast({ variant: "destructive", title: "Failed to deactivate all", description: err instanceof ApiError ? err.message : "Something went wrong." });
} finally {
setActivatingAll(false);
setDeactivatingAll(false);
}
};
@@ -653,33 +654,27 @@ export default function NetworkManagementPage() {
</CardContent>
</Card>
{/* Activate All section */}
{members.filter(m => m.status === "approved" && !m.active).length > 0 && (
<Card className="mt-4">
{/* Deactivate All section */}
{members.filter(m => m.active).length > 0 && (
<Card className="mt-4 border-orange-200">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-2">
<Zap className="w-4 h-4 text-green-500" />
<ZapOff className="w-4 h-4 text-orange-500" />
<span className="text-sm font-medium">
{members.filter(m => m.active).length} active,{" "}
{members.filter(m => m.status === "approved" && !m.active).length} ready to activate
{members.filter(m => m.active).length} active {members.filter(m => m.active).length === 1 ? "membership" : "memberships"}
</span>
</div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-1">
<span className="text-xs text-muted-foreground">Duration:</span>
<Input
type="number"
value={activateLifetime}
onChange={(e) => setActivateLifetime(e.target.value)}
className="h-8 w-20 text-xs"
placeholder="480"
/>
<span className="text-xs text-muted-foreground">min</span>
</div>
<Button size="sm" onClick={handleActivateAll} disabled={activatingAll} className="gap-1">
{activatingAll ? <Loader2 className="w-3 h-3 animate-spin" /> : <Zap className="w-3 h-3" />}
Activate All
<div className="flex items-center gap-3">
<Input
placeholder="Reason (optional)"
value={deactivateReason}
onChange={(e) => setDeactivateReason(e.target.value)}
className="h-8 w-44 text-xs"
/>
<Button size="sm" variant="destructive" onClick={handleDeactivateAll} disabled={deactivatingAll} className="gap-1">
{deactivatingAll ? <Loader2 className="w-3 h-3 animate-spin" /> : <ZapOff className="w-3 h-3" />}
Deactivate All
</Button>
</div>
</div>