chore: deep cleanup of test scripts and implement glassmorphism UI enhancements
This commit is contained in:
1 parent
7777b62305
commit
58040d6e6c
81 files changed
+2247
-6734
No files matched your search
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user