feat(prod): deploy web dashboard, migrate mongodb configuration, resolve server components render error and fix logo static asset paths
This commit is contained in:
1 parent
4882108068
commit
b2ea883601
119 files changed
+7123
-2011
No files matched your search
+139
-257
@@ -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>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user