feat(prod): deploy web dashboard, migrate mongodb configuration, resolve server components render error and fix logo static asset paths

This commit is contained in:
Rafif-Riqullah-Siregar committed 2026-07-15 00:27:55 +07:00
1 parent 4882108068
commit b2ea883601
119 files changed
+7123 -2011

No files matched your search

@@ -0,0 +1,123 @@
"use client";
import { AgentModals } from "@/components/admin/AgentModals";
import ExternalAccountModal from "@/components/admin/ExternalAccountModal";
import AgentLocationModal from "@/components/admin/AgentLocationModal";
import { Agent } from "@/lib/actions/agents";
import { type ManagedUser } from "@/lib/admin-api";
interface AgentsPageModalsProps {
isCreateOpen: boolean;
setIsCreateOpen: (open: boolean) => void;
isDeleteOpen: boolean;
setIsDeleteOpen: (open: boolean) => void;
selectedAgent: Agent | null;
loadAgents: () => Promise<void>;
detailAgent: Agent | null;
setDetailAgent: (agent: Agent | null) => void;
accountModalOpen: boolean;
setAccountModalOpen: (open: boolean) => void;
accountModalAgent: { uuid: string; label: string } | null;
setAccountModalAgent: (agent: { uuid: string; label: string } | null) => void;
managedUsers: ManagedUser[];
externalModalOpen: boolean;
setExternalModalOpen: (open: boolean) => void;
externalModalUser: ManagedUser | null;
setExternalModalUser: (user: ManagedUser | null) => void;
targetSiteUuid: string | null;
setTargetSiteUuid: (uuid: string | null) => void;
targetCreatedBy: string | null;
setTargetCreatedBy: (createdBy: string | null) => void;
isLocModalOpen: boolean;
setIsLocModalOpen: (open: boolean) => void;
locModalAgentUuid: string;
setLocModalAgentUuid: (uuid: string) => void;
locModalAgentLabel: string;
setLocModalAgentLabel: (label: string) => void;
locModalCurrentLoc: any | null;
setLocModalCurrentLoc: (loc: any | null) => void;
loadLocations: () => Promise<void>;
}
export default function AgentsPageModals({
isCreateOpen,
setIsCreateOpen,
isDeleteOpen,
setIsDeleteOpen,
selectedAgent,
loadAgents,
detailAgent,
setDetailAgent,
accountModalOpen,
setAccountModalOpen,
accountModalAgent,
setAccountModalAgent,
managedUsers,
externalModalOpen,
setExternalModalOpen,
externalModalUser,
setExternalModalUser,
targetSiteUuid,
setTargetSiteUuid,
targetCreatedBy,
setTargetCreatedBy,
isLocModalOpen,
setIsLocModalOpen,
locModalAgentUuid,
setLocModalAgentUuid,
locModalAgentLabel,
setLocModalAgentLabel,
locModalCurrentLoc,
setLocModalCurrentLoc,
loadLocations,
}: AgentsPageModalsProps) {
return (
<>
<AgentModals
isCreateOpen={isCreateOpen}
setIsCreateOpen={setIsCreateOpen}
isDeleteOpen={isDeleteOpen}
setIsDeleteOpen={setIsDeleteOpen}
selectedAgent={selectedAgent}
onSuccess={loadAgents}
detailAgent={detailAgent}
setDetailAgent={setDetailAgent}
accountModalOpen={accountModalOpen}
setAccountModalOpen={setAccountModalOpen}
accountModalAgent={accountModalAgent}
setAccountModalAgent={setAccountModalAgent}
managedUsers={managedUsers}
/>
<ExternalAccountModal
isOpen={externalModalOpen}
onClose={() => {
setExternalModalOpen(false);
setExternalModalUser(null);
setTargetSiteUuid(null);
setTargetCreatedBy(null);
}}
onSuccess={loadAgents}
user={externalModalUser}
targetSiteUuid={targetSiteUuid}
targetCreatedBy={targetCreatedBy}
/>
<AgentLocationModal
isOpen={isLocModalOpen}
onClose={() => {
setIsLocModalOpen(false);
setLocModalAgentUuid("");
setLocModalAgentLabel("");
setLocModalCurrentLoc(null);
}}
onSuccess={() => {
loadLocations();
}}
agentUuid={locModalAgentUuid}
agentLabel={locModalAgentLabel}
currentLocation={locModalCurrentLoc}
/>
</>
);
}
@@ -0,0 +1,47 @@
"use client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
import { DataTable, Column } from "@/components/ui/DataTable";
import { Loader2 } from "lucide-react";
import { Agent } from "@/lib/actions/agents";
interface AgentsTableSectionProps {
agents: Agent[];
isLoading: boolean;
columns: Column<Agent>[];
}
export default function AgentsTableSection({
agents,
isLoading,
columns,
}: AgentsTableSectionProps) {
return (
<Card className="bg-card/50 backdrop-blur-sm border-border/50">
<CardHeader>
<CardTitle className="text-white">Provisioned Network Agents ({agents.length})</CardTitle>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-8 h-8 animate-spin text-primary" />
</div>
) : (
<DataTable
data={agents}
columns={columns}
searchPlaceholder="Search agents..."
searchFilter={(row, query) => {
const q = query.toLowerCase();
return (
(row.uuid || "").toLowerCase().includes(q) ||
(row.serial || "").toLowerCase().includes(q) ||
(row.label || "").toLowerCase().includes(q)
);
}}
/>
)}
</CardContent>
</Card>
);
}
@@ -0,0 +1,207 @@
"use client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
import { DataTable, Column } from "@/components/ui/DataTable";
import { Plus, Loader2 } from "lucide-react";
import { type ManagedUser } from "@/lib/admin-api";
interface ExternalAccountsSectionProps {
role: string | null;
isLoading: boolean;
externalUsers: ManagedUser[];
superadminUsers: ManagedUser[];
siabUsers: ManagedUser[];
nexusUsers: ManagedUser[];
externalColumns: Column<ManagedUser>[];
setExternalModalUser: (user: ManagedUser | null) => void;
setTargetSiteUuid: (uuid: string | null) => void;
setTargetCreatedBy: (createdBy: string | null) => void;
setExternalModalOpen: (open: boolean) => void;
}
export default function ExternalAccountsSection({
role,
isLoading,
externalUsers,
superadminUsers,
siabUsers,
nexusUsers,
externalColumns,
setExternalModalUser,
setTargetSiteUuid,
setTargetCreatedBy,
setExternalModalOpen,
}: ExternalAccountsSectionProps) {
if (role === 'SUPER_ADMIN') {
return (
<div className="space-y-6">
{/* Table 1: Superadmin (BackOne) Accounts */}
<Card className="bg-card/50 backdrop-blur-sm border-border/50">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-white flex items-center gap-2">
<span>Superadmin (BackOne) External Accounts ({superadminUsers.length})</span>
</CardTitle>
<button
onClick={() => {
setExternalModalUser(null);
setTargetSiteUuid(null);
setTargetCreatedBy('admin');
setExternalModalOpen(true);
}}
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-semibold bg-amber-500/20 text-amber-400 hover:bg-amber-500/30 border border-amber-500/30 rounded-lg transition-colors"
>
<Plus className="w-3.5 h-3.5" />
Add External Account
</button>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-8 h-8 animate-spin text-primary" />
</div>
) : (
<DataTable
data={superadminUsers}
columns={externalColumns}
searchPlaceholder="Search accounts..."
searchFilter={(row, query) => {
const q = query.toLowerCase();
return (
(row.username || "").toLowerCase().includes(q) ||
(row.account_name || "").toLowerCase().includes(q) ||
(row.role || "").toLowerCase().includes(q)
);
}}
/>
)}
</CardContent>
</Card>
{/* Table 2: SIAB Accounts */}
<Card className="bg-card/50 backdrop-blur-sm border-border/50">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-white">SIAB Tenant External Accounts ({siabUsers.length})</CardTitle>
<button
onClick={() => {
setExternalModalUser(null);
setTargetSiteUuid('6681452d_9cae_4ff4_8ae8_0d504774265e');
setTargetCreatedBy('siab');
setExternalModalOpen(true);
}}
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-semibold bg-amber-500/20 text-amber-400 hover:bg-amber-500/30 border border-amber-500/30 rounded-lg transition-colors"
>
<Plus className="w-3.5 h-3.5" />
Add External Account
</button>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-8 h-8 animate-spin text-primary" />
</div>
) : (
<DataTable
data={siabUsers}
columns={externalColumns}
searchPlaceholder="Search accounts..."
searchFilter={(row, query) => {
const q = query.toLowerCase();
return (
(row.username || "").toLowerCase().includes(q) ||
(row.account_name || "").toLowerCase().includes(q) ||
(row.role || "").toLowerCase().includes(q)
);
}}
/>
)}
</CardContent>
</Card>
{/* Table 3: Nexus Accounts */}
<Card className="bg-card/50 backdrop-blur-sm border-border/50">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-white">Nexus Tenant External Accounts ({nexusUsers.length})</CardTitle>
<button
onClick={() => {
setExternalModalUser(null);
setTargetSiteUuid('d7902405_0dc2_458b_8584_ed4d24b64f24');
setTargetCreatedBy('nexus');
setExternalModalOpen(true);
}}
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-semibold bg-amber-500/20 text-amber-400 hover:bg-amber-500/30 border border-amber-500/30 rounded-lg transition-colors"
>
<Plus className="w-3.5 h-3.5" />
Add External Account
</button>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-8 h-8 animate-spin text-primary" />
</div>
) : (
<DataTable
data={nexusUsers}
columns={externalColumns}
searchPlaceholder="Search accounts..."
searchFilter={(row, query) => {
const q = query.toLowerCase();
return (
(row.username || "").toLowerCase().includes(q) ||
(row.account_name || "").toLowerCase().includes(q) ||
(row.role || "").toLowerCase().includes(q)
);
}}
/>
)}
</CardContent>
</Card>
</div>
);
}
if (role === 'TENANT_ADMIN') {
return (
<Card className="bg-card/50 backdrop-blur-sm border-border/50">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-white">External Accounts Inventory ({externalUsers.length})</CardTitle>
<button
onClick={() => {
setExternalModalUser(null);
setTargetSiteUuid(null);
setTargetCreatedBy(null);
setExternalModalOpen(true);
}}
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-semibold bg-amber-500/20 text-amber-400 hover:bg-amber-500/30 border border-amber-500/30 rounded-lg transition-colors"
>
<Plus className="w-3.5 h-3.5" />
Add External Account
</button>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-8 h-8 animate-spin text-primary" />
</div>
) : (
<DataTable
data={externalUsers}
columns={externalColumns}
searchPlaceholder="Search accounts..."
searchFilter={(row, query) => {
const q = query.toLowerCase();
return (
(row.username || "").toLowerCase().includes(q) ||
(row.account_name || "").toLowerCase().includes(q) ||
(row.role || "").toLowerCase().includes(q)
);
}}
/>
)}
</CardContent>
</Card>
);
}
return null;
}
@@ -0,0 +1,50 @@
"use client";
import { Server, CheckCircle2, XCircle, Activity } from "lucide-react";
interface UptimeStatsCardsProps {
totalAgents: number;
onlineAgents: number;
offlineAgents: number;
avgUptime: number;
}
export default function UptimeStatsCards({
totalAgents,
onlineAgents,
offlineAgents,
avgUptime,
}: UptimeStatsCardsProps) {
return (
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 text-xs font-semibold">
<div className="bg-slate-900/50 backdrop-blur-sm border border-slate-800/80 p-5 rounded-2xl flex flex-col relative overflow-hidden group">
<div className="absolute top-0 right-0 p-3 opacity-10 group-hover:opacity-20 transition-opacity">
<Server className="w-12 h-12 text-blue-400" />
</div>
<span className="text-slate-400 uppercase tracking-wider mb-1">Total Agents</span>
<span className="text-2xl font-black text-white tracking-tight">{totalAgents}</span>
</div>
<div className="bg-slate-900/50 backdrop-blur-sm border border-slate-800/80 p-5 rounded-2xl flex flex-col relative overflow-hidden group">
<div className="absolute top-0 right-0 p-3 opacity-10 group-hover:opacity-20 transition-opacity">
<CheckCircle2 className="w-12 h-12 text-emerald-400" />
</div>
<span className="text-slate-400 uppercase tracking-wider mb-1">Online Agents</span>
<span className="text-2xl font-black text-emerald-400 tracking-tight">{onlineAgents}</span>
</div>
<div className="bg-slate-900/50 backdrop-blur-sm border border-slate-800/80 p-5 rounded-2xl flex flex-col relative overflow-hidden group">
<div className="absolute top-0 right-0 p-3 opacity-10 group-hover:opacity-20 transition-opacity">
<XCircle className="w-12 h-12 text-red-400" />
</div>
<span className="text-slate-400 uppercase tracking-wider mb-1">Offline Agents</span>
<span className="text-2xl font-black text-red-400 tracking-tight">{offlineAgents}</span>
</div>
<div className="bg-slate-900/50 backdrop-blur-sm border border-slate-800/80 p-5 rounded-2xl flex flex-col relative overflow-hidden group">
<div className="absolute top-0 right-0 p-3 opacity-10 group-hover:opacity-20 transition-opacity">
<Activity className="w-12 h-12 text-cyan-400" />
</div>
<span className="text-slate-400 uppercase tracking-wider mb-1">Avg. Historical Uptime</span>
<span className="text-2xl font-black text-cyan-400 tracking-tight">{avgUptime.toFixed(2)}%</span>
</div>
</div>
);
}
+44 -81
View File
@@ -4,7 +4,7 @@ import { Column } from "@/components/ui/DataTable";
import { Agent } from "@/lib/actions/agents";
import { Badge } from "@/components/ui/Badge";
import { resolveAgentLabel, type ManagedUser } from "@/lib/admin-api";
import { Loader2, Trash2, CheckCircle2, XCircle, ChevronRight, UserCog, Eye } from "lucide-react";
import { Loader2, Trash2, CheckCircle2, XCircle, ChevronRight, UserCog, Eye, MapPin } from "lucide-react";
interface GetColumnsProps {
role: string | null;
@@ -17,6 +17,8 @@ interface GetColumnsProps {
setAccountModalAgent: (agent: { uuid: string; label: string } | null) => void;
handleViewAs: (uuid: string, label: string) => void;
openDelete: (agent: Agent) => void;
onEditLocation?: (agentUuid: string, agentLabel: string) => void;
locations?: { agent_uuid: string; latitude: number; longitude: number; label?: string }[];
}
export function getAgentColumns({
@@ -30,11 +32,14 @@ export function getAgentColumns({
setAccountModalAgent,
handleViewAs,
openDelete,
onEditLocation,
locations = [],
}: GetColumnsProps): Column<Agent>[] {
const isSuperAdmin = role === 'SUPER_ADMIN';
const cols: Column<Agent>[] = [
{
header: "UUID / Serial",
className: "w-[16%] text-center",
className: isSuperAdmin ? "w-[12%] text-center" : "w-[14%] text-center",
accessor: (row) => (
<button onClick={() => setDetailAgent(row)} className="flex items-center justify-center gap-2 group text-center mx-auto" title="Click to view agent details">
<span className="font-mono text-xs text-blue-400 group-hover:text-blue-300 transition-colors">{row.uuid || row.serial}</span>
@@ -44,7 +49,7 @@ export function getAgentColumns({
},
{
header: "Label",
className: "w-[24%] text-center",
className: isSuperAdmin ? "w-[18%] text-center" : "w-[24%] text-center",
accessor: (row) => {
const uuid = row.uuid || row.serial;
const dynamicLabel = resolveAgentLabel(uuid, managedUsers);
@@ -63,17 +68,17 @@ export function getAgentColumns({
},
{
header: "Provisioned",
className: "w-[12%] text-center",
className: isSuperAdmin ? "w-[10%] text-center" : "w-[11%] text-center",
accessor: (row) => row.provisioned ? <Badge variant="green"><CheckCircle2 className="w-3 h-3 mr-1" /> Yes</Badge> : <Badge variant="red"><XCircle className="w-3 h-3 mr-1" /> No</Badge>
},
{
header: "Activated",
className: "w-[12%] text-center",
className: isSuperAdmin ? "w-[10%] text-center" : "w-[11%] text-center",
accessor: (row) => row.activated ? <Badge variant="green"><CheckCircle2 className="w-3 h-3 mr-1" /> Yes</Badge> : <Badge variant="red"><XCircle className="w-3 h-3 mr-1" /> No</Badge>
},
{
header: "Status",
className: "w-[11%] text-center",
className: "w-[12%] text-center",
accessor: (row) => {
const statusText = row.last_seen_at?.human || '-';
if (statusText === 'Online') {
@@ -87,7 +92,7 @@ export function getAgentColumns({
},
{
header: "Historical Uptime",
className: "w-[12%] text-center",
className: "w-[10%] text-center",
accessor: (row) => {
const uuid = row.uuid || row.serial;
const uptime = uptimeMap[uuid] ?? 100;
@@ -99,7 +104,7 @@ export function getAgentColumns({
},
{
header: "Actions",
className: "w-[13%] text-center",
className: "w-[18%] text-center",
accessor: (row) => {
const uuid = row.uuid || row.serial;
const dynamicLabel = resolveAgentLabel(uuid, managedUsers);
@@ -112,6 +117,22 @@ export function getAgentColumns({
</button>
{(role === 'SUPER_ADMIN' || role === 'TENANT_ADMIN') && (
<>
{onEditLocation && (() => {
const hasCoords = locations.some(l => l.agent_uuid === uuid);
return (
<button
onClick={(e) => { e.stopPropagation(); onEditLocation(uuid, dynamicLabel !== uuid ? dynamicLabel : (row.label || uuid)); }}
className={`p-1.5 rounded transition-colors ${
hasCoords
? "text-blue-400 hover:text-blue-300 hover:bg-blue-950/30"
: "text-red-400/60 hover:text-red-400 hover:bg-red-950/30"
}`}
title={hasCoords ? "Edit Geolocation (Dikonfigurasi)" : "Configure Geolocation (Belum Dikonfigurasi)"}
>
<MapPin className="w-4 h-4" />
</button>
);
})()}
<button
onClick={(e) => { e.stopPropagation(); setAccountModalAgent({ uuid, label: dynamicLabel !== uuid ? dynamicLabel : (row.label || uuid) }); setAccountModalOpen(true); }}
className="p-1.5 text-emerald-400 hover:text-emerald-300 hover:bg-emerald-950/30 rounded transition-colors"
@@ -141,11 +162,11 @@ export function getAgentColumns({
if (role === 'SUPER_ADMIN') {
cols.splice(cols.length - 1, 0, {
header: "Data Size",
className: "w-[12%] text-center",
className: "w-[10%] text-center",
accessor: (row) => {
const uuid = row.uuid || row.serial;
const sizeMB = storageMap[uuid] ?? 0;
return <span className="font-mono text-slate-300 font-semibold">{sizeMB.toFixed(2)} MB</span>;
return <span className="font-mono text-slate-300 font-semibold">{formatStorageSize(sizeMB)}</span>;
}
});
}
@@ -153,75 +174,17 @@ export function getAgentColumns({
return cols;
}
export function getExternalAccountColumns(
onManageAccount: (user: ManagedUser) => void
): Column<ManagedUser>[] {
return [
{
header: "Account Name",
className: "w-[25%] text-left",
accessor: (row) => (
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-slate-800 flex items-center justify-center border border-slate-700 shrink-0 overflow-hidden">
{row.profile_picture ? (
<img src={`/api/uploads/${row.profile_picture}`} alt={row.account_name || row.username} className="w-full h-full object-cover" />
) : (
<span className="text-xs font-bold text-slate-400">{(row.account_name || row.username).substring(0, 2).toUpperCase()}</span>
)}
</div>
<span className="font-medium text-white">{row.account_name || "-"}</span>
</div>
)
},
{
header: "Username",
className: "w-[25%] text-left",
accessor: (row) => <span className="text-slate-300 font-mono text-sm">{row.username}</span>
},
{
header: "Role",
className: "w-[20%] text-center",
accessor: (row) => {
let badgeColor = "bg-slate-500/20 text-slate-400 border-slate-500/30";
if (row.role === 'SUPER_ADMIN') badgeColor = "bg-purple-500/20 text-purple-400 border-purple-500/30";
if (row.role === 'SOC_ANALYST') badgeColor = "bg-amber-500/20 text-amber-400 border-amber-500/30";
if (row.role === 'ENGINEER') badgeColor = "bg-blue-500/20 text-blue-400 border-blue-500/30";
if (row.role === 'TENANT_ADMIN') badgeColor = "bg-emerald-500/20 text-emerald-400 border-emerald-500/30";
return (
<span className={`px-2.5 py-1 rounded-full text-xs font-semibold border ${badgeColor}`}>
{row.role.replace('_', ' ')}
</span>
);
}
},
{
header: "Status",
className: "w-[15%] text-center",
accessor: (row) => (
<div className="flex items-center justify-center gap-2">
{/* Defaulting to active since we don't have is_active explicitly in ManagedUser interface yet, though API returns it. We'll check if it exists, else assume Active */}
{(row as any).is_active !== false ? (
<Badge variant="green"><CheckCircle2 className="w-3 h-3 mr-1" /> Active</Badge>
) : (
<Badge variant="red"><XCircle className="w-3 h-3 mr-1" /> Inactive</Badge>
)}
</div>
)
},
{
header: "Actions",
className: "w-[15%] text-center",
accessor: (row) => (
<button
onClick={() => onManageAccount(row)}
className="p-1.5 text-blue-400 hover:text-blue-300 hover:bg-blue-950/30 rounded transition-colors disabled:opacity-50"
title="Manage Account"
disabled={row.role === 'SUPER_ADMIN'}
>
<UserCog className="w-4 h-4" />
</button>
)
}
];
function formatStorageSize(sizeMB: number): string {
if (sizeMB === 0) return "0.00 MB";
if (sizeMB >= 1024 * 1024) {
return `${(sizeMB / (1024 * 1024)).toFixed(2)} TB`;
}
if (sizeMB >= 1024) {
return `${(sizeMB / 1024).toFixed(2)} GB`;
}
if (sizeMB < 1) {
return `${(sizeMB * 1024).toFixed(2)} KB`;
}
return `${sizeMB.toFixed(2)} MB`;
}
@@ -0,0 +1,78 @@
"use client";
import { Column } from "@/components/ui/DataTable";
import { Badge } from "@/components/ui/Badge";
import { type ManagedUser } from "@/lib/admin-api";
import { UserCog, CheckCircle2, XCircle } from "lucide-react";
export function getExternalAccountColumns(
onManageAccount: (user: ManagedUser) => void
): Column<ManagedUser>[] {
return [
{
header: "Account Name",
className: "w-[25%] text-left",
accessor: (row) => (
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-slate-800 flex items-center justify-center border border-slate-700 shrink-0 overflow-hidden">
{row.profile_picture ? (
<img src={`/api/uploads/${row.profile_picture}`} alt={row.account_name || row.username} className="w-full h-full object-cover" />
) : (
<span className="text-xs font-bold text-slate-400">{(row.account_name || row.username).substring(0, 2).toUpperCase()}</span>
)}
</div>
<span className="font-medium text-white">{row.account_name || "-"}</span>
</div>
)
},
{
header: "Username",
className: "w-[25%] text-left",
accessor: (row) => <span className="text-slate-300 font-mono text-sm">{row.username}</span>
},
{
header: "Role",
className: "w-[20%] text-center",
accessor: (row) => {
let badgeColor = "bg-slate-500/20 text-slate-400 border-slate-500/30";
if (row.role === 'SUPER_ADMIN') badgeColor = "bg-purple-500/20 text-purple-400 border-purple-500/30";
if (row.role === 'SOC_ANALYST') badgeColor = "bg-amber-500/20 text-amber-400 border-amber-500/30";
if (row.role === 'ENGINEER') badgeColor = "bg-blue-500/20 text-blue-400 border-blue-500/30";
if (row.role === 'TENANT_ADMIN') badgeColor = "bg-emerald-500/20 text-emerald-400 border-emerald-500/30";
return (
<span className={`px-2.5 py-1 rounded-full text-xs font-semibold border ${badgeColor}`}>
{row.role.replace('_', ' ')}
</span>
);
}
},
{
header: "Status",
className: "w-[15%] text-center",
accessor: (row) => (
<div className="flex items-center justify-center gap-2">
{(row as any).is_active !== false ? (
<Badge variant="green"><CheckCircle2 className="w-3 h-3 mr-1" /> Active</Badge>
) : (
<Badge variant="red"><XCircle className="w-3 h-3 mr-1" /> Inactive</Badge>
)}
</div>
)
},
{
header: "Actions",
className: "w-[15%] text-center",
accessor: (row) => (
<button
onClick={() => onManageAccount(row)}
className="p-1.5 text-blue-400 hover:text-blue-300 hover:bg-blue-950/30 rounded transition-colors disabled:opacity-50"
title="Manage Account"
disabled={row.role === 'SUPER_ADMIN'}
>
<UserCog className="w-4 h-4" />
</button>
)
}
];
}
+139 -257
View File
@@ -1,151 +1,75 @@
"use client";
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { getAgents, Agent } from "@/lib/actions/agents";
import { Loader2, Plus, Server } from "lucide-react";
import { getAgentColumns } from "./columns";
import { getExternalAccountColumns } from "./externalColumns";
import ExternalAccountsSection from "./ExternalAccountsSection";
import UptimeStatsCards from "./UptimeStatsCards";
import AgentsTableSection from "./AgentsTableSection";
import AgentsPageModals from "./AgentsPageModals";
import { useAgentsData } from "./useAgentsData";
import { HelpTrigger } from "@/components/help/HelpTrigger";
import dynamic from "next/dynamic";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
import { DataTable } from "@/components/ui/DataTable";
import { Cpu, History, Radio, Server, Activity, Plus, Loader2, CheckCircle2, XCircle } from "lucide-react";
import { getAdminUsers, startViewAs, resolveAgentLabel, getViewAsHeaders, type ManagedUser } from "@/lib/admin-api";
import { useTimeFilter } from "@/contexts/TimeFilterContext";
import { usePollingKey } from "@/lib/usePolling";
import { AgentModals } from "@/components/admin/AgentModals";
import ExternalAccountModal from "@/components/admin/ExternalAccountModal";
import { getAgentColumns, getExternalAccountColumns } from "./columns";
const IndonesiaAgentMap = dynamic(
() => import("@/components/dashboard/IndonesiaAgentMap").then(m => ({ default: m.IndonesiaAgentMap })),
{
ssr: false,
loading: () => (
<div className="h-[320px] w-full rounded-2xl border border-white/5 bg-slate-900/40 flex items-center justify-center">
<div className="flex flex-col items-center gap-2">
<Loader2 className="h-6 w-6 animate-spin text-primary" />
<span className="text-xs text-slate-400">Loading Agent Location Map...</span>
</div>
</div>
),
}
);
export default function AgentsPage() {
const router = useRouter();
const [role, setRole] = useState<string | null>(null);
const [agents, setAgents] = useState<Agent[]>([]);
const [uptimeMap, setUptimeMap] = useState<Record<string, number>>({});
const [storageMap, setStorageMap] = useState<Record<string, number>>({});
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState("");
const [isCreateOpen, setIsCreateOpen] = useState(false);
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
const [detailAgent, setDetailAgent] = useState<Agent | null>(null);
const [managedUsers, setManagedUsers] = useState<ManagedUser[]>([]);
const [accountModalOpen, setAccountModalOpen] = useState(false);
const [accountModalAgent, setAccountModalAgent] = useState<{ uuid: string; label: string } | null>(null);
const [externalModalOpen, setExternalModalOpen] = useState(false);
const [externalModalUser, setExternalModalUser] = useState<ManagedUser | null>(null);
const [viewAsLoading, setViewAsLoading] = useState<string | null>(null);
const [selectedAgent, setSelectedAgent] = useState<Agent | null>(null);
const { timeRange } = useTimeFilter();
const refreshKey = usePollingKey(60000); // Auto-refresh every 60 seconds (1 minute)
const loadUptime = async () => {
try {
const res = await fetch(`/api/dashboard/agents/uptime?timeRange=${timeRange}`, {
headers: getViewAsHeaders()
});
if (res.ok) {
const json = await res.json();
if (json.ok && json.uptime) setUptimeMap(json.uptime);
}
} catch (err) {
console.error("Failed to load uptime stats:", err);
}
};
const loadStorage = async () => {
try {
const res = await fetch('/api/dashboard/agents/storage', {
headers: getViewAsHeaders()
});
if (res.ok) {
const json = await res.json();
if (json.ok && json.storage) setStorageMap(json.storage);
}
} catch (err) {
console.error("Failed to load agent storage stats:", err);
}
};
const loadAgents = async () => {
setIsLoading(true);
try {
const siteUuid = localStorage.getItem("backone_site_uuid") || undefined;
const [agentData, userData] = await Promise.allSettled([
getAgents(siteUuid),
getAdminUsers(),
]);
if (agentData.status === 'fulfilled') setAgents(agentData.value);
else setError((agentData.reason as Error).message);
if (userData.status === 'fulfilled') setManagedUsers(userData.value);
await loadUptime();
} catch (e: any) {
setError(e.message);
}
setIsLoading(false);
};
const handleViewAs = async (agentUuid: string, agentLabel: string) => {
setViewAsLoading(agentUuid);
try {
await startViewAs(agentUuid, agentLabel);
router.refresh();
window.location.reload();
} catch (err: any) {
alert('Failed to enter View As mode: ' + err.message);
} finally {
setViewAsLoading(null);
}
};
useEffect(() => {
fetch('/api/auth/me', { headers: getViewAsHeaders() })
.then(res => res.json())
.then(data => {
if (data.user) {
setRole(data.user.role || null);
if (data.user.role === 'AGENT_VIEWER') {
router.push('/');
} else {
loadAgents();
}
} else {
setIsLoading(false);
}
})
.catch(err => {
console.error("Auth check failed:", err);
setIsLoading(false);
});
}, [router]);
useEffect(() => {
if (role && role !== 'AGENT_VIEWER') loadUptime();
}, [timeRange]);
useEffect(() => {
if (role === 'SUPER_ADMIN') {
loadStorage();
}
}, [role]);
useEffect(() => {
if (role && role !== 'AGENT_VIEWER' && refreshKey > 0) {
const siteUuid = localStorage.getItem("backone_site_uuid") || undefined;
getAgents(siteUuid)
.then(data => setAgents(data))
.catch(err => console.warn("Failed to auto-refresh agents:", err));
loadUptime();
if (role === 'SUPER_ADMIN') {
loadStorage();
}
}
}, [refreshKey, role]);
const openDelete = (agent: Agent) => {
setSelectedAgent(agent);
setIsDeleteOpen(true);
};
const {
role,
agents,
uptimeMap,
storageMap,
isLoading,
error,
isCreateOpen,
setIsCreateOpen,
isDeleteOpen,
setIsDeleteOpen,
detailAgent,
setDetailAgent,
managedUsers,
accountModalOpen,
setAccountModalOpen,
accountModalAgent,
setAccountModalAgent,
externalModalOpen,
setExternalModalOpen,
externalModalUser,
setExternalModalUser,
targetSiteUuid,
setTargetSiteUuid,
targetCreatedBy,
setTargetCreatedBy,
locations,
isLocModalOpen,
setIsLocModalOpen,
locModalAgentUuid,
setLocModalAgentUuid,
locModalAgentLabel,
setLocModalAgentLabel,
locModalCurrentLoc,
setLocModalCurrentLoc,
viewAsLoading,
selectedAgent,
loadAgents,
loadLocations,
handleViewAs,
openDelete,
handleEditLocation,
} = useAgentsData();
const columns = getAgentColumns({
role,
@@ -158,6 +82,8 @@ export default function AgentsPage() {
setAccountModalAgent,
handleViewAs,
openDelete,
onEditLocation: handleEditLocation,
locations,
});
const totalAgents = agents.length;
@@ -167,6 +93,10 @@ export default function AgentsPage() {
const avgUptime = uptimeValues.length > 0 ? uptimeValues.reduce((s, v) => s + v, 0) / uptimeValues.length : 100;
const externalUsers = managedUsers.filter(u => u.role !== 'AGENT_VIEWER');
const superadminUsers = externalUsers.filter(u => u.role === 'SUPER_ADMIN' || !u.site_uuid || u.site_uuid === 'default' || (u.site_uuid !== '6681452d_9cae_4ff4_8ae8_0d504774265e' && u.site_uuid !== 'd7902405_0dc2_458b_8584_ed4d24b64f24'));
const siabUsers = externalUsers.filter(u => u.role !== 'SUPER_ADMIN' && u.site_uuid === '6681452d_9cae_4ff4_8ae8_0d504774265e');
const nexusUsers = externalUsers.filter(u => u.role !== 'SUPER_ADMIN' && u.site_uuid === 'd7902405_0dc2_458b_8584_ed4d24b64f24');
const externalColumns = getExternalAccountColumns((user) => {
setExternalModalUser(user);
setExternalModalOpen(true);
@@ -174,21 +104,24 @@ export default function AgentsPage() {
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div className="flex items-center justify-between gap-4">
<h2 className="text-2xl font-bold tracking-tight text-white flex items-center gap-3">
<Server className="w-6 h-6 text-primary" />
Agents Inventory
{isLoading && <Loader2 className="w-5 h-5 animate-spin text-muted-foreground" />}
</h2>
{(role === 'SUPER_ADMIN' || role === 'TENANT_ADMIN') && (
<button
onClick={() => setIsCreateOpen(true)}
className="flex items-center gap-2 bg-primary hover:bg-primary/95 text-primary-foreground px-4 py-2 rounded-xl text-sm font-semibold transition-all shadow-lg shadow-blue-500/10"
>
<Plus className="w-4 h-4" />
Provision Agent
</button>
)}
<div className="flex items-center gap-3">
<HelpTrigger pageId="agents" />
{(role === 'SUPER_ADMIN' || role === 'TENANT_ADMIN') && (
<button
onClick={() => setIsCreateOpen(true)}
className="flex items-center gap-2 bg-primary hover:bg-primary/95 text-primary-foreground px-4 py-2 rounded-xl text-sm font-semibold transition-all shadow-lg shadow-blue-500/10"
>
<Plus className="w-4 h-4" />
Provision Agent
</button>
)}
</div>
</div>
{error && (
@@ -197,111 +130,50 @@ export default function AgentsPage() {
</div>
)}
{/* Historical Uptime Status Cards Grid */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 text-xs font-semibold">
<div className="bg-slate-900/50 backdrop-blur-sm border border-slate-800/80 p-5 rounded-2xl flex flex-col relative overflow-hidden group">
<div className="absolute top-0 right-0 p-3 opacity-10 group-hover:opacity-20 transition-opacity">
<Server className="w-12 h-12 text-blue-400" />
</div>
<span className="text-slate-400 uppercase tracking-wider mb-1">Total Agents</span>
<span className="text-2xl font-black text-white tracking-tight">{totalAgents}</span>
</div>
<div className="bg-slate-900/50 backdrop-blur-sm border border-slate-800/80 p-5 rounded-2xl flex flex-col relative overflow-hidden group">
<div className="absolute top-0 right-0 p-3 opacity-10 group-hover:opacity-20 transition-opacity">
<CheckCircle2 className="w-12 h-12 text-emerald-400" />
</div>
<span className="text-slate-400 uppercase tracking-wider mb-1">Online Agents</span>
<span className="text-2xl font-black text-emerald-400 tracking-tight">{onlineAgents}</span>
</div>
<div className="bg-slate-900/50 backdrop-blur-sm border border-slate-800/80 p-5 rounded-2xl flex flex-col relative overflow-hidden group">
<div className="absolute top-0 right-0 p-3 opacity-10 group-hover:opacity-20 transition-opacity">
<XCircle className="w-12 h-12 text-red-400" />
</div>
<span className="text-slate-400 uppercase tracking-wider mb-1">Offline Agents</span>
<span className="text-2xl font-black text-red-400 tracking-tight">{offlineAgents}</span>
</div>
<div className="bg-slate-900/50 backdrop-blur-sm border border-slate-800/80 p-5 rounded-2xl flex flex-col relative overflow-hidden group">
<div className="absolute top-0 right-0 p-3 opacity-10 group-hover:opacity-20 transition-opacity">
<Activity className="w-12 h-12 text-cyan-400" />
</div>
<span className="text-slate-400 uppercase tracking-wider mb-1">Avg. Historical Uptime</span>
<span className="text-2xl font-black text-cyan-400 tracking-tight">{avgUptime.toFixed(2)}%</span>
</div>
</div>
<UptimeStatsCards
totalAgents={totalAgents}
onlineAgents={onlineAgents}
offlineAgents={offlineAgents}
avgUptime={avgUptime}
/>
<Card className="bg-card/50 backdrop-blur-sm border-border/50">
<CardHeader>
<CardTitle className="text-white">Provisioned Network Agents ({agents.length})</CardTitle>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-8 h-8 animate-spin text-primary" />
</div>
) : (
<DataTable
data={agents}
columns={columns}
searchPlaceholder="Search agents..."
searchFilter={(row, query) => {
const q = query.toLowerCase();
return (
(row.uuid || "").toLowerCase().includes(q) ||
(row.serial || "").toLowerCase().includes(q) ||
(row.label || "").toLowerCase().includes(q)
);
}}
/>
)}
</CardContent>
</Card>
{(role === 'SUPER_ADMIN' || role === 'TENANT_ADMIN') && (
<Card className="bg-card/50 backdrop-blur-sm border-border/50">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-white">External Accounts Inventory ({externalUsers.length})</CardTitle>
<button
onClick={() => {
setExternalModalUser(null); // Mode Create
setExternalModalOpen(true);
}}
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-semibold bg-amber-500/20 text-amber-400 hover:bg-amber-500/30 border border-amber-500/30 rounded-lg transition-colors"
>
<Plus className="w-3.5 h-3.5" />
Add External Account
</button>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-8 h-8 animate-spin text-primary" />
</div>
) : (
<DataTable
data={externalUsers}
columns={externalColumns}
searchPlaceholder="Search accounts..."
searchFilter={(row, query) => {
const q = query.toLowerCase();
return (
(row.username || "").toLowerCase().includes(q) ||
(row.account_name || "").toLowerCase().includes(q) ||
(row.role || "").toLowerCase().includes(q)
);
}}
/>
)}
</CardContent>
</Card>
{/* Indonesia Agent Geolocation & Flow Map */}
{!isLoading && (
<IndonesiaAgentMap
agents={agents}
locations={locations}
onEditLocation={handleEditLocation}
role={role}
/>
)}
<AgentModals
<AgentsTableSection
agents={agents}
isLoading={isLoading}
columns={columns}
/>
<ExternalAccountsSection
role={role}
isLoading={isLoading}
externalUsers={externalUsers}
superadminUsers={superadminUsers}
siabUsers={siabUsers}
nexusUsers={nexusUsers}
externalColumns={externalColumns}
setExternalModalUser={setExternalModalUser}
setTargetSiteUuid={setTargetSiteUuid}
setTargetCreatedBy={setTargetCreatedBy}
setExternalModalOpen={setExternalModalOpen}
/>
<AgentsPageModals
isCreateOpen={isCreateOpen}
setIsCreateOpen={setIsCreateOpen}
isDeleteOpen={isDeleteOpen}
setIsDeleteOpen={setIsDeleteOpen}
selectedAgent={selectedAgent}
onSuccess={loadAgents}
loadAgents={loadAgents}
detailAgent={detailAgent}
setDetailAgent={setDetailAgent}
accountModalOpen={accountModalOpen}
@@ -309,13 +181,23 @@ export default function AgentsPage() {
accountModalAgent={accountModalAgent}
setAccountModalAgent={setAccountModalAgent}
managedUsers={managedUsers}
/>
<ExternalAccountModal
isOpen={externalModalOpen}
onClose={() => { setExternalModalOpen(false); setExternalModalUser(null); }}
onSuccess={loadAgents}
user={externalModalUser}
externalModalOpen={externalModalOpen}
setExternalModalOpen={setExternalModalOpen}
externalModalUser={externalModalUser}
setExternalModalUser={setExternalModalUser}
targetSiteUuid={targetSiteUuid}
setTargetSiteUuid={setTargetSiteUuid}
targetCreatedBy={targetCreatedBy}
setTargetCreatedBy={setTargetCreatedBy}
isLocModalOpen={isLocModalOpen}
setIsLocModalOpen={setIsLocModalOpen}
locModalAgentUuid={locModalAgentUuid}
setLocModalAgentUuid={setLocModalAgentUuid}
locModalAgentLabel={locModalAgentLabel}
setLocModalAgentLabel={setLocModalAgentLabel}
locModalCurrentLoc={locModalCurrentLoc}
setLocModalCurrentLoc={setLocModalCurrentLoc}
loadLocations={loadLocations}
/>
</div>
);
+229
View File
@@ -0,0 +1,229 @@
"use client";
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { getAgents, Agent } from "@/lib/actions/agents";
import { getAdminUsers, startViewAs, getViewAsHeaders, type ManagedUser } from "@/lib/admin-api";
import { useTimeFilter } from "@/contexts/TimeFilterContext";
import { usePollingKey } from "@/lib/usePolling";
export function useAgentsData() {
const router = useRouter();
const [role, setRole] = useState<string | null>(null);
const [agents, setAgents] = useState<Agent[]>([]);
const [uptimeMap, setUptimeMap] = useState<Record<string, number>>({});
const [storageMap, setStorageMap] = useState<Record<string, number>>({});
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState("");
const [isCreateOpen, setIsCreateOpen] = useState(false);
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
const [detailAgent, setDetailAgent] = useState<Agent | null>(null);
const [managedUsers, setManagedUsers] = useState<ManagedUser[]>([]);
const [accountModalOpen, setAccountModalOpen] = useState(false);
const [accountModalAgent, setAccountModalAgent] = useState<{ uuid: string; label: string } | null>(null);
const [externalModalOpen, setExternalModalOpen] = useState(false);
const [externalModalUser, setExternalModalUser] = useState<ManagedUser | null>(null);
const [targetSiteUuid, setTargetSiteUuid] = useState<string | null>(null);
const [targetCreatedBy, setTargetCreatedBy] = useState<string | null>(null);
const [selectedSite, setSelectedSite] = useState<string>("6681452d_9cae_4ff4_8ae8_0d504774265e");
const [locations, setLocations] = useState<any[]>([]);
const [isLocModalOpen, setIsLocModalOpen] = useState(false);
const [locModalAgentUuid, setLocModalAgentUuid] = useState("");
const [locModalAgentLabel, setLocModalAgentLabel] = useState("");
const [locModalCurrentLoc, setLocModalCurrentLoc] = useState<any | null>(null);
useEffect(() => {
if (typeof window !== 'undefined') {
const stored = localStorage.getItem('backone_site_uuid');
if (stored) setSelectedSite(stored);
}
}, []);
const [viewAsLoading, setViewAsLoading] = useState<string | null>(null);
const [selectedAgent, setSelectedAgent] = useState<Agent | null>(null);
const { timeRange } = useTimeFilter();
const refreshKey = usePollingKey(60000); // Auto-refresh every 60 seconds (1 minute)
const loadUptime = async () => {
try {
const res = await fetch(`/api/dashboard/agents/uptime?timeRange=${timeRange}`, {
headers: getViewAsHeaders()
});
if (res.ok) {
const json = await res.json();
if (json.ok && json.uptime) setUptimeMap(json.uptime);
}
} catch (err) {
console.error("Failed to load uptime stats:", err);
}
};
const loadStorage = async () => {
try {
const res = await fetch('/api/dashboard/agents/storage', {
headers: getViewAsHeaders()
});
if (res.ok) {
const json = await res.json();
if (json.ok && json.storage) setStorageMap(json.storage);
}
} catch (err) {
console.error("Failed to load agent storage stats:", err);
}
};
const loadLocations = async () => {
try {
const res = await fetch("/api/dashboard/agent-locations", {
headers: getViewAsHeaders(),
});
if (res.ok) {
const json = await res.json();
if (json.ok && json.data) setLocations(json.data);
}
} catch (err) {
console.error("Failed to load locations:", err);
}
};
const loadAgents = async () => {
setIsLoading(true);
try {
const siteUuid = localStorage.getItem("backone_site_uuid") || undefined;
const [agentData, userData] = await Promise.allSettled([
getAgents(siteUuid),
getAdminUsers(),
]);
if (agentData.status === 'fulfilled') setAgents(agentData.value);
else setError((agentData.reason as Error).message);
if (userData.status === 'fulfilled') setManagedUsers(userData.value);
await loadUptime();
await loadLocations();
} catch (e: any) {
setError(e.message);
}
setIsLoading(false);
};
const handleViewAs = async (agentUuid: string, agentLabel: string) => {
setViewAsLoading(agentUuid);
try {
await startViewAs(agentUuid, agentLabel);
router.refresh();
window.location.reload();
} catch (err: any) {
alert('Failed to enter View As mode: ' + err.message);
} finally {
setViewAsLoading(null);
}
};
useEffect(() => {
fetch('/api/auth/me', { headers: getViewAsHeaders() })
.then(res => res.json())
.then(data => {
if (data.user) {
const userRole = data.user.role || null;
setRole(userRole);
if (userRole !== 'SUPER_ADMIN' && userRole !== 'TENANT_ADMIN') {
router.push('/');
} else {
loadAgents();
}
} else {
setIsLoading(false);
}
})
.catch(err => {
console.error("Auth check failed:", err);
setIsLoading(false);
});
}, [router]);
useEffect(() => {
if (role && role !== 'AGENT_VIEWER') loadUptime();
}, [timeRange]);
useEffect(() => {
if (role === 'SUPER_ADMIN' || role === 'TENANT_ADMIN') {
loadStorage();
}
if (role && role !== 'AGENT_VIEWER') {
loadLocations();
}
}, [role]);
useEffect(() => {
if (role && role !== 'AGENT_VIEWER' && refreshKey > 0) {
const siteUuid = localStorage.getItem("backone_site_uuid") || undefined;
getAgents(siteUuid)
.then(data => setAgents(data))
.catch(err => console.warn("Failed to auto-refresh agents:", err));
loadUptime();
loadLocations();
if (role === 'SUPER_ADMIN' || role === 'TENANT_ADMIN') {
loadStorage();
}
}
}, [refreshKey, role]);
const openDelete = (agent: Agent) => {
setSelectedAgent(agent);
setIsDeleteOpen(true);
};
const handleEditLocation = (agentUuid: string, agentLabel: string) => {
const loc = locations.find(l => l.agent_uuid === agentUuid) || null;
setLocModalAgentUuid(agentUuid);
setLocModalAgentLabel(agentLabel);
setLocModalCurrentLoc(loc);
setIsLocModalOpen(true);
};
return {
role,
agents,
uptimeMap,
storageMap,
isLoading,
error,
isCreateOpen,
setIsCreateOpen,
isDeleteOpen,
setIsDeleteOpen,
detailAgent,
setDetailAgent,
managedUsers,
accountModalOpen,
setAccountModalOpen,
accountModalAgent,
setAccountModalAgent,
externalModalOpen,
setExternalModalOpen,
externalModalUser,
setExternalModalUser,
targetSiteUuid,
setTargetSiteUuid,
targetCreatedBy,
setTargetCreatedBy,
locations,
isLocModalOpen,
setIsLocModalOpen,
locModalAgentUuid,
setLocModalAgentUuid,
locModalAgentLabel,
setLocModalAgentLabel,
locModalCurrentLoc,
setLocModalCurrentLoc,
viewAsLoading,
selectedAgent,
loadAgents,
loadLocations,
handleViewAs,
openDelete,
handleEditLocation,
};
}