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, { 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 ──────────────────────────────────────────────────────────── // ── Memberships ────────────────────────────────────────────────────────────
listMemberships: (orgId: string, requestConfig?: RequestConfig) => listMemberships: (orgId: string, requestConfig?: RequestConfig) =>
request<{ memberships: DeviceNetworkMembership[]; count: number }>( request<{ memberships: DeviceNetworkMembership[]; count: number }>(
@@ -1694,6 +1683,13 @@ export const api = {
true, requestConfig, 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) ───────────────────────────────── // ── ZeroTier Controller (org-scoped admin) ─────────────────────────────────
getZtStatus: (orgId: string, requestConfig?: RequestConfig) => getZtStatus: (orgId: string, requestConfig?: RequestConfig) =>
request<{ status: Record<string, unknown> }>( request<{ status: Record<string, unknown> }>(
-86
View File
@@ -11,7 +11,6 @@ import {
Loader2, Loader2,
Search, Search,
MoreHorizontal, MoreHorizontal,
UserPlus,
Trash2, Trash2,
RefreshCw, RefreshCw,
Skull, Skull,
@@ -131,13 +130,6 @@ export default function AccessPage() {
const [isApproving, setIsApproving] = useState(false); const [isApproving, setIsApproving] = useState(false);
const [rejectConfirmId, setRejectConfirmId] = useState<string | null>(null); 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 [showKillSwitch, setShowKillSwitch] = useState(false);
const [killTargetUserId, setKillTargetUserId] = useState(""); const [killTargetUserId, setKillTargetUserId] = useState("");
const [killScope, setKillScope] = useState<"organization" | "global">("organization"); 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 () => { const handleKillSwitch = async () => {
if (!orgId) return; if (!orgId) return;
setKillError(null); setKillError(null);
@@ -436,9 +405,6 @@ export default function AccessPage() {
{networks.map((n) => <SelectItem key={n.id} value={n.id}>{n.name}</SelectItem>)} {networks.map((n) => <SelectItem key={n.id} value={n.id}>{n.name}</SelectItem>)}
</SelectContent> </SelectContent>
</Select> </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"> <Button variant="destructive" onClick={() => setShowKillSwitch(true)} className="gap-2">
<Skull className="w-4 h-4" /> Kill Switch <Skull className="w-4 h-4" /> Kill Switch
</Button> </Button>
@@ -906,58 +872,6 @@ export default function AccessPage() {
</TabsContent> </TabsContent>
</Tabs> </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 */} {/* Kill Switch Dialog */}
<Dialog open={showKillSwitch} onOpenChange={(open) => { if (!open) setShowKillSwitch(false); }}> <Dialog open={showKillSwitch} onOpenChange={(open) => { if (!open) setShowKillSwitch(false); }}>
<DialogContent className="sm:max-w-lg"> <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 [confirmRemoveUser, setConfirmRemoveUser] = useState<string | null>(null);
const [showActivateDialog, setShowActivateDialog] = useState<string | null>(null); const [showActivateDialog, setShowActivateDialog] = useState<string | null>(null);
const [activateLifetime, setActivateLifetime] = useState("480"); const [activateLifetime, setActivateLifetime] = useState("480");
const [activatingAll, setActivatingAll] = useState(false); const [deactivatingAll, setDeactivatingAll] = useState(false);
const [deactivateReason, setDeactivateReason] = useState("");
// Add New Membership dialog state // Add New Membership dialog state
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false); const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
@@ -296,19 +297,19 @@ export default function NetworkManagementPage() {
} }
}; };
const handleActivateAll = async () => { const handleDeactivateAll = async () => {
if (!orgId) return; if (!orgId) return;
setActivatingAll(true); setDeactivatingAll(true);
try { try {
const lifetime = parseInt(activateLifetime); const res = await api.zerotier.networkKillSwitch(orgId, networkId!, { reason: deactivateReason.trim() || undefined });
const res = await api.zerotier.activateAllMemberships(orgId, lifetime); toast({ title: "All memberships deactivated", description: `${res.count} memberships deactivated.` });
toast({ title: "All memberships activated", description: `${res.count} memberships activated for ${lifetime} minutes.` }); setDeactivateReason("");
const result = await api.zerotier.getNetworkMembers(orgId, networkId!); const result = await api.zerotier.getNetworkMembers(orgId, networkId!);
setMembers(result.memberships || []); setMembers(result.memberships || []);
} catch (err) { } 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 { } finally {
setActivatingAll(false); setDeactivatingAll(false);
} }
}; };
@@ -653,33 +654,27 @@ export default function NetworkManagementPage() {
</CardContent> </CardContent>
</Card> </Card>
{/* Activate All section */} {/* Deactivate All section */}
{members.filter(m => m.status === "approved" && !m.active).length > 0 && ( {members.filter(m => m.active).length > 0 && (
<Card className="mt-4"> <Card className="mt-4 border-orange-200">
<CardContent className="p-4"> <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"> <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"> <span className="text-sm font-medium">
{members.filter(m => m.active).length} active,{" "} {members.filter(m => m.active).length} active {members.filter(m => m.active).length === 1 ? "membership" : "memberships"}
{members.filter(m => m.status === "approved" && !m.active).length} ready to activate
</span> </span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-3">
<div className="flex items-center gap-1"> <Input
<span className="text-xs text-muted-foreground">Duration:</span> placeholder="Reason (optional)"
<Input value={deactivateReason}
type="number" onChange={(e) => setDeactivateReason(e.target.value)}
value={activateLifetime} className="h-8 w-44 text-xs"
onChange={(e) => setActivateLifetime(e.target.value)} />
className="h-8 w-20 text-xs" <Button size="sm" variant="destructive" onClick={handleDeactivateAll} disabled={deactivatingAll} className="gap-1">
placeholder="480" {deactivatingAll ? <Loader2 className="w-3 h-3 animate-spin" /> : <ZapOff className="w-3 h-3" />}
/> Deactivate All
<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
</Button> </Button>
</div> </div>
</div> </div>