- Add comprehensive database migrations (001-024) for schema evolution - Enhance API schema with expanded type definitions and resolvers - Add new middleware: audit logging, rate limiting, MFA enforcement, security, tenant auth - Implement new services: AI optimization, billing, blockchain, compliance, marketplace - Add adapter layer for cloud integrations (Cloudflare, Kubernetes, Proxmox, storage) - Update Crossplane provider with enhanced VM management capabilities - Add comprehensive test suite for API endpoints and services - Update frontend components with improved GraphQL subscriptions and real-time updates - Enhance security configurations and headers (CSP, CORS, etc.) - Update documentation and configuration files - Add new CI/CD workflows and validation scripts - Implement design system improvements and UI enhancements
223 lines
8.3 KiB
TypeScript
223 lines
8.3 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import { useQuery } from '@tanstack/react-query'
|
|
import { useSession } from 'next-auth/react'
|
|
import { createCrossplaneClient } from '@/lib/crossplane-client'
|
|
import { Card, CardContent, CardHeader, CardTitle } from '../ui/Card'
|
|
import { Input } from '../ui/Input'
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'
|
|
import { Badge } from '../ui/badge'
|
|
import { Button } from '../ui/Button'
|
|
import { Server, Database, Network, HardDrive } from 'lucide-react'
|
|
|
|
interface CrossplaneResource {
|
|
apiVersion: string
|
|
kind: string
|
|
metadata: {
|
|
name: string
|
|
namespace: string
|
|
uid: string
|
|
creationTimestamp: string
|
|
labels?: Record<string, string>
|
|
}
|
|
spec?: any
|
|
status?: any
|
|
}
|
|
|
|
export default function CrossplaneResourceBrowser() {
|
|
const { data: session } = useSession()
|
|
const [search, setSearch] = useState('')
|
|
const [filterKind, setFilterKind] = useState<string>('all')
|
|
const [filterNamespace, setFilterNamespace] = useState<string>('all')
|
|
|
|
// This would need to be implemented to query Crossplane resources
|
|
// For now, we'll use a placeholder that shows the structure
|
|
const { data: resources = [], isLoading } = useQuery<CrossplaneResource[]>({
|
|
queryKey: ['crossplane-resources', filterKind, filterNamespace],
|
|
queryFn: async () => {
|
|
// In a real implementation, this would query the Kubernetes API
|
|
// for Crossplane managed resources
|
|
const crossplane = createCrossplaneClient(session?.accessToken as string)
|
|
|
|
// Get VMs as an example
|
|
try {
|
|
const vms = await crossplane.getVMs()
|
|
return vms.map((vm) => ({
|
|
apiVersion: process.env.NEXT_PUBLIC_CROSSPLANE_API_GROUP || 'proxmox.sankofa.nexus/v1alpha1',
|
|
kind: 'ProxmoxVM',
|
|
metadata: vm.metadata,
|
|
spec: vm.spec,
|
|
status: vm.status,
|
|
}))
|
|
} catch (error) {
|
|
console.error('Failed to fetch Crossplane resources:', error)
|
|
return []
|
|
}
|
|
},
|
|
enabled: !!session,
|
|
})
|
|
|
|
const getResourceIcon = (kind: string) => {
|
|
if (kind.includes('VM') || kind.includes('VirtualMachine')) {
|
|
return <Server className="h-5 w-5 text-blue-500" />
|
|
}
|
|
if (kind.includes('Storage') || kind.includes('Volume')) {
|
|
return <HardDrive className="h-5 w-5 text-green-500" />
|
|
}
|
|
if (kind.includes('Network')) {
|
|
return <Network className="h-5 w-5 text-purple-500" />
|
|
}
|
|
if (kind.includes('Database')) {
|
|
return <Database className="h-5 w-5 text-yellow-500" />
|
|
}
|
|
return <Server className="h-5 w-5 text-gray-500" />
|
|
}
|
|
|
|
const getResourceStatusColor = (status: any) => {
|
|
if (!status) return 'bg-gray-500'
|
|
|
|
const state = status.state || status.phase || 'Unknown'
|
|
switch (state.toLowerCase()) {
|
|
case 'running':
|
|
case 'ready':
|
|
case 'active':
|
|
return 'bg-green-500'
|
|
case 'pending':
|
|
case 'provisioning':
|
|
return 'bg-yellow-500'
|
|
case 'failed':
|
|
case 'error':
|
|
return 'bg-red-500'
|
|
default:
|
|
return 'bg-gray-500'
|
|
}
|
|
}
|
|
|
|
const filteredResources = resources.filter((resource) => {
|
|
const matchesSearch = resource.metadata.name.toLowerCase().includes(search.toLowerCase())
|
|
const matchesKind = filterKind === 'all' || resource.kind === filterKind
|
|
const matchesNamespace =
|
|
filterNamespace === 'all' || resource.metadata.namespace === filterNamespace
|
|
return matchesSearch && matchesKind && matchesNamespace
|
|
})
|
|
|
|
const uniqueKinds = Array.from(new Set(resources.map((r) => r.kind)))
|
|
const uniqueNamespaces = Array.from(new Set(resources.map((r) => r.metadata.namespace)))
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<h2 className="text-2xl font-bold">Crossplane Resources</h2>
|
|
</div>
|
|
|
|
<div className="flex gap-4">
|
|
<Input
|
|
placeholder="Search resources..."
|
|
value={search}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
className="flex-1"
|
|
/>
|
|
<Select value={filterKind} onValueChange={setFilterKind}>
|
|
<SelectTrigger className="w-[200px]">
|
|
<SelectValue placeholder="Resource Type" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">All Types</SelectItem>
|
|
{uniqueKinds.map((kind) => (
|
|
<SelectItem key={kind} value={kind}>
|
|
{kind}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
<Select value={filterNamespace} onValueChange={setFilterNamespace}>
|
|
<SelectTrigger className="w-[200px]">
|
|
<SelectValue placeholder="Namespace" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">All Namespaces</SelectItem>
|
|
{uniqueNamespaces.map((ns) => (
|
|
<SelectItem key={ns} value={ns}>
|
|
{ns}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{isLoading ? (
|
|
<div className="flex items-center justify-center p-8">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
|
</div>
|
|
) : filteredResources.length === 0 ? (
|
|
<Card>
|
|
<CardContent className="p-8 text-center text-gray-400">
|
|
No Crossplane resources found
|
|
</CardContent>
|
|
</Card>
|
|
) : (
|
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
{filteredResources.map((resource) => (
|
|
<Card key={resource.metadata.uid}>
|
|
<CardHeader>
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
{getResourceIcon(resource.kind)}
|
|
<CardTitle className="text-lg">{resource.metadata.name}</CardTitle>
|
|
</div>
|
|
<div
|
|
className={`h-3 w-3 rounded-full ${getResourceStatusColor(resource.status)}`}
|
|
title={resource.status?.state || 'Unknown'}
|
|
/>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-sm text-gray-500">Kind:</span>
|
|
<Badge>{resource.kind}</Badge>
|
|
</div>
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-sm text-gray-500">Namespace:</span>
|
|
<span className="text-sm">{resource.metadata.namespace}</span>
|
|
</div>
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-sm text-gray-500">API Version:</span>
|
|
<span className="text-sm text-gray-400">{resource.apiVersion}</span>
|
|
</div>
|
|
{resource.status?.state && (
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-sm text-gray-500">State:</span>
|
|
<Badge variant="outline">{resource.status.state}</Badge>
|
|
</div>
|
|
)}
|
|
{resource.status?.ipAddress && (
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-sm text-gray-500">IP Address:</span>
|
|
<span className="text-sm font-mono">{resource.status.ipAddress}</span>
|
|
</div>
|
|
)}
|
|
{resource.metadata.labels && Object.keys(resource.metadata.labels).length > 0 && (
|
|
<div className="mt-2 pt-2 border-t border-gray-700">
|
|
<div className="text-xs text-gray-500 mb-1">Labels:</div>
|
|
<div className="flex flex-wrap gap-1">
|
|
{Object.entries(resource.metadata.labels).map(([key, value]) => (
|
|
<Badge key={key} variant="outline" className="text-xs">
|
|
{key}={value}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|