chore: deep cleanup of test scripts and implement glassmorphism UI enhancements

This commit is contained in:
Rafif-Riqullah-Siregar committed 2026-07-07 00:35:54 +07:00
1 parent 7777b62305
commit 58040d6e6c
81 files changed
+2247 -6734

No files matched your search

+114 -93
View File
@@ -2,13 +2,17 @@
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { getAgents, createAgent, updateAgent, deleteAgent, Agent } from "@/lib/actions/agents";
import { getAgents, createAgent, deleteAgent, Agent } from "@/lib/actions/agents";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
import { DataTable, Column } from "@/components/ui/DataTable";
import { Modal } from "@/components/ui/Modal";
import { Badge } from "@/components/ui/Badge";
import { AgentDetailModal } from "@/components/ui/AgentDetailModal";
import { Loader2, Plus, Edit, Trash2, Server, CheckCircle2, XCircle, ChevronRight } from "lucide-react";
import AgentAccountModal from "@/components/admin/AgentAccountModal";
import { Loader2, Plus, Trash2, Server, CheckCircle2, XCircle, ChevronRight, UserCog, Eye } from "lucide-react";
import { getAdminUsers, startViewAs, resolveAgentLabel, type ManagedUser } from "@/lib/admin-api";
export default function AgentsPage() {
const router = useRouter();
@@ -19,19 +23,20 @@ export default function AgentsPage() {
// Modals state
const [isCreateOpen, setIsCreateOpen] = useState(false);
const [isEditOpen, setIsEditOpen] = useState(false);
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
// Detail modal
const [detailAgent, setDetailAgent] = useState<Agent | null>(null);
// Known agent UUIDs with labels
const AGENT_LABELS: Record<string, string> = {
'2F-TF-1D-GK': 'JRP Cibubur',
'8A-V3-PB-85': 'IFG LT.18',
'F6-2V-DT-8A': 'CPI Balaraja',
'1R-79-J9-YE': 'CPI Balaraja WAN',
};
// Admin: managed users (for label resolution + account management)
const [managedUsers, setManagedUsers] = useState<ManagedUser[]>([]);
// Admin: AgentAccountModal state
const [accountModalOpen, setAccountModalOpen] = useState(false);
const [accountModalAgent, setAccountModalAgent] = useState<{ uuid: string; label: string } | null>(null);
// View-as loading state
const [viewAsLoading, setViewAsLoading] = useState<string | null>(null);
// Form state
const [selectedAgent, setSelectedAgent] = useState<Agent | null>(null);
@@ -41,14 +46,32 @@ export default function AgentsPage() {
const loadAgents = async () => {
setIsLoading(true);
try {
const data = await getAgents();
setAgents(data);
const [agentData, userData] = await Promise.allSettled([
getAgents(),
getAdminUsers(),
]);
if (agentData.status === 'fulfilled') setAgents(agentData.value);
else setError((agentData.reason as Error).message);
if (userData.status === 'fulfilled') setManagedUsers(userData.value);
} 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('Gagal masuk mode View As: ' + err.message);
} finally {
setViewAsLoading(null);
}
};
useEffect(() => {
fetch('/api/auth/me')
.then(res => res.json())
@@ -75,12 +98,6 @@ export default function AgentsPage() {
setIsCreateOpen(true);
};
const openEdit = (agent: Agent) => {
setSelectedAgent(agent);
setFormData({ agentId: agent.uuid || agent.serial, label: agent.label || "" });
setIsEditOpen(true);
};
const openDelete = (agent: Agent) => {
setSelectedAgent(agent);
setIsDeleteOpen(true);
@@ -91,7 +108,6 @@ export default function AgentsPage() {
setIsSubmitting(true);
const res = await createAgent(formData.agentId, formData.label);
setIsSubmitting(false);
if (res.success) {
setIsCreateOpen(false);
loadAgents();
@@ -100,29 +116,11 @@ export default function AgentsPage() {
}
};
const handleUpdate = async (e: React.FormEvent) => {
e.preventDefault();
if (!selectedAgent) return;
setIsSubmitting(true);
const res = await updateAgent(selectedAgent.uuid || selectedAgent.serial, formData.label);
setIsSubmitting(false);
if (res.success) {
setIsEditOpen(false);
loadAgents();
} else {
alert(res.message);
}
};
const handleDelete = async () => {
if (!selectedAgent) return;
setIsSubmitting(true);
const res = await deleteAgent(selectedAgent.uuid || selectedAgent.serial);
setIsSubmitting(false);
if (res.success) {
setIsDeleteOpen(false);
loadAgents();
@@ -148,36 +146,78 @@ export default function AgentsPage() {
{
header: "Label",
accessor: (row) => {
const friendlyLabel = AGENT_LABELS[row.uuid || row.serial];
// Ambil label dari account_name user yang terhubung ke agent ini
const uuid = row.uuid || row.serial;
const dynamicLabel = resolveAgentLabel(uuid, managedUsers);
const hasAccount = managedUsers.some(u => u.agent_uuid === uuid && u.role === 'AGENT_VIEWER');
return (
<button onClick={() => setDetailAgent(row)} className="text-left hover:text-white transition-colors">
{friendlyLabel || row.label || <span className="text-muted-foreground italic">No label</span>}
</button>
<div className="flex items-center gap-2">
<button onClick={() => setDetailAgent(row)} className="text-left hover:text-white transition-colors">
{dynamicLabel !== uuid ? dynamicLabel : (row.label || <span className="text-muted-foreground italic font-mono text-xs">{uuid}</span>)}
</button>
{!hasAccount && role === 'SUPER_ADMIN' && (
<span className="text-[10px] px-1.5 py-0.5 rounded bg-amber-900/40 border border-amber-700/50 text-amber-400">Belum ada akun</span>
)}
</div>
);
}
},
{ header: "Provisioned", 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", 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: "Last Seen", accessor: (row) => <span className="text-muted-foreground">{row.last_seen_at?.human || '-'}</span> },
{
header: "Status",
accessor: (row) => {
const statusText = row.last_seen_at?.human || '-';
if (statusText === 'Online') {
return <span className="text-emerald-400 font-medium flex items-center gap-1.5"><div className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse"></div> Online</span>;
} else if (statusText === 'Offline') {
return <span className="text-red-400 font-medium">Offline</span>;
} else {
return <span className="text-slate-400 text-xs">{statusText}</span>; // Last Seen ...
}
}
},
{
header: "Actions",
accessor: (row) => (
<div className="flex items-center gap-2">
<button
onClick={() => setDetailAgent(row)}
className="p-1.5 text-blue-400 hover:text-blue-300 hover:bg-blue-950/30 rounded transition-colors"
title="View details"
>
<ChevronRight className="w-4 h-4" />
</button>
<button onClick={(e) => { e.stopPropagation(); openEdit(row); }} className="p-1.5 text-slate-400 hover:text-white hover:bg-slate-800 rounded transition-colors">
<Edit className="w-4 h-4" />
</button>
<button onClick={(e) => { e.stopPropagation(); openDelete(row); }} className="p-1.5 text-red-400 hover:text-red-300 hover:bg-red-950/30 rounded transition-colors">
<Trash2 className="w-4 h-4" />
</button>
</div>
)
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;
return (
<div className="flex items-center gap-1.5">
<button
onClick={() => setDetailAgent(row)}
className="p-1.5 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' && (
<>
<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"
title={existingUser ? 'Edit Akun Agent' : 'Buat Akun Agent'}
>
<UserCog className="w-4 h-4" />
</button>
<button
onClick={(e) => { e.stopPropagation(); handleViewAs(uuid, dynamicLabel !== uuid ? dynamicLabel : (row.label || uuid)); }}
disabled={isViewAsLoading}
className="p-1.5 text-amber-400 hover:text-amber-300 hover:bg-amber-950/30 rounded transition-colors disabled:opacity-50"
title="Lihat Sebagai Agent Ini"
>
{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.5 text-red-400 hover:text-red-300 hover:bg-red-950/30 rounded transition-colors" title="Hapus">
<Trash2 className="w-4 h-4" />
</button>
</>
)}
</div>
);
}
},
];
@@ -272,39 +312,6 @@ export default function AgentsPage() {
</form>
</Modal>
{/* Edit Modal */}
<Modal isOpen={isEditOpen} onClose={() => setIsEditOpen(false)} title="Edit Agent">
<form onSubmit={handleUpdate} className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-medium text-slate-400">Agent UUID / Serial</label>
<input
disabled
type="text"
value={selectedAgent?.uuid || selectedAgent?.serial || ""}
className="w-full bg-slate-900/50 border border-slate-800 rounded-md px-3 py-2 text-sm text-slate-500 cursor-not-allowed"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-slate-300">Agent Label</label>
<input
autoFocus
type="text"
placeholder="e.g. Branch Office Gateway"
value={formData.label}
onChange={e => setFormData({...formData, label: e.target.value})}
className="w-full bg-slate-900 border border-slate-700 rounded-md px-3 py-2 text-sm text-white focus:outline-none focus:ring-2 focus:ring-primary/50"
/>
</div>
<div className="pt-4 flex justify-end gap-3">
<button type="button" onClick={() => setIsEditOpen(false)} className="px-4 py-2 text-sm font-medium text-slate-300 hover:text-white transition-colors">Cancel</button>
<button type="submit" disabled={isSubmitting} className="flex items-center gap-2 bg-primary hover:bg-primary/90 text-primary-foreground px-4 py-2 rounded-md text-sm font-medium transition-colors disabled:opacity-50">
{isSubmitting ? <Loader2 className="w-4 h-4 animate-spin" /> : null}
Save Changes
</button>
</div>
</form>
</Modal>
{/* Delete Modal */}
<Modal isOpen={isDeleteOpen} onClose={() => setIsDeleteOpen(false)} title="Confirm Deletion">
<div className="space-y-4">
@@ -325,11 +332,25 @@ export default function AgentsPage() {
{detailAgent && (
<AgentDetailModal
agentUuid={detailAgent.uuid || detailAgent.serial}
agentLabel={AGENT_LABELS[detailAgent.uuid || detailAgent.serial] || detailAgent.label || detailAgent.uuid || detailAgent.serial}
agentLabel={resolveAgentLabel(detailAgent.uuid || detailAgent.serial, managedUsers) !== (detailAgent.uuid || detailAgent.serial)
? resolveAgentLabel(detailAgent.uuid || detailAgent.serial, managedUsers)
: (detailAgent.label || detailAgent.uuid || detailAgent.serial)}
onClose={() => setDetailAgent(null)}
/>
)}
{/* Agent Account Modal */}
{accountModalOpen && accountModalAgent && (
<AgentAccountModal
isOpen={accountModalOpen}
onClose={() => { setAccountModalOpen(false); setAccountModalAgent(null); }}
onSuccess={() => { loadAgents(); }}
agentUuid={accountModalAgent.uuid}
agentLabel={accountModalAgent.label}
existingUser={managedUsers.find(u => u.agent_uuid === accountModalAgent.uuid && u.role === 'AGENT_VIEWER') || null}
/>
)}
</div>
);
}