Files
Sankofa/portal/src/components/Dashboard.tsx
defiQUG 7cd7022f6e Update .gitignore, remove package-lock.json, and enhance Cloudflare and Proxmox adapters
- Added lock file exclusions for pnpm in .gitignore.
- Removed obsolete package-lock.json from the api and portal directories.
- Enhanced Cloudflare adapter with additional interfaces for zones and tunnels.
- Improved Proxmox adapter error handling and logging for API requests.
- Updated Proxmox VM parameters with validation rules in the API schema.
- Enhanced documentation for Proxmox VM specifications and examples.
2025-12-12 19:29:01 -08:00

167 lines
5.7 KiB
TypeScript

'use client';
import { useSession } from 'next-auth/react';
import { useQuery } from '@tanstack/react-query';
import { createCrossplaneClient, VM } from '@/lib/crossplane-client';
import { Card, CardContent, CardHeader, CardTitle } from './ui/Card';
import { Server, Activity, AlertCircle, CheckCircle, Loader2 } from 'lucide-react';
import { Badge } from './ui/badge';
import { gql } from '@apollo/client';
import { useQuery as useApolloQuery } from '@apollo/client';
interface ActivityItem {
id: string;
type: string;
description: string;
timestamp: Date;
}
const GET_RESOURCES = gql`
query GetResources {
resources {
id
name
type
status
}
}
`;
const GET_HEALTH = gql`
query GetHealth {
health {
status
timestamp
}
}
`;
export default function Dashboard() {
const { data: session } = useSession();
const crossplane = createCrossplaneClient(session?.accessToken as string);
const { data: vms = [] } = useQuery({
queryKey: ['vms'],
queryFn: () => crossplane.getVMs(),
});
const { data: resourcesData, loading: resourcesLoading } = useApolloQuery(GET_RESOURCES, {
skip: !session,
});
const { data: healthData, loading: healthLoading } = useApolloQuery(GET_HEALTH, {
skip: !session,
pollInterval: 30000, // Refresh every 30 seconds
});
const resources = resourcesData?.resources || [];
const health = healthData?.health;
const runningVMs = vms.filter((vm: VM) => vm.status?.state === 'running').length;
const stoppedVMs = vms.filter((vm: VM) => vm.status?.state === 'stopped').length;
const totalVMs = vms.length;
// Get recent activity from resources (last 10 created/updated)
const recentActivity: ActivityItem[] = resources
?.slice(0, 10)
.map((resource: any) => ({
id: resource.id,
type: resource.type,
description: `${resource.name} - ${resource.status}`,
timestamp: new Date(resource.updatedAt || resource.createdAt),
}))
.sort((a: ActivityItem, b: ActivityItem) => b.timestamp.getTime() - a.timestamp.getTime()) || [];
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-6">Dashboard</h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total VMs</CardTitle>
<Server className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{isLoading ? '...' : totalVMs}</div>
<p className="text-xs text-muted-foreground">Across all sites</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Running</CardTitle>
<CheckCircle className="h-4 w-4 text-green-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{isLoading ? '...' : runningVMs}</div>
<p className="text-xs text-muted-foreground">Active virtual machines</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Stopped</CardTitle>
<AlertCircle className="h-4 w-4 text-yellow-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{isLoading ? '...' : stoppedVMs}</div>
<p className="text-xs text-muted-foreground">Inactive virtual machines</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">System Health</CardTitle>
<Activity className="h-4 w-4 text-blue-500" />
</CardHeader>
<CardContent>
{healthLoading ? (
<Loader2 className="h-6 w-6 animate-spin" />
) : (
<>
<div className="text-2xl font-bold">
{health?.status === 'ok' ? 'Healthy' : health?.status || 'Unknown'}
</div>
<p className="text-xs text-muted-foreground">
{health?.status === 'ok' ? 'All systems operational' : 'Checking system status...'}
</p>
</>
)}
</CardContent>
</Card>
</div>
<Card>
<CardHeader>
<CardTitle>Recent Activity</CardTitle>
</CardHeader>
<CardContent>
{resourcesLoading ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
) : recentActivity.length === 0 ? (
<p className="text-sm text-muted-foreground">No recent activity</p>
) : (
<div className="space-y-3">
{recentActivity.map((activity) => (
<div key={activity.id} className="flex items-center justify-between border-b pb-2">
<div>
<p className="text-sm font-medium">{activity.description}</p>
<p className="text-xs text-muted-foreground">
{activity.timestamp.toLocaleString()}
</p>
</div>
<Badge variant="outline">{activity.type}</Badge>
</div>
))}
</div>
)}
</CardContent>
</Card>
</div>
);
}