feat(dashboard): UI refinements - density, flat agents table, history log
- Reduce layout density ~20% (tighter padding, margins, fonts) - Flatten Agents table view with Client/Site columns (no grouping) - Add version info to sidebar footer (UI v0.2.0, API v0.1.0) - Replace Commands nav with sidebar History log - Add /history page with full command list - Add /history/:id detail view with output display - Apply Mission Control styling to all new components Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Link } from "react-router-dom";
|
||||
import {
|
||||
Activity,
|
||||
Server,
|
||||
@@ -9,6 +11,7 @@ import {
|
||||
RefreshCw,
|
||||
Shield,
|
||||
Zap,
|
||||
ArrowRight,
|
||||
} from "lucide-react";
|
||||
import { agentsApi, Agent } from "../api/client";
|
||||
|
||||
@@ -60,7 +63,7 @@ function StatCardSkeleton({ delay }: { delay: number }) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Stat card component with Mission Control styling
|
||||
* Stat card component with Mission Control styling - navigates to filtered agents page on click
|
||||
*/
|
||||
function StatCard({
|
||||
title,
|
||||
@@ -69,6 +72,7 @@ function StatCard({
|
||||
description,
|
||||
accentColor,
|
||||
delay,
|
||||
linkTo,
|
||||
}: {
|
||||
title: string;
|
||||
value: string | number;
|
||||
@@ -76,47 +80,59 @@ function StatCard({
|
||||
description?: string;
|
||||
accentColor: "cyan" | "green" | "amber" | "rose";
|
||||
delay: number;
|
||||
linkTo: string;
|
||||
}) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
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];
|
||||
|
||||
const handleClick = () => {
|
||||
navigate(linkTo);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="glass-card relative overflow-hidden opacity-0 border-l-4 group cursor-default"
|
||||
className="glass-card relative overflow-hidden opacity-0 border-l-4 group cursor-pointer transition-transform duration-200 hover:scale-[1.02]"
|
||||
style={{
|
||||
animation: `fadeInUp 0.4s ease-out ${delay}s forwards`,
|
||||
borderLeftColor: `var(--accent-${accentColor})`,
|
||||
}}
|
||||
onClick={handleClick}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
handleClick();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* Subtle gradient overlay on hover */}
|
||||
<div
|
||||
@@ -126,24 +142,34 @@ function StatCard({
|
||||
}}
|
||||
/>
|
||||
|
||||
<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 className="relative">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<p className="text-muted font-mono text-xs uppercase tracking-widest-custom">
|
||||
{title}
|
||||
</p>
|
||||
<p className={`font-mono text-3xl font-bold ${colors.value} mt-1`}>
|
||||
{value}
|
||||
</p>
|
||||
{description && (
|
||||
<p className="text-muted text-xs mt-0.5">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Icon - smaller padding */}
|
||||
<div
|
||||
className={`p-2 rounded-lg ${colors.bg} ${colors.glow} transition-all duration-300 group-hover:scale-110`}
|
||||
>
|
||||
<Icon className={`h-5 w-5 ${colors.icon}`} />
|
||||
</div>
|
||||
</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}`} />
|
||||
{/* Simplified view all - no border */}
|
||||
<div className="mt-3 flex items-center justify-between opacity-60 group-hover:opacity-100 transition-opacity">
|
||||
<span className="text-muted text-xs font-mono">View all</span>
|
||||
<ArrowRight
|
||||
className={`h-3 w-3 ${colors.icon} group-hover:translate-x-1 transition-transform`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -168,12 +194,15 @@ function StatusDot({ status }: { status: string }) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Activity list item with hover effects
|
||||
* Activity list item with hover effects - clickable with Link
|
||||
*/
|
||||
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">
|
||||
<Link
|
||||
to={`/agents/${agent.id}`}
|
||||
className="flex items-center justify-between p-2 rounded-lg transition-all duration-200 hover:bg-[rgba(6,182,212,0.08)] group cursor-pointer"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<StatusDot status={agent.status} />
|
||||
<div>
|
||||
<p className="font-mono text-sm font-medium text-primary group-hover:text-cyan transition-colors">
|
||||
@@ -184,10 +213,13 @@ function ActivityItem({ agent }: { agent: Agent }) {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className="font-mono text-xs text-muted tabular-nums">
|
||||
{formatRelativeTime(agent.last_seen)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="font-mono text-xs text-muted tabular-nums">
|
||||
{formatRelativeTime(agent.last_seen)}
|
||||
</span>
|
||||
<ArrowRight className="h-4 w-4 text-muted opacity-0 group-hover:opacity-100 group-hover:text-cyan transition-all duration-200 transform group-hover:translate-x-1" />
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -243,6 +275,10 @@ function ActivityListSkeleton() {
|
||||
|
||||
/**
|
||||
* Main Dashboard component with Mission Control aesthetic
|
||||
*
|
||||
* Stat cards navigate to the Agents page with status filters.
|
||||
* This approach scales better for large agent counts (1000+) since
|
||||
* the Agents page handles pagination, search, and sorting.
|
||||
*/
|
||||
export function Dashboard() {
|
||||
const { data: agents = [], isLoading } = useQuery({
|
||||
@@ -270,12 +306,12 @@ export function Dashboard() {
|
||||
: "status-online";
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-5">
|
||||
{/* Page Header */}
|
||||
<header className="space-y-2 animate-fade-in">
|
||||
<header className="space-y-1 animate-fade-in">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-gradient font-mono text-4xl font-bold tracking-tight">
|
||||
<h1 className="text-gradient font-mono text-3xl font-bold tracking-tight">
|
||||
DASHBOARD
|
||||
</h1>
|
||||
<p className="text-muted text-sm uppercase tracking-widest-custom mt-1">
|
||||
@@ -294,7 +330,7 @@ export function Dashboard() {
|
||||
</header>
|
||||
|
||||
{/* Stat Cards Grid */}
|
||||
<section className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<section className="grid gap-3 md:grid-cols-2 lg:grid-cols-4">
|
||||
{isLoading ? (
|
||||
<>
|
||||
<StatCardSkeleton delay={0.1} />
|
||||
@@ -311,6 +347,7 @@ export function Dashboard() {
|
||||
description="Registered endpoints"
|
||||
accentColor="cyan"
|
||||
delay={0.1}
|
||||
linkTo="/agents"
|
||||
/>
|
||||
<StatCard
|
||||
title="Online"
|
||||
@@ -319,6 +356,7 @@ export function Dashboard() {
|
||||
description="Currently connected"
|
||||
accentColor="green"
|
||||
delay={0.2}
|
||||
linkTo="/agents?status=online"
|
||||
/>
|
||||
<StatCard
|
||||
title="Offline"
|
||||
@@ -327,6 +365,7 @@ export function Dashboard() {
|
||||
description="Not responding"
|
||||
accentColor="amber"
|
||||
delay={0.3}
|
||||
linkTo="/agents?status=offline"
|
||||
/>
|
||||
<StatCard
|
||||
title="Errors"
|
||||
@@ -335,13 +374,14 @@ export function Dashboard() {
|
||||
description="Requires attention"
|
||||
accentColor="rose"
|
||||
delay={0.4}
|
||||
linkTo="/agents?status=error"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Bottom Grid: Activity + Quick Actions */}
|
||||
<section className="grid gap-6 md:grid-cols-2">
|
||||
<section className="grid gap-4 md:grid-cols-2">
|
||||
{/* Recent Activity Card */}
|
||||
<div
|
||||
className="glass-card opacity-0"
|
||||
@@ -349,7 +389,7 @@ export function Dashboard() {
|
||||
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)]">
|
||||
<div className="flex items-center justify-between mb-3 pb-3 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
|
||||
@@ -374,7 +414,7 @@ export function Dashboard() {
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-1 -mx-3">
|
||||
<div className="space-y-0.5 -mx-2">
|
||||
{agents.slice(0, 5).map((agent: Agent) => (
|
||||
<ActivityItem key={agent.id} agent={agent} />
|
||||
))}
|
||||
@@ -389,12 +429,12 @@ export function Dashboard() {
|
||||
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)]">
|
||||
<div className="flex items-center gap-2 mb-3 pb-3 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">
|
||||
<div className="space-y-2">
|
||||
<QuickActionButton
|
||||
icon={Terminal}
|
||||
label="Deploy Agent"
|
||||
@@ -413,7 +453,7 @@ export function Dashboard() {
|
||||
</div>
|
||||
|
||||
{/* Terminal-style hint */}
|
||||
<div className="mt-6 p-3 rounded-lg bg-[var(--bg-primary)] border border-[var(--border-secondary)]">
|
||||
<div className="mt-4 p-2 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">
|
||||
|
||||
Reference in New Issue
Block a user