refactor(web): split AdminDashboardView into admin-dashboard-view module
- types: DashboardStats, UploadItem, AuditLogItem, StatCardProps, Report - useAdminDashboardView: fetchData, handleAction, triggerProtocol - Header, StatCard, TrafficCard, ProtocolsCard, NodeHealthCard, Tabs - AdminDashboardSkeleton for Loading state - max-w-layout-content, text-xs, gap-0.5 (no arbitrary values) - Stories: Default, Loading (Skeleton). Decorator min-h-layout-page - Re-export from AdminDashboardView.tsx Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
27db3ef8ed
commit
fb3e5ceb5b
13 changed files with 797 additions and 322 deletions
|
|
@ -1,5 +1,6 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { AdminDashboardView } from './AdminDashboardView';
|
||||
import { AdminDashboardSkeleton } from './admin-dashboard-view';
|
||||
import { ToastProvider } from '../../components/feedback/ToastProvider';
|
||||
|
||||
/**
|
||||
|
|
@ -9,48 +10,44 @@ import { ToastProvider } from '../../components/feedback/ToastProvider';
|
|||
* visualisation du trafic, queue de modération et logs système.
|
||||
*/
|
||||
const meta: Meta<typeof AdminDashboardView> = {
|
||||
title: 'Components/Features/Admin/AdminDashboardView',
|
||||
component: AdminDashboardView,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Dashboard admin avec métriques, graphiques de trafic et contrôles système.',
|
||||
},
|
||||
},
|
||||
title: 'Components/Features/Admin/AdminDashboardView',
|
||||
component: AdminDashboardView,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
'Dashboard admin avec métriques, graphiques de trafic et contrôles système.',
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<ToastProvider>
|
||||
<div className="bg-kodo-background min-h-screen p-4">
|
||||
<Story />
|
||||
</div>
|
||||
</ToastProvider>
|
||||
),
|
||||
],
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<ToastProvider>
|
||||
<div className="bg-kodo-background min-h-layout-page p-4">
|
||||
<Story />
|
||||
</div>
|
||||
</ToastProvider>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
/**
|
||||
* État par défaut avec données chargées.
|
||||
*/
|
||||
export const Default: Story = {
|
||||
name: 'Par défaut',
|
||||
name: 'Par défaut',
|
||||
};
|
||||
|
||||
/**
|
||||
* État de chargement initial.
|
||||
*/
|
||||
export const Loading: Story = {
|
||||
name: 'Chargement',
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Affiche le spinner pendant le chargement des données admin.',
|
||||
},
|
||||
},
|
||||
name: 'Chargement',
|
||||
render: () => <AdminDashboardSkeleton />,
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Skeleton pendant le chargement des données admin.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,289 +1 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Card } from '../ui/card';
|
||||
import { Button } from '../ui/button';
|
||||
import {
|
||||
Users, DollarSign, Activity, AlertTriangle, HardDrive, ShoppingBag, ShieldAlert, Loader2, Server, Database, Lock, RefreshCw, Eye, ShieldCheck, History
|
||||
} from 'lucide-react';
|
||||
import { adminService } from '../../services/adminService';
|
||||
import { Report } from '../../types';
|
||||
import { useToast } from '../../components/feedback/ToastProvider';
|
||||
import { logger } from '@/utils/logger';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
|
||||
|
||||
const NebulaStatCard = ({ label, value, icon, color, trend }: any) => (
|
||||
<Card variant="glass" className="p-5 relative overflow-hidden group hover:border-primary/50 transition-all cursor-default">
|
||||
<div className={cn("absolute -right-4 -top-4 w-24 h-24 rounded-full opacity-10 blur-2xl transition-all group-hover:opacity-20", `bg-${color}-500`)} />
|
||||
<div className="flex justify-between items-start mb-3 relative z-10">
|
||||
<div className={cn("p-2.5 rounded-xl bg-white/5 shadow-inner", `text-${color}-500`)}>{icon}</div>
|
||||
{trend !== undefined && (
|
||||
<span className={cn("text-[10px] font-mono font-bold px-2 py-0.5 rounded-full border tracking-tighter", trend > 0 ? "text-lime-500 border-lime-500/20 bg-lime-500/10" : "text-red-500 border-red-500/20 bg-red-500/10")}>
|
||||
{trend > 0 ? '+' : ''}{trend}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-3xl font-display font-bold text-white mb-1 relative z-10">{value}</div>
|
||||
<div className="text-[10px] text-muted-foreground uppercase tracking-[0.2em] font-mono relative z-10">{label}</div>
|
||||
</Card>
|
||||
);
|
||||
|
||||
export const AdminDashboardView: React.FC = () => {
|
||||
const { addToast } = useToast();
|
||||
const [stats, setStats] = useState<any>({});
|
||||
const [reports, setReports] = useState<Report[]>([]);
|
||||
const [uploads, setUploads] = useState<any[]>([]);
|
||||
const [auditLogs, setAuditLogs] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [protocolActive, setProtocolActive] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [statsData, reportsData, uploadsData, logsData] = await Promise.all([
|
||||
adminService.getDashboardStats(),
|
||||
adminService.getModerationQueue('pending'),
|
||||
adminService.getRecentUploads(),
|
||||
adminService.getAuditLogs({ limit: 10 }),
|
||||
]);
|
||||
setStats(statsData);
|
||||
setReports(reportsData);
|
||||
setUploads(uploadsData);
|
||||
setAuditLogs(logsData.logs || []);
|
||||
} catch (e) {
|
||||
logger.error('Error loading admin dashboard', { error: e });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const handleAction = async (id: string, action: string) => {
|
||||
await adminService.resolveReport(id, action);
|
||||
setReports(reports.filter((r) => r.id !== id));
|
||||
addToast(`Protocol "${action.toUpperCase()}" executed successfully.`, 'success');
|
||||
};
|
||||
|
||||
const triggerProtocol = (name: string, color: string) => {
|
||||
setProtocolActive(name);
|
||||
addToast(`INITIALIZING ${name.toUpperCase()}...`, 'info');
|
||||
setTimeout(() => {
|
||||
addToast(`${name.toUpperCase()} DEPLOYED`, color as any);
|
||||
setProtocolActive(null);
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
if (loading) return <div className="flex justify-center py-24"><Loader2 className="w-10 h-10 text-primary animate-spin" /></div>;
|
||||
|
||||
return (
|
||||
<div className="space-y-8 pb-24 animate-fadeIn container mx-auto px-4 py-8 max-w-[1600px]">
|
||||
<div className="flex flex-col md:flex-row items-start md:items-end justify-between gap-6">
|
||||
<div>
|
||||
<h2 className="text-4xl font-display font-bold text-white mb-2 flex items-center gap-3">
|
||||
<ShieldCheck className="text-primary w-10 h-10" /> COMMAND CENTER
|
||||
</h2>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-lime-500 animate-pulse shadow-glow-lime" />
|
||||
<span className="text-muted-foreground text-[10px] font-mono tracking-widest uppercase">Nodes Online</span>
|
||||
</div>
|
||||
<div className="h-3 w-px bg-white/10" />
|
||||
<span className="text-muted-foreground text-[10px] font-mono tracking-widest uppercase">Sector: 00-ALPHA</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<Button variant="outline" className="border-white/10 hover:bg-white/5 font-mono text-[10px] h-10 tracking-widest" onClick={() => triggerProtocol('RESCAN', 'success')}>
|
||||
<RefreshCw className={cn("w-3 h-3 mr-2", protocolActive === 'RESCAN' && "animate-spin")} /> FULL RESCAN
|
||||
</Button>
|
||||
<Button variant="outline" className="border-red-500/20 text-red-500 hover:bg-red-500/10 font-mono text-[10px] h-10 tracking-widest" onClick={() => triggerProtocol('LOCKDOWN', 'error')}>
|
||||
<Lock className="w-3 h-3 mr-2" /> LOCKDOWN
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Primary Metrics */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<NebulaStatCard label="Total Nodes" value={stats.totalUsers?.toLocaleString()} icon={<Users className="w-5 h-5" />} trend={stats.trends?.users} color="cyan" />
|
||||
<NebulaStatCard label="Credit Volume" value={`$${stats.monthlyRevenue?.toLocaleString()}`} icon={<DollarSign className="w-5 h-5" />} trend={stats.trends?.revenue} color="gold" />
|
||||
<NebulaStatCard label="Active Uplinks" value={stats.activeSessions?.toLocaleString()} icon={<Activity className="w-5 h-5" />} trend={stats.trends?.sessions} color="lime" />
|
||||
<NebulaStatCard label="Threat Reports" value={stats.pendingReports} icon={<ShieldAlert className="w-5 h-5" />} trend={stats.trends?.reports} color="red" />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Real-time Traffic Visualizer */}
|
||||
<Card variant="glass" className="lg:col-span-2 p-8 bg-black/40 border-white/5 relative overflow-hidden">
|
||||
<div className="flex justify-between items-center mb-10 relative z-10">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-white flex items-center gap-3">
|
||||
<Server className="w-5 h-5 text-primary" /> Traffic Flux
|
||||
</h3>
|
||||
<p className="text-[10px] text-muted-foreground font-mono mt-1">HOLOGRAPHIC STREAMING INTERFACE</p>
|
||||
</div>
|
||||
<div className="flex gap-8 text-[10px] font-mono uppercase tracking-[0.2em] italic">
|
||||
<span className="flex items-center gap-2"><div className="w-2 h-2 rounded-full bg-cyan-500 shadow-glow-cyan" /> Uplink</span>
|
||||
<span className="flex items-center gap-2"><div className="w-2 h-2 rounded-full bg-magenta-500 shadow-glow-magenta" /> Downlink</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-64 flex items-end gap-[2px] px-2 border-b border-l border-white/10 relative bg-black/30 rounded-lg overflow-hidden group">
|
||||
{/* Holographic scanning line */}
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-primary/5 to-transparent w-24 h-full animate-scan pointer-events-none" />
|
||||
|
||||
<div className="absolute inset-0 grid grid-rows-4 w-full h-full pointer-events-none">
|
||||
{[...Array(4)].map((_, i) => <div key={i} className="border-t border-white/5 w-full" />)}
|
||||
</div>
|
||||
|
||||
{Array.from({ length: 60 }).map((_, i) => {
|
||||
const h1 = Math.random() * 50 + 5;
|
||||
const h2 = Math.random() * 30 + 10;
|
||||
return (
|
||||
<div key={i} className="flex-1 flex flex-col justify-end h-full opacity-60 group-hover:opacity-100 transition-opacity">
|
||||
<div className="w-full bg-magenta-500/50 hover:bg-magenta-500 transition-all rounded-t-sm" style={{ height: `${h2}%` }} />
|
||||
<div className="w-full bg-cyan-500/50 hover:bg-cyan-500 transition-all rounded-t-sm" style={{ height: `${h1}%` }} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="flex justify-between mt-4 text-[10px] font-mono text-muted-foreground uppercase opacity-50 px-2 tracking-tighter">
|
||||
<span>SYS_INIT</span>
|
||||
<span>BUFFERING_NODES...</span>
|
||||
<span>LIVE_DATA</span>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Global Control Terminal */}
|
||||
<div className="space-y-6">
|
||||
<Card variant="glass" className="p-6 bg-black/40 border-white/5">
|
||||
<h3 className="text-xs font-mono font-bold text-muted-foreground uppercase tracking-[0.3em] mb-6">Protocols</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{[
|
||||
{ id: 'purge', label: 'PURGE CACHE', icon: <HardDrive className="w-5 h-5 text-gold-500" />, color: 'gold' },
|
||||
{ id: 'index', label: 'REINDEX DB', icon: <Database className="w-5 h-5 text-cyan-500" />, color: 'cyan' },
|
||||
{ id: 'sales', label: 'SALES RT', icon: <ShoppingBag className="w-5 h-5 text-lime-500" />, color: 'lime' },
|
||||
{ id: 'logs', label: 'SEC LOGS', icon: <Eye className="w-5 h-5 text-white" />, color: 'primary' }
|
||||
].map((act) => (
|
||||
<button key={act.id} onClick={() => triggerProtocol(act.label, act.color as any)}
|
||||
className="flex flex-col items-center justify-center gap-3 h-24 rounded-2xl border border-white/5 bg-white/2 hover:bg-white/5 hover:border-white/20 transition-all group overflow-hidden relative">
|
||||
<div className={cn("absolute inset-0 bg-gradient-to-br transition-opacity opacity-0 group-hover:opacity-5", `from-${act.color}-500 to-transparent`)} />
|
||||
<div className="transition-transform group-hover:-translate-y-1">{act.icon}</div>
|
||||
<span className="text-[10px] font-mono tracking-widest text-muted-foreground group-hover:text-white">{act.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card variant="glass" className="p-6 bg-black/40 border-white/5">
|
||||
<h3 className="text-xs font-mono font-bold text-muted-foreground uppercase tracking-[0.3em] mb-6">Node Health</h3>
|
||||
<div className="space-y-4">
|
||||
{[{ l: 'CORE_KERNEL', s: 'STABLE', c: 'text-lime-500' },
|
||||
{ l: 'STORAGE_HIVE', s: '88% CAPACITY', c: 'text-gold-500' },
|
||||
{ l: 'REST_UPLINK', s: '12ms', c: 'text-cyan-500' },
|
||||
{ l: 'SECURITY_GRID', s: 'ACTIVE', c: 'text-lime-500' }]
|
||||
.map((m, i) => (
|
||||
<div key={i} className="flex justify-between items-center py-2 border-b border-white/5 last:border-0 group">
|
||||
<span className="text-[10px] font-mono text-muted-foreground group-hover:text-white transition-colors uppercase">{m.l}</span>
|
||||
<span className={cn("text-[10px] font-bold font-mono tracking-widest", m.c)}>{m.s}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Multi-Tab Interaction Terminal */}
|
||||
<Tabs defaultValue="reports" className="w-full">
|
||||
<TabsList className="bg-black/20 border-b border-white/5 w-full justify-start h-auto p-0 rounded-none gap-10 mb-8 backdrop-blur-md">
|
||||
<TabsTrigger value="reports" className="relative group rounded-none border-b-2 border-transparent data-[state=active]:bg-transparent data-[state=active]:border-red-500 data-[state=active]:text-red-500 py-5 px-0 text-lg font-display bg-transparent transition-all">
|
||||
<div className="flex items-center gap-3">
|
||||
MODERATION
|
||||
<span className="bg-red-500/20 text-red-500 px-2 py-0.5 rounded-full text-[10px] font-mono">{reports.length}</span>
|
||||
</div>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="uploads" className="relative group rounded-none border-b-2 border-transparent data-[state=active]:bg-transparent data-[state=active]:border-primary data-[state=active]:text-primary py-5 px-0 text-lg font-display bg-transparent transition-all">
|
||||
<div className="flex items-center gap-3">
|
||||
SIGNALS
|
||||
<span className="bg-primary/20 text-primary px-2 py-0.5 rounded-full text-[10px] font-mono">{uploads.length}</span>
|
||||
</div>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="logs" className="relative group rounded-none border-b-2 border-transparent data-[state=active]:bg-transparent data-[state=active]:border-gold-500 data-[state=active]:text-gold-500 py-5 px-0 text-lg font-display bg-transparent transition-all">
|
||||
<div className="flex items-center gap-3">
|
||||
SYSTEM LOGS
|
||||
<History className="w-4 h-4 opacity-50" />
|
||||
</div>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="reports" className="animate-fadeIn">
|
||||
<Card variant="glass" className="bg-black/40 border-white/5 overflow-hidden">
|
||||
<div className="divide-y divide-white/5">
|
||||
{reports.length === 0 ? (
|
||||
<div className="text-center py-20 text-muted-foreground font-mono uppercase tracking-[0.2em] opacity-40">No pending reports detected.</div>
|
||||
) : reports.map(r => (
|
||||
<div key={r.id} className="flex items-center justify-between p-6 hover:bg-white/2 transition-colors group">
|
||||
<div className="flex items-center gap-5">
|
||||
<div className="p-3 bg-red-500/10 rounded-xl text-red-500 group-hover:scale-110 transition-transform"><AlertTriangle className="w-5 h-5" /></div>
|
||||
<div>
|
||||
<div className="font-bold text-lg text-white group-hover:text-red-400 transition-colors uppercase tracking-tight">{r.targetName}</div>
|
||||
<div className="text-[10px] text-muted-foreground font-mono uppercase flex items-center gap-2">
|
||||
<span className="text-red-500/70">{r.targetType}</span> • <span className="opacity-60">{r.reason}</span> • <span className="opacity-40">{r.timestamp}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3 opacity-0 group-hover:opacity-100 transition-all translate-x-4 group-hover:translate-x-0">
|
||||
<Button variant="outline" size="sm" className="border-lime-500/20 text-lime-500 hover:bg-lime-500/10" onClick={() => handleAction(r.id, 'cleared')}>
|
||||
APPROVE
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="border-red-500/20 text-red-500 hover:bg-red-500/10" onClick={() => handleAction(r.id, 'quarantined')}>
|
||||
QUARANTINE
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="uploads" className="animate-fadeIn">
|
||||
<Card variant="glass" className="bg-black/40 border-white/5 overflow-hidden">
|
||||
<div className="divide-y divide-white/5">
|
||||
{uploads.map(u => (
|
||||
<div key={u.id} className="flex items-center justify-between p-6 hover:bg-white/2 transition-colors group">
|
||||
<div className="flex items-center gap-5">
|
||||
<div className="p-3 bg-primary/10 rounded-xl text-primary"><HardDrive className="w-5 h-5" /></div>
|
||||
<div>
|
||||
<div className="font-bold text-lg text-white">{u.name}</div>
|
||||
<div className="text-[10px] text-muted-foreground font-mono uppercase">User: {u.user} • Payload: {u.size} • Handshake: {u.date}</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" className="group-hover:bg-primary/10 hover:text-primary">INSPECT</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="logs" className="animate-fadeIn">
|
||||
<Card variant="glass" className="bg-black/40 border-white/5 font-mono overflow-hidden">
|
||||
<div className="bg-black/40 p-4 border-b border-white/5 flex gap-4 text-[10px] font-bold text-muted-foreground uppercase tracking-widest">
|
||||
<span className="w-32">Timestamp</span>
|
||||
<span className="w-24">Action</span>
|
||||
<span className="w-24">Node</span>
|
||||
<span>Payload Data</span>
|
||||
</div>
|
||||
<div className="divide-y divide-white/5 text-[10px]">
|
||||
{auditLogs.map((log, i) => (
|
||||
<div key={i} className="flex gap-4 p-4 hover:bg-white/5 transition-colors">
|
||||
<span className="w-32 text-muted-foreground">{new Date(log.timestamp).toLocaleTimeString()}</span>
|
||||
<span className="w-24 text-primary font-bold">{log.action || 'AUTH_VAL'}</span>
|
||||
<span className="w-24 text-magenta-500">USER_{log.user_id?.slice(0, 4)}</span>
|
||||
<span className="text-white opacity-80 truncate">{JSON.stringify(log.details || log.metadata || {})}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export { AdminDashboardView } from './admin-dashboard-view';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
import React from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { RefreshCw, Lock, ShieldCheck } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface AdminDashboardHeaderProps {
|
||||
protocolActive: string | null;
|
||||
onRescan: () => void;
|
||||
onLockdown: () => void;
|
||||
}
|
||||
|
||||
export function AdminDashboardHeader({
|
||||
protocolActive,
|
||||
onRescan,
|
||||
onLockdown,
|
||||
}: AdminDashboardHeaderProps) {
|
||||
return (
|
||||
<div className="flex flex-col md:flex-row items-start md:items-end justify-between gap-6">
|
||||
<div>
|
||||
<h2 className="text-4xl font-display font-bold text-white mb-2 flex items-center gap-3">
|
||||
<ShieldCheck className="text-primary w-10 h-10" /> COMMAND CENTER
|
||||
</h2>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-lime-500 animate-pulse shadow-glow-lime" />
|
||||
<span className="text-muted-foreground text-xs font-mono tracking-widest uppercase">
|
||||
Nodes Online
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-3 w-px bg-white/10" />
|
||||
<span className="text-muted-foreground text-xs font-mono tracking-widest uppercase">
|
||||
Sector: 00-ALPHA
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-white/10 hover:bg-white/5 font-mono text-xs h-10 tracking-widest"
|
||||
onClick={onRescan}
|
||||
>
|
||||
<RefreshCw
|
||||
className={cn('w-3 h-3 mr-2', protocolActive === 'RESCAN' && 'animate-spin')}
|
||||
/>{' '}
|
||||
FULL RESCAN
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-red-500/20 text-red-500 hover:bg-red-500/10 font-mono text-xs h-10 tracking-widest"
|
||||
onClick={onLockdown}
|
||||
>
|
||||
<Lock className="w-3 h-3 mr-2" /> LOCKDOWN
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import React from 'react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const NODES = [
|
||||
{ l: 'CORE_KERNEL', s: 'STABLE', c: 'text-lime-500' },
|
||||
{ l: 'STORAGE_HIVE', s: '88% CAPACITY', c: 'text-gold-500' },
|
||||
{ l: 'REST_UPLINK', s: '12ms', c: 'text-cyan-500' },
|
||||
{ l: 'SECURITY_GRID', s: 'ACTIVE', c: 'text-lime-500' },
|
||||
];
|
||||
|
||||
export function AdminDashboardNodeHealthCard() {
|
||||
return (
|
||||
<Card variant="glass" className="p-6 bg-black/40 border-white/5">
|
||||
<h3 className="text-xs font-mono font-bold text-muted-foreground uppercase tracking-widest mb-6">
|
||||
Node Health
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
{NODES.map((m, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex justify-between items-center py-2 border-b border-white/5 last:border-0 group"
|
||||
>
|
||||
<span className="text-xs font-mono text-muted-foreground group-hover:text-white transition-colors uppercase">
|
||||
{m.l}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
'text-xs font-bold font-mono tracking-widest',
|
||||
m.c,
|
||||
)}
|
||||
>
|
||||
{m.s}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
import React from 'react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { HardDrive, Database, ShoppingBag, Eye } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface AdminDashboardProtocolsCardProps {
|
||||
onTrigger: (name: string, color: string) => void;
|
||||
}
|
||||
|
||||
const PROTOCOLS = [
|
||||
{
|
||||
id: 'purge',
|
||||
label: 'PURGE CACHE',
|
||||
icon: <HardDrive className="w-5 h-5 text-gold-500" />,
|
||||
color: 'gold',
|
||||
},
|
||||
{
|
||||
id: 'index',
|
||||
label: 'REINDEX DB',
|
||||
icon: <Database className="w-5 h-5 text-cyan-500" />,
|
||||
color: 'cyan',
|
||||
},
|
||||
{
|
||||
id: 'sales',
|
||||
label: 'SALES RT',
|
||||
icon: <ShoppingBag className="w-5 h-5 text-lime-500" />,
|
||||
color: 'lime',
|
||||
},
|
||||
{
|
||||
id: 'logs',
|
||||
label: 'SEC LOGS',
|
||||
icon: <Eye className="w-5 h-5 text-white" />,
|
||||
color: 'primary',
|
||||
},
|
||||
];
|
||||
|
||||
export function AdminDashboardProtocolsCard({
|
||||
onTrigger,
|
||||
}: AdminDashboardProtocolsCardProps) {
|
||||
return (
|
||||
<Card variant="glass" className="p-6 bg-black/40 border-white/5">
|
||||
<h3 className="text-xs font-mono font-bold text-muted-foreground uppercase tracking-widest mb-6">
|
||||
Protocols
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{PROTOCOLS.map((act) => (
|
||||
<button
|
||||
key={act.id}
|
||||
type="button"
|
||||
onClick={() => onTrigger(act.label, act.color)}
|
||||
className="flex flex-col items-center justify-center gap-3 h-24 rounded-2xl border border-white/5 bg-white/5 hover:bg-white/10 hover:border-white/20 transition-all group overflow-hidden relative"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'absolute inset-0 bg-gradient-to-br transition-opacity opacity-0 group-hover:opacity-5 to-transparent',
|
||||
act.color === 'gold' && 'from-gold-500',
|
||||
act.color === 'cyan' && 'from-cyan-500',
|
||||
act.color === 'lime' && 'from-lime-500',
|
||||
act.color === 'primary' && 'from-primary',
|
||||
)}
|
||||
/>
|
||||
<div className="transition-transform group-hover:-translate-y-1 relative z-10">
|
||||
{act.icon}
|
||||
</div>
|
||||
<span className="text-xs font-mono tracking-widest text-muted-foreground group-hover:text-white relative z-10">
|
||||
{act.label}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
import React from 'react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
|
||||
export function AdminDashboardSkeleton() {
|
||||
return (
|
||||
<div className="space-y-8 pb-24 animate-fadeIn container mx-auto px-4 py-8 max-w-layout-content">
|
||||
<div className="flex flex-col md:flex-row items-start md:items-end justify-between gap-6">
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-12 w-80" />
|
||||
<div className="flex items-center gap-4">
|
||||
<Skeleton className="h-4 w-24" />
|
||||
<Skeleton className="h-4 w-28" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<Skeleton className="h-10 w-32" />
|
||||
<Skeleton className="h-10 w-28" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{[1, 2, 3, 4].map((i) => (
|
||||
<Card key={i} variant="glass" className="p-5">
|
||||
<Skeleton className="h-10 w-10 rounded-xl mb-3" />
|
||||
<Skeleton className="h-9 w-20 mb-2" />
|
||||
<Skeleton className="h-4 w-24" />
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<Card variant="glass" className="lg:col-span-2 p-8">
|
||||
<Skeleton className="h-6 w-32 mb-4" />
|
||||
<Skeleton className="h-64 w-full rounded-lg" />
|
||||
</Card>
|
||||
<div className="space-y-6">
|
||||
<Card variant="glass" className="p-6">
|
||||
<Skeleton className="h-4 w-24 mb-6" />
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{[1, 2, 3, 4].map((i) => (
|
||||
<Skeleton key={i} className="h-24 rounded-2xl" />
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
<Card variant="glass" className="p-6">
|
||||
<Skeleton className="h-4 w-28 mb-6" />
|
||||
<div className="space-y-4">
|
||||
{[1, 2, 3, 4].map((i) => (
|
||||
<Skeleton key={i} className="h-8 w-full" />
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<Skeleton className="h-12 w-full" />
|
||||
<Card variant="glass" className="p-6">
|
||||
<div className="space-y-4">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<Skeleton key={i} className="h-16 w-full" />
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
import React from 'react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { StatCardProps } from './types';
|
||||
|
||||
const colorClasses: Record<StatCardProps['color'], string> = {
|
||||
cyan: 'bg-cyan-500',
|
||||
gold: 'bg-gold-500',
|
||||
lime: 'bg-lime-500',
|
||||
red: 'bg-red-500',
|
||||
};
|
||||
|
||||
const textColorClasses: Record<StatCardProps['color'], string> = {
|
||||
cyan: 'text-cyan-500',
|
||||
gold: 'text-gold-500',
|
||||
lime: 'text-lime-500',
|
||||
red: 'text-red-500',
|
||||
};
|
||||
|
||||
export function AdminDashboardStatCard({
|
||||
label,
|
||||
value,
|
||||
icon,
|
||||
color,
|
||||
trend,
|
||||
}: StatCardProps) {
|
||||
return (
|
||||
<Card
|
||||
variant="glass"
|
||||
className="p-5 relative overflow-hidden group hover:border-primary/50 transition-all cursor-default"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'absolute -right-4 -top-4 w-24 h-24 rounded-full opacity-10 blur-2xl transition-all group-hover:opacity-20',
|
||||
colorClasses[color],
|
||||
)}
|
||||
/>
|
||||
<div className="flex justify-between items-start mb-3 relative z-10">
|
||||
<div
|
||||
className={cn(
|
||||
'p-2.5 rounded-xl bg-white/5 shadow-inner',
|
||||
textColorClasses[color],
|
||||
)}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
{trend !== undefined && (
|
||||
<span
|
||||
className={cn(
|
||||
'text-xs font-mono font-bold px-2 py-0.5 rounded-full border tracking-tighter',
|
||||
trend > 0
|
||||
? 'text-lime-500 border-lime-500/20 bg-lime-500/10'
|
||||
: 'text-red-500 border-red-500/20 bg-red-500/10',
|
||||
)}
|
||||
>
|
||||
{trend > 0 ? '+' : ''}
|
||||
{trend}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-3xl font-display font-bold text-white mb-1 relative z-10">
|
||||
{value ?? '—'}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground uppercase tracking-widest font-mono relative z-10">
|
||||
{label}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,181 @@
|
|||
import React from 'react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
|
||||
import { AlertTriangle, HardDrive, History } from 'lucide-react';
|
||||
import type { Report } from '@/types';
|
||||
import type { UploadItem, AuditLogItem } from './types';
|
||||
|
||||
interface AdminDashboardTabsProps {
|
||||
reports: Report[];
|
||||
uploads: UploadItem[];
|
||||
auditLogs: AuditLogItem[];
|
||||
onReportAction: (id: string, action: string) => void;
|
||||
}
|
||||
|
||||
export function AdminDashboardTabs({
|
||||
reports,
|
||||
uploads,
|
||||
auditLogs,
|
||||
onReportAction,
|
||||
}: AdminDashboardTabsProps) {
|
||||
return (
|
||||
<Tabs defaultValue="reports" className="w-full">
|
||||
<TabsList className="bg-black/20 border-b border-white/5 w-full justify-start h-auto p-0 rounded-none gap-10 mb-8 backdrop-blur-md">
|
||||
<TabsTrigger
|
||||
value="reports"
|
||||
className="relative group rounded-none border-b-2 border-transparent data-[state=active]:bg-transparent data-[state=active]:border-red-500 data-[state=active]:text-red-500 py-5 px-0 text-lg font-display bg-transparent transition-all"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
MODERATION
|
||||
<span className="bg-red-500/20 text-red-500 px-2 py-0.5 rounded-full text-xs font-mono">
|
||||
{reports.length}
|
||||
</span>
|
||||
</div>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="uploads"
|
||||
className="relative group rounded-none border-b-2 border-transparent data-[state=active]:bg-transparent data-[state=active]:border-primary data-[state=active]:text-primary py-5 px-0 text-lg font-display bg-transparent transition-all"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
SIGNALS
|
||||
<span className="bg-primary/20 text-primary px-2 py-0.5 rounded-full text-xs font-mono">
|
||||
{uploads.length}
|
||||
</span>
|
||||
</div>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="logs"
|
||||
className="relative group rounded-none border-b-2 border-transparent data-[state=active]:bg-transparent data-[state=active]:border-gold-500 data-[state=active]:text-gold-500 py-5 px-0 text-lg font-display bg-transparent transition-all"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
SYSTEM LOGS
|
||||
<History className="w-4 h-4 opacity-50" />
|
||||
</div>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="reports" className="animate-fadeIn">
|
||||
<Card variant="glass" className="bg-black/40 border-white/5 overflow-hidden">
|
||||
<div className="divide-y divide-white/5">
|
||||
{reports.length === 0 ? (
|
||||
<div className="text-center py-20 text-muted-foreground font-mono uppercase tracking-widest opacity-40">
|
||||
No pending reports detected.
|
||||
</div>
|
||||
) : (
|
||||
reports.map((r) => (
|
||||
<div
|
||||
key={r.id}
|
||||
className="flex items-center justify-between p-6 hover:bg-white/5 transition-colors group"
|
||||
>
|
||||
<div className="flex items-center gap-5">
|
||||
<div className="p-3 bg-red-500/10 rounded-xl text-red-500 group-hover:scale-110 transition-transform">
|
||||
<AlertTriangle className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-bold text-lg text-white group-hover:text-red-400 transition-colors uppercase tracking-tight">
|
||||
{r.targetName}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground font-mono uppercase flex items-center gap-2">
|
||||
<span className="text-red-500/70">{r.targetType}</span> •{' '}
|
||||
<span className="opacity-60">{r.reason}</span> •{' '}
|
||||
<span className="opacity-40">{r.timestamp}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3 opacity-0 group-hover:opacity-100 transition-all translate-x-4 group-hover:translate-x-0">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-lime-500/20 text-lime-500 hover:bg-lime-500/10"
|
||||
onClick={() => onReportAction(r.id, 'cleared')}
|
||||
>
|
||||
APPROVE
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-red-500/20 text-red-500 hover:bg-red-500/10"
|
||||
onClick={() => onReportAction(r.id, 'quarantined')}
|
||||
>
|
||||
QUARANTINE
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="uploads" className="animate-fadeIn">
|
||||
<Card variant="glass" className="bg-black/40 border-white/5 overflow-hidden">
|
||||
<div className="divide-y divide-white/5">
|
||||
{uploads.map((u) => (
|
||||
<div
|
||||
key={u.id}
|
||||
className="flex items-center justify-between p-6 hover:bg-white/5 transition-colors group"
|
||||
>
|
||||
<div className="flex items-center gap-5">
|
||||
<div className="p-3 bg-primary/10 rounded-xl text-primary">
|
||||
<HardDrive className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-bold text-lg text-white">{u.name}</div>
|
||||
<div className="text-xs text-muted-foreground font-mono uppercase">
|
||||
User: {u.user} • Payload: {u.size} • Handshake: {u.date}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="group-hover:bg-primary/10 hover:text-primary"
|
||||
>
|
||||
INSPECT
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="logs" className="animate-fadeIn">
|
||||
<Card
|
||||
variant="glass"
|
||||
className="bg-black/40 border-white/5 font-mono overflow-hidden"
|
||||
>
|
||||
<div className="bg-black/40 p-4 border-b border-white/5 flex gap-4 text-xs font-bold text-muted-foreground uppercase tracking-widest">
|
||||
<span className="w-32">Timestamp</span>
|
||||
<span className="w-24">Action</span>
|
||||
<span className="w-24">Node</span>
|
||||
<span>Payload Data</span>
|
||||
</div>
|
||||
<div className="divide-y divide-white/5 text-xs">
|
||||
{auditLogs.map((log, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex gap-4 p-4 hover:bg-white/5 transition-colors"
|
||||
>
|
||||
<span className="w-32 text-muted-foreground">
|
||||
{log.timestamp
|
||||
? new Date(log.timestamp).toLocaleTimeString()
|
||||
: '—'}
|
||||
</span>
|
||||
<span className="w-24 text-primary font-bold">
|
||||
{log.action ?? 'AUTH_VAL'}
|
||||
</span>
|
||||
<span className="w-24 text-magenta-500">
|
||||
USER_{log.user_id != null ? String(log.user_id).slice(0, 4) : '???'}
|
||||
</span>
|
||||
<span className="text-white opacity-80 truncate">
|
||||
{JSON.stringify(log.details ?? log.metadata ?? {})}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
import React from 'react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Server } from 'lucide-react';
|
||||
|
||||
export function AdminDashboardTrafficCard() {
|
||||
return (
|
||||
<Card
|
||||
variant="glass"
|
||||
className="lg:col-span-2 p-8 bg-black/40 border-white/5 relative overflow-hidden"
|
||||
>
|
||||
<div className="flex justify-between items-center mb-10 relative z-10">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-white flex items-center gap-3">
|
||||
<Server className="w-5 h-5 text-primary" /> Traffic Flux
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground font-mono mt-1">
|
||||
HOLOGRAPHIC STREAMING INTERFACE
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-8 text-xs font-mono uppercase tracking-widest italic">
|
||||
<span className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-cyan-500 shadow-glow-cyan" />
|
||||
Uplink
|
||||
</span>
|
||||
<span className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-magenta-500 shadow-glow-magenta" />
|
||||
Downlink
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-64 flex items-end gap-0.5 px-2 border-b border-l border-white/10 relative bg-black/30 rounded-lg overflow-hidden group">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-primary/5 to-transparent w-24 h-full animate-scan pointer-events-none" />
|
||||
<div className="absolute inset-0 grid grid-rows-4 w-full h-full pointer-events-none">
|
||||
{[...Array(4)].map((_, i) => (
|
||||
<div key={i} className="border-t border-white/5 w-full" />
|
||||
))}
|
||||
</div>
|
||||
{Array.from({ length: 60 }).map((_, i) => {
|
||||
const h1 = Math.random() * 50 + 5;
|
||||
const h2 = Math.random() * 30 + 10;
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className="flex-1 flex flex-col justify-end h-full opacity-60 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<div
|
||||
className="w-full bg-magenta-500/50 hover:bg-magenta-500 transition-all rounded-t-sm"
|
||||
style={{ height: `${h2}%` }}
|
||||
/>
|
||||
<div
|
||||
className="w-full bg-cyan-500/50 hover:bg-cyan-500 transition-all rounded-t-sm"
|
||||
style={{ height: `${h1}%` }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="flex justify-between mt-4 text-xs font-mono text-muted-foreground uppercase opacity-50 px-2 tracking-tighter">
|
||||
<span>SYS_INIT</span>
|
||||
<span>BUFFERING_NODES...</span>
|
||||
<span>LIVE_DATA</span>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
import React from 'react';
|
||||
import { Users, DollarSign, Activity, ShieldAlert } from 'lucide-react';
|
||||
import { useAdminDashboardView } from './useAdminDashboardView';
|
||||
import { AdminDashboardHeader } from './AdminDashboardHeader';
|
||||
import { AdminDashboardStatCard } from './AdminDashboardStatCard';
|
||||
import { AdminDashboardTrafficCard } from './AdminDashboardTrafficCard';
|
||||
import { AdminDashboardProtocolsCard } from './AdminDashboardProtocolsCard';
|
||||
import { AdminDashboardNodeHealthCard } from './AdminDashboardNodeHealthCard';
|
||||
import { AdminDashboardTabs } from './AdminDashboardTabs';
|
||||
import { AdminDashboardSkeleton } from './AdminDashboardSkeleton';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
|
||||
export function AdminDashboardView() {
|
||||
const {
|
||||
stats,
|
||||
reports,
|
||||
uploads,
|
||||
auditLogs,
|
||||
loading,
|
||||
protocolActive,
|
||||
handleAction,
|
||||
triggerProtocol,
|
||||
} = useAdminDashboardView();
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex justify-center py-24">
|
||||
<Loader2 className="w-10 h-10 text-primary animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8 pb-24 animate-fadeIn container mx-auto px-4 py-8 max-w-layout-content">
|
||||
<AdminDashboardHeader
|
||||
protocolActive={protocolActive}
|
||||
onRescan={() => triggerProtocol('RESCAN', 'success')}
|
||||
onLockdown={() => triggerProtocol('LOCKDOWN', 'error')}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<AdminDashboardStatCard
|
||||
label="Total Nodes"
|
||||
value={stats.totalUsers?.toLocaleString()}
|
||||
icon={<Users className="w-5 h-5" />}
|
||||
trend={stats.trends?.users}
|
||||
color="cyan"
|
||||
/>
|
||||
<AdminDashboardStatCard
|
||||
label="Credit Volume"
|
||||
value={`$${stats.monthlyRevenue?.toLocaleString()}`}
|
||||
icon={<DollarSign className="w-5 h-5" />}
|
||||
trend={stats.trends?.revenue}
|
||||
color="gold"
|
||||
/>
|
||||
<AdminDashboardStatCard
|
||||
label="Active Uplinks"
|
||||
value={stats.activeSessions?.toLocaleString()}
|
||||
icon={<Activity className="w-5 h-5" />}
|
||||
trend={stats.trends?.sessions}
|
||||
color="lime"
|
||||
/>
|
||||
<AdminDashboardStatCard
|
||||
label="Threat Reports"
|
||||
value={stats.pendingReports}
|
||||
icon={<ShieldAlert className="w-5 h-5" />}
|
||||
trend={stats.trends?.reports}
|
||||
color="red"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<AdminDashboardTrafficCard />
|
||||
<div className="space-y-6">
|
||||
<AdminDashboardProtocolsCard onTrigger={triggerProtocol} />
|
||||
<AdminDashboardNodeHealthCard />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AdminDashboardTabs
|
||||
reports={reports}
|
||||
uploads={uploads}
|
||||
auditLogs={auditLogs}
|
||||
onReportAction={handleAction}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
10
apps/web/src/components/admin/admin-dashboard-view/index.ts
Normal file
10
apps/web/src/components/admin/admin-dashboard-view/index.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
export type {
|
||||
DashboardStats,
|
||||
UploadItem,
|
||||
AuditLogItem,
|
||||
StatCardProps,
|
||||
Report,
|
||||
} from './types';
|
||||
export { AdminDashboardView } from './AdminDashboardView';
|
||||
export { AdminDashboardSkeleton } from './AdminDashboardSkeleton';
|
||||
export { useAdminDashboardView } from './useAdminDashboardView';
|
||||
41
apps/web/src/components/admin/admin-dashboard-view/types.ts
Normal file
41
apps/web/src/components/admin/admin-dashboard-view/types.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import type { ReactNode } from 'react';
|
||||
import type { Report } from '@/types';
|
||||
|
||||
export interface DashboardStats {
|
||||
totalUsers?: number;
|
||||
monthlyRevenue?: number;
|
||||
activeSessions?: number;
|
||||
pendingReports?: number;
|
||||
trends?: {
|
||||
users?: number;
|
||||
revenue?: number;
|
||||
sessions?: number;
|
||||
reports?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface UploadItem {
|
||||
id: string;
|
||||
name: string;
|
||||
user: string;
|
||||
size: string;
|
||||
date: string;
|
||||
}
|
||||
|
||||
export interface AuditLogItem {
|
||||
timestamp?: string;
|
||||
action?: string;
|
||||
user_id?: string | number;
|
||||
details?: unknown;
|
||||
metadata?: unknown;
|
||||
}
|
||||
|
||||
export interface StatCardProps {
|
||||
label: string;
|
||||
value: string | number | undefined;
|
||||
icon: ReactNode;
|
||||
color: 'cyan' | 'gold' | 'lime' | 'red';
|
||||
trend?: number;
|
||||
}
|
||||
|
||||
export type { Report };
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useToast } from '@/components/feedback/ToastProvider';
|
||||
import { adminService } from '@/services/adminService';
|
||||
import { logger } from '@/utils/logger';
|
||||
import type { Report } from '@/types';
|
||||
import type { DashboardStats, UploadItem, AuditLogItem } from './types';
|
||||
|
||||
export function useAdminDashboardView() {
|
||||
const { addToast } = useToast();
|
||||
const [stats, setStats] = useState<DashboardStats>({});
|
||||
const [reports, setReports] = useState<Report[]>([]);
|
||||
const [uploads, setUploads] = useState<UploadItem[]>([]);
|
||||
const [auditLogs, setAuditLogs] = useState<AuditLogItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [protocolActive, setProtocolActive] = useState<string | null>(null);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [statsData, reportsData, uploadsData, logsData] = await Promise.all([
|
||||
adminService.getDashboardStats(),
|
||||
adminService.getModerationQueue('pending'),
|
||||
adminService.getRecentUploads(),
|
||||
adminService.getAuditLogs({ limit: 10 }),
|
||||
]);
|
||||
setStats(statsData as DashboardStats);
|
||||
setReports((reportsData as Report[]) || []);
|
||||
setUploads((uploadsData as UploadItem[]) || []);
|
||||
setAuditLogs(logsData?.logs || []);
|
||||
} catch (e) {
|
||||
logger.error('Error loading admin dashboard', { error: e });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
const handleAction = useCallback(
|
||||
async (id: string, action: string) => {
|
||||
await adminService.resolveReport(id, action);
|
||||
setReports((prev) => prev.filter((r) => r.id !== id));
|
||||
addToast(`Protocol "${action.toUpperCase()}" executed successfully.`, 'success');
|
||||
},
|
||||
[addToast],
|
||||
);
|
||||
|
||||
const triggerProtocol = useCallback(
|
||||
(name: string, color: string) => {
|
||||
setProtocolActive(name);
|
||||
addToast(`INITIALIZING ${name.toUpperCase()}...`, 'info');
|
||||
setTimeout(() => {
|
||||
addToast(`${name.toUpperCase()} DEPLOYED`, color as 'success' | 'error');
|
||||
setProtocolActive(null);
|
||||
}, 2000);
|
||||
},
|
||||
[addToast],
|
||||
);
|
||||
|
||||
return {
|
||||
stats,
|
||||
reports,
|
||||
uploads,
|
||||
auditLogs,
|
||||
loading,
|
||||
protocolActive,
|
||||
handleAction,
|
||||
triggerProtocol,
|
||||
};
|
||||
}
|
||||
Loading…
Reference in a new issue