v1.1: Add CIDR subnet filtering and Agent filter in Devices page
This commit is contained in:
commit
966058e2fe
422 files changed
+54656
No files matched your search
@@ -0,0 +1,124 @@
|
||||
"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/agentsCore";
|
||||
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}
|
||||
modalContext="agents"
|
||||
/>
|
||||
|
||||
<AgentLocationModal
|
||||
isOpen={isLocModalOpen}
|
||||
onClose={() => {
|
||||
setIsLocModalOpen(false);
|
||||
setLocModalAgentUuid("");
|
||||
setLocModalAgentLabel("");
|
||||
setLocModalCurrentLoc(null);
|
||||
}}
|
||||
onSuccess={() => {
|
||||
loadLocations();
|
||||
}}
|
||||
agentUuid={locModalAgentUuid}
|
||||
agentLabel={locModalAgentLabel}
|
||||
currentLocation={locModalCurrentLoc}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
"use client";
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
|
||||
import { DataTable, Column } from "@/components/ui/DataTable";
|
||||
import { Loader2, Plus } from "lucide-react";
|
||||
import { Agent } from "@/lib/actions/agentsCore";
|
||||
|
||||
interface AgentsTableSectionProps {
|
||||
agents: Agent[];
|
||||
isLoading: boolean;
|
||||
columns: Column<Agent>[];
|
||||
role: string | null;
|
||||
onProvisionClick?: () => void;
|
||||
}
|
||||
|
||||
export default function AgentsTableSection({
|
||||
agents,
|
||||
isLoading,
|
||||
columns,
|
||||
role,
|
||||
onProvisionClick,
|
||||
}: AgentsTableSectionProps) {
|
||||
return (
|
||||
<Card className="bg-card/50 backdrop-blur-sm border-border/50">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-4">
|
||||
<CardTitle className="text-white">Provisioned Network Agents ({agents.length})</CardTitle>
|
||||
{(role === 'SUPER_ADMIN' || role === 'TENANT_ADMIN') && onProvisionClick && (
|
||||
<button
|
||||
onClick={onProvisionClick}
|
||||
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>
|
||||
)}
|
||||
</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,242 @@
|
||||
"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 Technical 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 Technical 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: Office 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">Office Tenant External Accounts ({nexusUsers.length})</CardTitle>
|
||||
<button
|
||||
onClick={() => {
|
||||
setExternalModalUser(null);
|
||||
setTargetSiteUuid('1959bb55_045b_47c7_bbdd_f33b7db197b9');
|
||||
setTargetCreatedBy('office');
|
||||
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 Technical 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 Technical 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>
|
||||
);
|
||||
}
|
||||
|
||||
// EXECUTIVE: read-only view of all accounts, no create/edit buttons
|
||||
if (role === 'EXECUTIVE') {
|
||||
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 flex items-center gap-2">
|
||||
<span>External Accounts Directory ({externalUsers.length})</span>
|
||||
</CardTitle>
|
||||
<span className="text-xs text-slate-500 italic px-3 py-1.5 border border-slate-700/50 rounded-lg">Read-Only View</span>
|
||||
</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 text-[10px] font-medium">Total Agents</span>
|
||||
<span className="text-2xl font-bold 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 text-[10px] font-medium">Online Agents</span>
|
||||
<span className="text-2xl font-bold 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 text-[10px] font-medium">Offline Agents</span>
|
||||
<span className="text-2xl font-bold 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 text-[10px] font-medium">Avg. Historical Uptime</span>
|
||||
<span className="text-2xl font-bold text-cyan-400 tracking-tight">{avgUptime.toFixed(2)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
"use client";
|
||||
|
||||
import { Column } from "@/components/ui/DataTable";
|
||||
import { Agent } from "@/lib/actions/agentsCore";
|
||||
import { Badge } from "@/components/ui/Badge";
|
||||
import { resolveAgentLabel, type ManagedUser } from "@/lib/admin-api";
|
||||
import { Loader2, Trash2, CheckCircle2, XCircle, ChevronRight, UserCog, Eye, MapPin, Network } from "lucide-react";
|
||||
|
||||
interface GetColumnsProps {
|
||||
role: string | null;
|
||||
uptimeMap: Record<string, number>;
|
||||
storageMap?: Record<string, number>;
|
||||
managedUsers: ManagedUser[];
|
||||
viewAsLoading: string | null;
|
||||
setDetailAgent: (agent: Agent | null) => void;
|
||||
setAccountModalOpen: (open: boolean) => void;
|
||||
setAccountModalAgent: (agent: { uuid: string; label: string } | null) => void;
|
||||
handleViewAs: (uuid: string, label: string) => void;
|
||||
openDelete: (agent: Agent) => void;
|
||||
handleEditLocation?: (agentUuid: string, agentLabel: string) => void;
|
||||
handleEditSubnet?: (agentUuid: string, agentLabel: string) => void;
|
||||
/** Set of agent UUIDs that already have a location saved */
|
||||
locationUuids?: Set<string>;
|
||||
}
|
||||
|
||||
export function getAgentColumns({
|
||||
role,
|
||||
uptimeMap,
|
||||
storageMap = {},
|
||||
managedUsers,
|
||||
viewAsLoading,
|
||||
setDetailAgent,
|
||||
setAccountModalOpen,
|
||||
setAccountModalAgent,
|
||||
handleViewAs,
|
||||
openDelete,
|
||||
handleEditLocation,
|
||||
handleEditSubnet,
|
||||
locationUuids = new Set(),
|
||||
}: GetColumnsProps): Column<Agent>[] {
|
||||
const cols: Column<Agent>[] = [
|
||||
{
|
||||
header: "UUID / Serial",
|
||||
className: "w-[12%] 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>
|
||||
<ChevronRight className="w-3 h-3 text-slate-600 group-hover:text-blue-400 transition-colors" />
|
||||
</button>
|
||||
)
|
||||
},
|
||||
{
|
||||
header: "Label",
|
||||
className: "w-[15%] text-center",
|
||||
accessor: (row) => {
|
||||
const uuid = row.uuid || row.serial;
|
||||
const dynamicLabel = resolveAgentLabel(uuid, managedUsers);
|
||||
const hasAccount = managedUsers.some(u => u.agent_uuid === uuid && u.role === 'AGENT_VIEWER');
|
||||
const displayLabel = dynamicLabel !== uuid ? dynamicLabel : (row.label || uuid);
|
||||
return (
|
||||
<div className="flex items-center justify-center gap-2 mx-auto max-w-full">
|
||||
<button
|
||||
onClick={() => setDetailAgent(row)}
|
||||
className="text-center hover:text-white transition-colors truncate"
|
||||
title={displayLabel}
|
||||
>
|
||||
{dynamicLabel !== uuid ? dynamicLabel : (row.label || <span className="text-muted-foreground italic font-mono text-xs">{uuid}</span>)}
|
||||
</button>
|
||||
{!hasAccount && (role === 'SUPER_ADMIN' || role === 'TENANT_ADMIN') && (
|
||||
<Badge variant="orange" className="text-[10px] px-1.5 py-0.5 font-semibold shrink-0 whitespace-nowrap">No account yet</Badge>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
header: "Provisioned",
|
||||
className: "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-[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: "Traffic Status",
|
||||
className: "w-[15%] text-center",
|
||||
accessor: (row) => {
|
||||
const statusText = row.last_seen_at?.human || '-';
|
||||
if (statusText.startsWith('Last seen:')) {
|
||||
return (
|
||||
<span className="text-emerald-400 font-medium flex items-center justify-center gap-1.5 mx-auto">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse"></div>
|
||||
{statusText}
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
return <span className="text-slate-400 text-xs font-normal">{statusText}</span>;
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
header: "Avg Uptime",
|
||||
className: "w-[10%] text-center",
|
||||
accessor: (row) => {
|
||||
const uuid = row.uuid || row.serial;
|
||||
const uptime = uptimeMap[uuid] ?? 100;
|
||||
let color = "text-emerald-400";
|
||||
if (uptime < 90) color = "text-red-400";
|
||||
else if (uptime < 98) color = "text-amber-400";
|
||||
return <span className={`font-mono font-medium text-xs tracking-wide ${color}`}>{uptime.toFixed(1)}%</span>;
|
||||
}
|
||||
},
|
||||
{
|
||||
header: "Actions",
|
||||
className: "w-[16%] text-center",
|
||||
accessor: (row) => {
|
||||
const uuid = row.uuid || row.serial;
|
||||
const dynamicLabel = resolveAgentLabel(uuid, managedUsers);
|
||||
const existingUser = managedUsers.find(u => u.agent_uuid === uuid && u.role === 'AGENT_VIEWER') || null;
|
||||
const isViewAsLoading = viewAsLoading === uuid;
|
||||
const agentLabel = dynamicLabel !== uuid ? dynamicLabel : (row.label || uuid);
|
||||
return (
|
||||
<div className="flex items-center justify-center gap-1 mx-auto">
|
||||
<button onClick={() => setDetailAgent(row)} className="p-1 text-blue-400 hover:text-blue-300 hover:bg-blue-950/30 rounded transition-colors" title="Detail Agent">
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</button>
|
||||
{(role === 'SUPER_ADMIN' || role === 'TENANT_ADMIN') && (
|
||||
<>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setAccountModalAgent({ uuid, label: agentLabel }); setAccountModalOpen(true); }}
|
||||
className="p-1 text-emerald-400 hover:text-emerald-300 hover:bg-emerald-950/30 rounded transition-colors"
|
||||
title={existingUser ? 'Edit Agent Account' : 'Create Agent Account'}
|
||||
>
|
||||
<UserCog className="w-4 h-4" />
|
||||
</button>
|
||||
{handleEditLocation && (() => {
|
||||
const hasLocation = locationUuids.has(uuid);
|
||||
return (
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleEditLocation(uuid, agentLabel); }}
|
||||
className={`relative p-1 rounded transition-colors ${
|
||||
hasLocation
|
||||
? "text-teal-400 hover:text-teal-300 hover:bg-teal-950/30"
|
||||
: "text-slate-500 hover:text-indigo-400 hover:bg-indigo-950/30"
|
||||
}`}
|
||||
title={hasLocation ? "✅ Location set — click to update" : "⚠️ No location — click to set coordinates"}
|
||||
>
|
||||
<MapPin className="w-4 h-4" />
|
||||
{hasLocation && (
|
||||
<span className="absolute -top-0.5 -right-0.5 w-2 h-2 rounded-full bg-teal-400 border border-slate-900" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})()}
|
||||
{handleEditSubnet && (
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleEditSubnet(uuid, agentLabel); }}
|
||||
className="p-1 text-violet-400 hover:text-violet-300 hover:bg-violet-950/30 rounded transition-colors"
|
||||
title="Konfigurasi subnet IP agen ini"
|
||||
>
|
||||
<Network className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleViewAs(uuid, agentLabel); }}
|
||||
disabled={isViewAsLoading}
|
||||
className="p-1 text-amber-400 hover:text-amber-300 hover:bg-amber-950/30 rounded transition-colors disabled:opacity-50"
|
||||
title="View As Agent"
|
||||
>
|
||||
{isViewAsLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Eye className="w-4 h-4" />}
|
||||
</button>
|
||||
<button onClick={(e) => { e.stopPropagation(); openDelete(row); }} className="p-1 text-red-400 hover:text-red-300 hover:bg-red-950/30 rounded transition-colors" title="Delete">
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{/* COMPANY_ADMIN dan COMPANY_OPERATOR: hanya View-As */}
|
||||
{(role === 'COMPANY_ADMIN' || role === 'COMPANY_OPERATOR') && (
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleViewAs(uuid, agentLabel); }}
|
||||
disabled={isViewAsLoading}
|
||||
className="p-1 text-amber-400 hover:text-amber-300 hover:bg-amber-950/30 rounded transition-colors disabled:opacity-50"
|
||||
title="View As Agent — see dashboard from this agent's perspective"
|
||||
>
|
||||
{isViewAsLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Eye className="w-4 h-4" />}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
if (role === 'SUPER_ADMIN') {
|
||||
cols.splice(cols.length - 1, 0, {
|
||||
header: "DB Storage",
|
||||
className: "w-[10%] text-center",
|
||||
accessor: (row) => {
|
||||
const uuid = row.uuid || row.serial;
|
||||
let sizeMB = storageMap[uuid] ?? 0;
|
||||
|
||||
let displaySize = "";
|
||||
if (sizeMB > 1024 * 1024) {
|
||||
displaySize = `${(sizeMB / (1024 * 1024)).toFixed(2)} TB`;
|
||||
} else if (sizeMB > 1024) {
|
||||
displaySize = `${(sizeMB / 1024).toFixed(2)} GB`;
|
||||
} else {
|
||||
displaySize = `${sizeMB.toFixed(2)} MB`;
|
||||
}
|
||||
return <span className="text-slate-400 font-mono text-xs">{displaySize}</span>;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return cols;
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
"use client";
|
||||
|
||||
import { Column } from "@/components/ui/DataTable";
|
||||
import { Badge } from "@/components/ui/Badge";
|
||||
import { SafeImage } from "@/components/ui/SafeImage";
|
||||
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 ? (
|
||||
<SafeImage
|
||||
src={`/api/uploads/${row.profile_picture}`}
|
||||
alt={row.account_name || row.username}
|
||||
fallbackName={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 === 'EXECUTIVE') badgeColor = "bg-yellow-500/20 text-yellow-400 border-yellow-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>
|
||||
)
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Plus, Loader2, MapPin } from "lucide-react";
|
||||
import { HelpTrigger } from "@/components/help/HelpTrigger";
|
||||
import { useAgentsData } from "./useAgentsData";
|
||||
import { getAgentColumns } from "./columns";
|
||||
import { getExternalAccountColumns } from "./externalColumns";
|
||||
import UptimeStatsCards from "./UptimeStatsCards";
|
||||
import AgentsTableSection from "./AgentsTableSection";
|
||||
import ExternalAccountsSection from "./ExternalAccountsSection";
|
||||
import AgentsPageModals from "./AgentsPageModals";
|
||||
import AgentLocationMap from "@/components/admin/AgentLocationMap";
|
||||
import { SubnetConfigModal } from "@/components/admin/SubnetConfigModal";
|
||||
import { resolveAgentLabel } from "@/lib/admin-api";
|
||||
|
||||
export default function AgentsPage() {
|
||||
useEffect(() => {
|
||||
document.title = "Agents | BackOne - Deep Package Inspection";
|
||||
}, []);
|
||||
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 [subnetModalOpen, setSubnetModalOpen] = useState(false);
|
||||
const [subnetModalAgent, setSubnetModalAgent] = useState<{ uuid: string; label: string } | null>(null);
|
||||
|
||||
const handleEditSubnet = (agentUuid: string, agentLabel: string) => {
|
||||
setSubnetModalAgent({ uuid: agentUuid, label: agentLabel });
|
||||
setSubnetModalOpen(true);
|
||||
};
|
||||
|
||||
const columns = getAgentColumns({
|
||||
role,
|
||||
uptimeMap,
|
||||
storageMap,
|
||||
managedUsers,
|
||||
viewAsLoading,
|
||||
setDetailAgent,
|
||||
setAccountModalOpen,
|
||||
setAccountModalAgent,
|
||||
handleViewAs,
|
||||
openDelete,
|
||||
handleEditLocation,
|
||||
handleEditSubnet,
|
||||
locationUuids: new Set(locations.map((l: any) => l.agent_uuid)),
|
||||
});
|
||||
|
||||
const externalColumns = getExternalAccountColumns((user) => {
|
||||
setExternalModalUser(user);
|
||||
setExternalModalOpen(true);
|
||||
});
|
||||
|
||||
const totalAgents = agents.length;
|
||||
const onlineAgents = agents.filter(a => {
|
||||
const hum = a.last_seen_at?.human;
|
||||
return hum === 'Online' || hum === 'Flows Detected' || (hum && hum.startsWith('Last seen:'));
|
||||
}).length;
|
||||
const offlineAgents = totalAgents - onlineAgents;
|
||||
const uptimeValues = agents.map(a => uptimeMap[a.uuid || a.serial] ?? 100);
|
||||
const avgUptime = uptimeValues.length > 0 ? uptimeValues.reduce((s, v) => s + v, 0) / uptimeValues.length : 100;
|
||||
|
||||
// External Accounts di halaman Agents = akun eksternal teknikal/operasional saja
|
||||
// AGENT_VIEWER = akun network agent (sudah ada di tabel agents) → BUKAN akun eksternal
|
||||
// COMPANY_* = akun customer/client → sudah ada di halaman User Accounts
|
||||
const EXCLUDED_ROLES = ['COMPANY_ADMIN', 'COMPANY_OPERATOR', 'COMPANY_VIEWER', 'AGENT_VIEWER'];
|
||||
const externalUsers = managedUsers.filter(u => !EXCLUDED_ROLES.includes(u.role || ''));
|
||||
// Grouping by site — semua sudah dipastikan hanya SUPER_ADMIN, EXECUTIVE, TENANT_ADMIN, SOC_ANALYST, ENGINEER
|
||||
const superadminUsers = externalUsers.filter(u => u.role === 'SUPER_ADMIN' || u.role === 'EXECUTIVE' || !u.site_uuid || u.site_uuid === 'default');
|
||||
const siabUsers = externalUsers.filter(u => u.site_uuid === '6681452d_9cae_4ff4_8ae8_0d504774265e' && u.role !== 'SUPER_ADMIN' && u.role !== 'EXECUTIVE');
|
||||
const nexusUsers = externalUsers.filter(u => u.site_uuid === '1959bb55_045b_47c7_bbdd_f33b7db197b9' && u.role !== 'SUPER_ADMIN' && u.role !== 'EXECUTIVE');
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center justify-between gap-3 sm:justify-start sm:gap-4 w-full">
|
||||
<h2 className="text-xl sm:text-2xl font-bold tracking-tight text-white flex items-center gap-2 sm:gap-3 truncate">
|
||||
<span>Agents Inventory</span>
|
||||
{isLoading && <Loader2 className="w-4 h-4 sm:w-5 sm:h-5 animate-spin text-primary flex-shrink-0" />}
|
||||
</h2>
|
||||
{/* Mobile Actions: Inline next to Title */}
|
||||
<div className="flex items-center gap-1.5 flex-shrink-0 sm:hidden">
|
||||
<HelpTrigger pageId="agents" />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs sm:text-sm text-muted-foreground mt-1 leading-normal sm:leading-relaxed">
|
||||
Managed DPI probes, remote collectors, and probe infrastructure.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Laptop/Tablet Actions: Floats on the right */}
|
||||
<div className="hidden sm:flex items-center gap-2 sm:gap-3 flex-shrink-0">
|
||||
<HelpTrigger pageId="agents" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-500/15 border border-red-500/30 text-red-400 px-4 py-3 rounded-xl text-sm font-medium">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<UptimeStatsCards
|
||||
totalAgents={totalAgents}
|
||||
onlineAgents={onlineAgents}
|
||||
offlineAgents={offlineAgents}
|
||||
avgUptime={avgUptime}
|
||||
/>
|
||||
|
||||
{/* Agent Location Map — tampil untuk SUPER_ADMIN, TENANT_ADMIN, dan COMPANY_ADMIN */}
|
||||
{(role === 'SUPER_ADMIN' || role === 'TENANT_ADMIN' || role === 'COMPANY_ADMIN') && (
|
||||
<div className="rounded-2xl border border-white/8 bg-slate-900/40 overflow-hidden relative z-0">
|
||||
<div className="flex items-center justify-between px-5 py-3.5 border-b border-white/8">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<MapPin className="w-4 h-4 text-indigo-400" />
|
||||
<span className="text-sm font-semibold text-white">Agent Network Map</span>
|
||||
<span className="text-xs text-slate-500">
|
||||
{locations.length > 0
|
||||
? `${locations.length} of ${agents.length} agent${agents.length !== 1 ? 's' : ''} positioned`
|
||||
: 'No positions configured yet'}
|
||||
</span>
|
||||
</div>
|
||||
{agents.length > locations.length && (
|
||||
<span className="text-[11px] text-amber-400 bg-amber-400/10 border border-amber-400/20 px-2.5 py-1 rounded-full">
|
||||
{agents.length - locations.length} agent{agents.length - locations.length !== 1 ? 's' : ''} without location — click 📍 in the table
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="p-3">
|
||||
<AgentLocationMap
|
||||
pins={locations
|
||||
.filter(loc => agents.some(a => (a.uuid || a.serial) === loc.agent_uuid))
|
||||
.map(loc => {
|
||||
const agent = agents.find(a => (a.uuid || a.serial) === loc.agent_uuid);
|
||||
const agentLabel = agent
|
||||
? (resolveAgentLabel(loc.agent_uuid, managedUsers) !== loc.agent_uuid
|
||||
? resolveAgentLabel(loc.agent_uuid, managedUsers)
|
||||
: (agent.label || loc.label || loc.agent_uuid))
|
||||
: (loc.label || loc.agent_uuid);
|
||||
const isOnline = !!(agent?.last_seen_at?.human === 'Online' ||
|
||||
agent?.last_seen_at?.human === 'Flows Detected' ||
|
||||
(agent?.last_seen_at?.human && agent.last_seen_at.human.startsWith('Last seen:')));
|
||||
const uptime = uptimeMap[loc.agent_uuid];
|
||||
return {
|
||||
agent_uuid: loc.agent_uuid,
|
||||
label: agentLabel,
|
||||
latitude: loc.latitude,
|
||||
longitude: loc.longitude,
|
||||
isOnline,
|
||||
uptimePercent: uptime,
|
||||
};
|
||||
})}
|
||||
onEditLocation={handleEditLocation}
|
||||
canEdit={role === 'SUPER_ADMIN' || role === 'TENANT_ADMIN'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<AgentsTableSection
|
||||
agents={agents}
|
||||
isLoading={isLoading}
|
||||
columns={columns}
|
||||
role={role}
|
||||
onProvisionClick={() => setIsCreateOpen(true)}
|
||||
/>
|
||||
|
||||
{(role === 'SUPER_ADMIN' || role === 'TENANT_ADMIN') && (
|
||||
<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}
|
||||
loadAgents={loadAgents}
|
||||
detailAgent={detailAgent}
|
||||
setDetailAgent={setDetailAgent}
|
||||
accountModalOpen={accountModalOpen}
|
||||
setAccountModalOpen={setAccountModalOpen}
|
||||
accountModalAgent={accountModalAgent}
|
||||
setAccountModalAgent={setAccountModalAgent}
|
||||
managedUsers={managedUsers}
|
||||
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}
|
||||
/>
|
||||
<SubnetConfigModal
|
||||
agentUuid={subnetModalAgent?.uuid || ""}
|
||||
agentLabel={subnetModalAgent?.label || ""}
|
||||
open={subnetModalOpen}
|
||||
onClose={() => setSubnetModalOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { getAgents } from "@/lib/actions/agents";
|
||||
import { Agent } from "@/lib/actions/agentsCore";
|
||||
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 [myAgentUuids, setMyAgentUuids] = useState<string[]>([]);
|
||||
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 (allowedUuids?: string[], currentRole?: string) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const siteUuid = localStorage.getItem("backone_site_uuid") || undefined;
|
||||
const [agentData, userData] = await Promise.allSettled([
|
||||
getAgents(siteUuid),
|
||||
getAdminUsers(),
|
||||
]);
|
||||
if (agentData.status === 'fulfilled') {
|
||||
let allAgents = agentData.value;
|
||||
// Filter agents untuk COMPANY_ADMIN dan COMPANY_OPERATOR — hanya tampilkan yang di-assign
|
||||
const uuids = allowedUuids ?? myAgentUuids;
|
||||
const r = currentRole ?? role;
|
||||
if ((r === 'COMPANY_ADMIN' || r === 'COMPANY_OPERATOR') && uuids.length > 0) {
|
||||
allAgents = allAgents.filter(a => uuids.includes(a.uuid || a.serial));
|
||||
}
|
||||
|
||||
setAgents(allAgents);
|
||||
} 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;
|
||||
const uuids: string[] = data.user.agent_uuids || [];
|
||||
setRole(userRole);
|
||||
setMyAgentUuids(uuids);
|
||||
const allowed = ['SUPER_ADMIN', 'TENANT_ADMIN', 'EXECUTIVE', 'COMPANY_ADMIN', 'COMPANY_OPERATOR'];
|
||||
if (!allowed.includes(userRole || '')) {
|
||||
router.push('/');
|
||||
} else {
|
||||
loadAgents(uuids, userRole);
|
||||
}
|
||||
} 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();
|
||||
}
|
||||
if (role && role !== 'AGENT_VIEWER') {
|
||||
loadLocations();
|
||||
}
|
||||
}, [role]);
|
||||
|
||||
useEffect(() => {
|
||||
if (role && role !== 'AGENT_VIEWER' && refreshKey > 0) {
|
||||
loadAgents();
|
||||
if (role === 'SUPER_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,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user