feat: replace Activate All with Deactivate All button and remove Assign Access modal
This commit is contained in:
+7
-11
@@ -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> }>(
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user