feat(dashboard): Complete "Mission Control" UI redesign
Overhaul the GuruRMM dashboard with a dark cyberpunk aesthetic featuring glassmorphism effects, cyan accent lighting, and smooth animations. Visual Changes: - Dark theme with CSS variables for consistent theming - Glassmorphism card effects with colored glow variants - Grid pattern backgrounds and floating geometric shapes - JetBrains Mono + Inter font pairing for tech aesthetic - Cyan, green, amber, and rose accent colors with glow effects Component Updates: - index.css: Complete CSS overhaul with utility classes, animations, and glassmorphism foundations (1300+ lines added) - Login.tsx: Glassmorphism login card with gradient logo and floating background shapes - Layout.tsx: Dark sidebar with cyan nav highlights, grid pattern main area, animated user profile section - Dashboard.tsx: Animated stat cards with staggered entrances, live status indicator with pulse animation, relative timestamps - Card.tsx: Added glow variants (cyan/green/amber/rose) with hover lift effects - Button.tsx: Gradient backgrounds, glow-on-hover, scale animations - Input.tsx: Dark styling with cyan focus glow, added Textarea component Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,35 +1,249 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Activity, Server, AlertTriangle, CheckCircle } from "lucide-react";
|
||||
import {
|
||||
Activity,
|
||||
Server,
|
||||
AlertTriangle,
|
||||
Wifi,
|
||||
WifiOff,
|
||||
Terminal,
|
||||
RefreshCw,
|
||||
Shield,
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
import { agentsApi, Agent } from "../api/client";
|
||||
import { Card, CardHeader, CardTitle, CardContent } from "../components/Card";
|
||||
|
||||
/**
|
||||
* Formats a date to a relative time string (e.g., "2 minutes ago", "1 hour ago")
|
||||
*/
|
||||
function formatRelativeTime(dateString: string | null): string {
|
||||
if (!dateString) return "Never seen";
|
||||
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
|
||||
|
||||
if (diffInSeconds < 60) {
|
||||
return "Just now";
|
||||
} else if (diffInSeconds < 3600) {
|
||||
const minutes = Math.floor(diffInSeconds / 60);
|
||||
return `${minutes}m ago`;
|
||||
} else if (diffInSeconds < 86400) {
|
||||
const hours = Math.floor(diffInSeconds / 3600);
|
||||
return `${hours}h ago`;
|
||||
} else {
|
||||
const days = Math.floor(diffInSeconds / 86400);
|
||||
return `${days}d ago`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loading skeleton for stat cards with shimmer animation
|
||||
*/
|
||||
function StatCardSkeleton({ delay }: { delay: number }) {
|
||||
return (
|
||||
<div
|
||||
className="glass-card relative overflow-hidden opacity-0"
|
||||
style={{
|
||||
animation: `fadeInUp 0.4s ease-out ${delay}s forwards`,
|
||||
}}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="space-y-3">
|
||||
<div className="skeleton skeleton-text w-24 h-4" />
|
||||
<div className="skeleton skeleton-text w-16 h-10" />
|
||||
<div className="skeleton skeleton-text w-32 h-3" />
|
||||
</div>
|
||||
<div className="skeleton w-12 h-12 rounded-lg" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stat card component with Mission Control styling
|
||||
*/
|
||||
function StatCard({
|
||||
title,
|
||||
value,
|
||||
icon: Icon,
|
||||
description,
|
||||
accentColor,
|
||||
delay,
|
||||
}: {
|
||||
title: string;
|
||||
value: string | number;
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
description?: string;
|
||||
accentColor: "cyan" | "green" | "amber" | "rose";
|
||||
delay: number;
|
||||
}) {
|
||||
const colorClasses = {
|
||||
cyan: {
|
||||
icon: "text-cyan",
|
||||
glow: "glow-cyan",
|
||||
bg: "bg-[var(--accent-cyan-muted)]",
|
||||
border: "border-l-[var(--accent-cyan)]",
|
||||
value: "text-cyan",
|
||||
},
|
||||
green: {
|
||||
icon: "text-green",
|
||||
glow: "glow-green",
|
||||
bg: "bg-[var(--accent-green-muted)]",
|
||||
border: "border-l-[var(--accent-green)]",
|
||||
value: "text-green",
|
||||
},
|
||||
amber: {
|
||||
icon: "text-amber",
|
||||
glow: "glow-amber",
|
||||
bg: "bg-[var(--accent-amber-muted)]",
|
||||
border: "border-l-[var(--accent-amber)]",
|
||||
value: "text-amber",
|
||||
},
|
||||
rose: {
|
||||
icon: "text-rose",
|
||||
glow: "glow-rose",
|
||||
bg: "bg-[var(--accent-rose-muted)]",
|
||||
border: "border-l-[var(--accent-rose)]",
|
||||
value: "text-rose",
|
||||
},
|
||||
};
|
||||
|
||||
const colors = colorClasses[accentColor];
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">{title}</CardTitle>
|
||||
<Icon className="h-4 w-4 text-[hsl(var(--muted-foreground))]" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{value}</div>
|
||||
{description && (
|
||||
<p className="text-xs text-[hsl(var(--muted-foreground))]">{description}</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div
|
||||
className="glass-card relative overflow-hidden opacity-0 border-l-4 group cursor-default"
|
||||
style={{
|
||||
animation: `fadeInUp 0.4s ease-out ${delay}s forwards`,
|
||||
borderLeftColor: `var(--accent-${accentColor})`,
|
||||
}}
|
||||
>
|
||||
{/* Subtle gradient overlay on hover */}
|
||||
<div
|
||||
className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none"
|
||||
style={{
|
||||
background: `radial-gradient(circle at top right, var(--accent-${accentColor}-muted), transparent 70%)`,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="relative flex items-start justify-between">
|
||||
<div className="space-y-1">
|
||||
<p className="text-muted font-mono text-xs uppercase tracking-widest-custom">
|
||||
{title}
|
||||
</p>
|
||||
<p className={`font-mono text-4xl font-bold ${colors.value}`}>
|
||||
{value}
|
||||
</p>
|
||||
{description && (
|
||||
<p className="text-muted text-sm">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Icon with glow effect */}
|
||||
<div
|
||||
className={`p-3 rounded-lg ${colors.bg} ${colors.glow} transition-all duration-300 group-hover:scale-110`}
|
||||
>
|
||||
<Icon className={`h-6 w-6 ${colors.icon}`} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Status indicator dot with pulse animation
|
||||
*/
|
||||
function StatusDot({ status }: { status: string }) {
|
||||
const statusClasses = {
|
||||
online: "status-dot online",
|
||||
offline: "status-dot offline",
|
||||
error: "status-dot error",
|
||||
};
|
||||
|
||||
return (
|
||||
<span
|
||||
className={statusClasses[status as keyof typeof statusClasses] || "status-dot offline"}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Activity list item with hover effects
|
||||
*/
|
||||
function ActivityItem({ agent }: { agent: Agent }) {
|
||||
return (
|
||||
<div className="flex items-center justify-between p-3 rounded-lg transition-all duration-200 hover:bg-[rgba(6,182,212,0.05)] group cursor-default">
|
||||
<div className="flex items-center gap-4">
|
||||
<StatusDot status={agent.status} />
|
||||
<div>
|
||||
<p className="font-mono text-sm font-medium text-primary group-hover:text-cyan transition-colors">
|
||||
{agent.hostname}
|
||||
</p>
|
||||
<p className="text-xs text-muted uppercase tracking-wide">
|
||||
{agent.os_type}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className="font-mono text-xs text-muted tabular-nums">
|
||||
{formatRelativeTime(agent.last_seen)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick action button with glow effect
|
||||
*/
|
||||
function QuickActionButton({
|
||||
icon: Icon,
|
||||
label,
|
||||
description,
|
||||
onClick,
|
||||
}: {
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
label: string;
|
||||
description: string;
|
||||
onClick?: () => void;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className="flex items-center gap-4 p-4 w-full text-left rounded-lg border border-[var(--border-primary)] bg-[var(--bg-tertiary)] hover:border-[var(--accent-cyan)] hover:bg-[var(--accent-cyan-muted)] transition-all duration-200 group"
|
||||
>
|
||||
<div className="p-2 rounded-lg bg-[var(--bg-secondary)] group-hover:glow-cyan transition-all duration-200">
|
||||
<Icon className="h-5 w-5 text-cyan" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-mono text-sm font-medium text-primary">{label}</p>
|
||||
<p className="text-xs text-muted">{description}</p>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loading skeleton for activity list
|
||||
*/
|
||||
function ActivityListSkeleton() {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{[0, 1, 2, 3, 4].map((i) => (
|
||||
<div key={i} className="flex items-center gap-4 p-3">
|
||||
<div className="skeleton w-2 h-2 rounded-full" />
|
||||
<div className="flex-1 space-y-2">
|
||||
<div className="skeleton skeleton-text w-32 h-4" />
|
||||
<div className="skeleton skeleton-text w-20 h-3" />
|
||||
</div>
|
||||
<div className="skeleton skeleton-text w-16 h-3" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main Dashboard component with Mission Control aesthetic
|
||||
*/
|
||||
export function Dashboard() {
|
||||
const { data: agents = [], isLoading } = useQuery({
|
||||
queryKey: ["agents"],
|
||||
@@ -41,101 +255,175 @@ export function Dashboard() {
|
||||
const offlineAgents = agents.filter((a: Agent) => a.status === "offline");
|
||||
const errorAgents = agents.filter((a: Agent) => a.status === "error");
|
||||
|
||||
// Determine system status
|
||||
const hasErrors = errorAgents.length > 0;
|
||||
const allOffline = agents.length > 0 && onlineAgents.length === 0;
|
||||
const systemStatus = hasErrors
|
||||
? "ATTENTION REQUIRED"
|
||||
: allOffline
|
||||
? "ALL SYSTEMS OFFLINE"
|
||||
: "SYSTEMS OPERATIONAL";
|
||||
const statusClass = hasErrors
|
||||
? "status-error"
|
||||
: allOffline
|
||||
? "status-warning"
|
||||
: "status-online";
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Dashboard</h1>
|
||||
<p className="text-[hsl(var(--muted-foreground))]">
|
||||
Overview of your managed endpoints
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-8">
|
||||
{/* Page Header */}
|
||||
<header className="space-y-2 animate-fade-in">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-gradient font-mono text-4xl font-bold tracking-tight">
|
||||
DASHBOARD
|
||||
</h1>
|
||||
<p className="text-muted text-sm uppercase tracking-widest-custom mt-1">
|
||||
Mission Control Overview
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<StatCard
|
||||
title="Total Agents"
|
||||
value={isLoading ? "..." : agents.length}
|
||||
icon={Server}
|
||||
description="Registered endpoints"
|
||||
/>
|
||||
<StatCard
|
||||
title="Online"
|
||||
value={isLoading ? "..." : onlineAgents.length}
|
||||
icon={CheckCircle}
|
||||
description="Currently connected"
|
||||
/>
|
||||
<StatCard
|
||||
title="Offline"
|
||||
value={isLoading ? "..." : offlineAgents.length}
|
||||
icon={Activity}
|
||||
description="Not responding"
|
||||
/>
|
||||
<StatCard
|
||||
title="Errors"
|
||||
value={isLoading ? "..." : errorAgents.length}
|
||||
icon={AlertTriangle}
|
||||
description="Requires attention"
|
||||
/>
|
||||
</div>
|
||||
{/* System Status Indicator */}
|
||||
<div className={`status-indicator ${statusClass}`}>
|
||||
<span className="status-dot" />
|
||||
<span className="font-mono text-xs uppercase tracking-wide">
|
||||
{systemStatus}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Recent Activity</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoading ? (
|
||||
<p className="text-[hsl(var(--muted-foreground))]">Loading...</p>
|
||||
) : agents.length === 0 ? (
|
||||
<p className="text-[hsl(var(--muted-foreground))]">
|
||||
No agents registered yet. Deploy an agent to get started.
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{agents.slice(0, 5).map((agent: Agent) => (
|
||||
<div key={agent.id} className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className={`h-2 w-2 rounded-full ${
|
||||
agent.status === "online"
|
||||
? "bg-green-500"
|
||||
: agent.status === "error"
|
||||
? "bg-red-500"
|
||||
: "bg-gray-400"
|
||||
}`}
|
||||
/>
|
||||
<div>
|
||||
<p className="font-medium">{agent.hostname}</p>
|
||||
<p className="text-xs text-[hsl(var(--muted-foreground))]">
|
||||
{agent.os_type}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xs text-[hsl(var(--muted-foreground))]">
|
||||
{agent.last_seen
|
||||
? new Date(agent.last_seen).toLocaleString()
|
||||
: "Never seen"}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{/* Stat Cards Grid */}
|
||||
<section className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
{isLoading ? (
|
||||
<>
|
||||
<StatCardSkeleton delay={0.1} />
|
||||
<StatCardSkeleton delay={0.2} />
|
||||
<StatCardSkeleton delay={0.3} />
|
||||
<StatCardSkeleton delay={0.4} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<StatCard
|
||||
title="Total Agents"
|
||||
value={agents.length}
|
||||
icon={Server}
|
||||
description="Registered endpoints"
|
||||
accentColor="cyan"
|
||||
delay={0.1}
|
||||
/>
|
||||
<StatCard
|
||||
title="Online"
|
||||
value={onlineAgents.length}
|
||||
icon={Wifi}
|
||||
description="Currently connected"
|
||||
accentColor="green"
|
||||
delay={0.2}
|
||||
/>
|
||||
<StatCard
|
||||
title="Offline"
|
||||
value={offlineAgents.length}
|
||||
icon={WifiOff}
|
||||
description="Not responding"
|
||||
accentColor="amber"
|
||||
delay={0.3}
|
||||
/>
|
||||
<StatCard
|
||||
title="Errors"
|
||||
value={errorAgents.length}
|
||||
icon={AlertTriangle}
|
||||
description="Requires attention"
|
||||
accentColor="rose"
|
||||
delay={0.4}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Bottom Grid: Activity + Quick Actions */}
|
||||
<section className="grid gap-6 md:grid-cols-2">
|
||||
{/* Recent Activity Card */}
|
||||
<div
|
||||
className="glass-card opacity-0"
|
||||
style={{
|
||||
animation: "fadeInUp 0.4s ease-out 0.5s forwards",
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-4 pb-4 border-b border-[var(--border-secondary)]">
|
||||
<h2 className="card-title flex items-center gap-2">
|
||||
<Activity className="h-4 w-4 text-cyan" />
|
||||
Recent Activity
|
||||
</h2>
|
||||
{!isLoading && agents.length > 0 && (
|
||||
<span className="font-mono text-xs text-muted">
|
||||
{agents.length} agent{agents.length !== 1 ? "s" : ""}
|
||||
</span>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Quick Actions</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2 text-sm text-[hsl(var(--muted-foreground))]">
|
||||
<p>Deploy a new agent to start monitoring endpoints.</p>
|
||||
<p className="mt-4">
|
||||
Use the Agents page to view details and send commands to your endpoints.
|
||||
{isLoading ? (
|
||||
<ActivityListSkeleton />
|
||||
) : agents.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<Server className="h-12 w-12 text-muted mx-auto mb-4 opacity-50" />
|
||||
<p className="text-secondary font-medium mb-2">
|
||||
No agents registered
|
||||
</p>
|
||||
<p className="text-muted text-sm">
|
||||
Deploy an agent to start monitoring endpoints.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-1 -mx-3">
|
||||
{agents.slice(0, 5).map((agent: Agent) => (
|
||||
<ActivityItem key={agent.id} agent={agent} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Quick Actions Card */}
|
||||
<div
|
||||
className="glass-card opacity-0"
|
||||
style={{
|
||||
animation: "fadeInUp 0.4s ease-out 0.6s forwards",
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-4 pb-4 border-b border-[var(--border-secondary)]">
|
||||
<Zap className="h-4 w-4 text-cyan" />
|
||||
<h2 className="card-title">Quick Actions</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<QuickActionButton
|
||||
icon={Terminal}
|
||||
label="Deploy Agent"
|
||||
description="Install agent on a new endpoint"
|
||||
/>
|
||||
<QuickActionButton
|
||||
icon={RefreshCw}
|
||||
label="Refresh All"
|
||||
description="Force update all agent statuses"
|
||||
/>
|
||||
<QuickActionButton
|
||||
icon={Shield}
|
||||
label="Security Scan"
|
||||
description="Run security audit on all endpoints"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Terminal-style hint */}
|
||||
<div className="mt-6 p-3 rounded-lg bg-[var(--bg-primary)] border border-[var(--border-secondary)]">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono text-xs text-cyan">$</span>
|
||||
<span className="font-mono text-xs text-muted">
|
||||
guru-rmm --help
|
||||
</span>
|
||||
<span className="inline-block w-2 h-4 bg-cyan animate-pulse ml-1" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user