- 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.
167 lines
5.7 KiB
TypeScript
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>
|
|
);
|
|
}
|
|
|