Files
Sankofa/portal/src/components/crossplane/CrossplaneResourceBrowser.tsx
defiQUG 9daf1fd378 Apply Composer changes: comprehensive API updates, migrations, middleware, and infrastructure improvements
- 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
2025-12-12 18:01:35 -08:00

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>
)
}