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
This commit is contained in:
222
portal/src/components/crossplane/CrossplaneResourceBrowser.tsx
Normal file
222
portal/src/components/crossplane/CrossplaneResourceBrowser.tsx
Normal file
@@ -0,0 +1,222 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user