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:
defiQUG
2025-12-12 18:01:35 -08:00
parent e01131efaf
commit 9daf1fd378
968 changed files with 160890 additions and 1092 deletions

View File

@@ -4,20 +4,80 @@ import { useSession } from 'next-auth/react';
import { useQuery } from '@tanstack/react-query';
import { createCrossplaneClient } from '@/lib/crossplane-client';
import { Card, CardContent, CardHeader, CardTitle } from './ui/Card';
import { Server, Activity, AlertCircle, CheckCircle } from 'lucide-react';
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 VM {
id: string;
status?: {
state?: string;
};
}
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 = [], isLoading } = useQuery({
const { data: vms = [], isLoading: vmsLoading } = useQuery<VM[]>({
queryKey: ['vms'],
queryFn: () => crossplane.getVMs(),
});
const runningVMs = vms.filter((vm: any) => vm.status?.state === 'running').length;
const stoppedVMs = vms.filter((vm: any) => vm.status?.state === 'stopped').length;
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">
@@ -63,8 +123,18 @@ export default function Dashboard() {
<Activity className="h-4 w-4 text-blue-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">Healthy</div>
<p className="text-xs text-muted-foreground">All systems operational</p>
{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>
@@ -74,9 +144,27 @@ export default function Dashboard() {
<CardTitle>Recent Activity</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Activity feed will be displayed here
</p>
{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>

View File

@@ -0,0 +1,54 @@
'use client';
import { X } from 'lucide-react';
interface Shortcut {
keys: string[];
description: string;
}
const shortcuts: Shortcut[] = [
{ keys: ['/'], description: 'Focus search' },
{ keys: ['Ctrl', 'G'], description: 'Go to dashboard' },
{ keys: ['?'], description: 'Show keyboard shortcuts' },
{ keys: ['Esc'], description: 'Close modal/dialog' },
];
export function KeyboardShortcutsHelp() {
return (
<dialog id="keyboard-shortcuts-help" className="modal">
<div className="modal-box bg-gray-800 border border-gray-700">
<div className="flex items-center justify-between mb-4">
<h3 className="text-xl font-bold text-white">Keyboard Shortcuts</h3>
<form method="dialog">
<button className="btn btn-sm btn-circle btn-ghost text-gray-400 hover:text-white">
<X className="h-4 w-4" />
</button>
</form>
</div>
<div className="space-y-3">
{shortcuts.map((shortcut, index) => (
<div key={index} className="flex items-center justify-between p-2 bg-gray-900 rounded">
<span className="text-sm text-gray-300">{shortcut.description}</span>
<div className="flex gap-1">
{shortcut.keys.map((key, keyIndex) => (
<kbd
key={keyIndex}
className="px-2 py-1 text-xs font-semibold text-gray-300 bg-gray-700 border border-gray-600 rounded"
>
{key}
</kbd>
))}
</div>
</div>
))}
</div>
</div>
<form method="dialog" className="modal-backdrop">
<button>close</button>
</form>
</dialog>
);
}

View File

@@ -0,0 +1,17 @@
'use client';
import { ReactNode } from 'react';
import { useGlobalKeyboardShortcuts } from '@/hooks/useKeyboardShortcuts';
import { KeyboardShortcutsHelp } from './KeyboardShortcutsHelp';
export function KeyboardShortcutsProvider({ children }: { children: ReactNode }) {
useGlobalKeyboardShortcuts();
return (
<>
{children}
<KeyboardShortcutsHelp />
</>
);
}

View File

@@ -0,0 +1,177 @@
'use client'
import { useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Badge } from '@/components/ui/badge'
interface Resource {
id: string
name: string
type: string
provider: string
region: string
status: string
tags: string[]
}
export function ResourceExplorer() {
const [search, setSearch] = useState('')
const [filterProvider, setFilterProvider] = useState<string>('all')
const [filterType, setFilterType] = useState<string>('all')
const { data: resources, isLoading } = useQuery<Resource[]>({
queryKey: ['resources', filterProvider, filterType],
queryFn: async () => {
const graphqlEndpoint = process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT || 'http://localhost:4000/graphql'
const query = `
query GetResourceInventory($filter: ResourceInventoryFilter) {
resourceInventory(filter: $filter) {
id
name
resourceType
provider
region
tags
metadata
}
}
`
const variables: any = {}
if (filterProvider !== 'all') {
variables.provider = filterProvider
}
if (filterType !== 'all') {
variables.resourceType = filterType
}
const response = await fetch(graphqlEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query,
variables: Object.keys(variables).length > 0 ? { filter: variables } : undefined,
}),
})
if (!response.ok) {
throw new Error(`Failed to fetch resources: ${response.statusText}`)
}
const result = await response.json()
if (result.errors) {
throw new Error(result.errors[0]?.message || 'GraphQL error')
}
// Transform GraphQL response to component format
return (result.data?.resourceInventory || []).map((item: any) => ({
id: item.id,
name: item.name,
type: item.resourceType,
provider: item.provider,
region: item.region || 'Unknown',
status: item.metadata?.status || 'UNKNOWN',
tags: item.tags || [],
}))
},
})
const filteredResources = resources?.filter((resource) => {
const matchesSearch = resource.name.toLowerCase().includes(search.toLowerCase())
const matchesProvider = filterProvider === 'all' || resource.provider === filterProvider
const matchesType = filterType === 'all' || resource.type === filterType
return matchesSearch && matchesProvider && matchesType
})
return (
<div className="space-y-4">
<div className="flex gap-4">
<Input
placeholder="Search resources..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="flex-1"
/>
<Select value={filterProvider} onValueChange={setFilterProvider}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Provider" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Providers</SelectItem>
<SelectItem value="PROXMOX">Proxmox</SelectItem>
<SelectItem value="KUBERNETES">Kubernetes</SelectItem>
<SelectItem value="CLOUDFLARE">Cloudflare</SelectItem>
</SelectContent>
</Select>
<Select value={filterType} onValueChange={setFilterType}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Types</SelectItem>
<SelectItem value="VM">VM</SelectItem>
<SelectItem value="CONTAINER">Container</SelectItem>
<SelectItem value="STORAGE">Storage</SelectItem>
</SelectContent>
</Select>
</div>
{isLoading ? (
<div>Loading resources...</div>
) : (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{filteredResources?.map((resource) => (
<Card key={resource.id}>
<CardHeader>
<CardTitle className="text-lg">{resource.name}</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm text-gray-500">Provider:</span>
<Badge>{resource.provider}</Badge>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-gray-500">Type:</span>
<Badge variant="outline">{resource.type}</Badge>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-gray-500">Region:</span>
<span className="text-sm">{resource.region}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-gray-500">Status:</span>
<Badge
variant={
resource.status === 'RUNNING' ? 'default' : 'secondary'
}
>
{resource.status}
</Badge>
</div>
{resource.tags && resource.tags.length > 0 && (
<div className="flex flex-wrap gap-1 mt-2">
{resource.tags.map((tag) => (
<Badge key={tag} variant="outline" className="text-xs">
{tag}
</Badge>
))}
</div>
)}
</div>
</CardContent>
</Card>
))}
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,96 @@
'use client'
import { useQuery } from '@tanstack/react-query'
import axios from 'axios'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Server, Play, Pause, Trash2 } from 'lucide-react'
interface VM {
id: string
name: string
status: string
cpu: number
memory: number
disk: number
}
export function VMList() {
const { data: vms, isLoading } = useQuery<VM[]>({
queryKey: ['vms'],
queryFn: async () => {
// Use Crossplane client to get VMs
const { createCrossplaneClient } = await import('@/lib/crossplane-client')
const { useSession } = await import('next-auth/react')
// Get session token if available
const session = typeof window !== 'undefined'
? await import('next-auth/react').then(m => {
// This is a workaround - in a real component we'd use the hook
// For now, we'll use the client without auth or get token from storage
return null
})
: null
const client = createCrossplaneClient()
const vms = await client.getVMs()
// Transform Crossplane VM format to component format
return vms.map((vm: any) => ({
id: vm.metadata?.name || vm.metadata?.uid || '',
name: vm.metadata?.name || 'Unknown',
status: vm.status?.state || 'unknown',
cpu: vm.spec?.forProvider?.cpu || 0,
memory: vm.spec?.forProvider?.memory || 0,
disk: vm.spec?.forProvider?.disk || 0,
}))
},
})
if (isLoading) return <div>Loading VMs...</div>
return (
<div className="space-y-4">
{vms?.map((vm) => (
<Card key={vm.id}>
<CardHeader>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Server className="h-5 w-5" />
<div>
<CardTitle>{vm.name}</CardTitle>
<p className="text-sm text-gray-500">Status: {vm.status}</p>
</div>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm">
<Play className="h-4 w-4" />
</Button>
<Button variant="outline" size="sm">
<Pause className="h-4 w-4" />
</Button>
<Button variant="outline" size="sm">
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
</CardHeader>
<CardContent>
<div className="grid grid-cols-3 gap-4 text-sm">
<div>
<span className="text-gray-500">CPU:</span> {vm.cpu} cores
</div>
<div>
<span className="text-gray-500">Memory:</span> {vm.memory} GB
</div>
<div>
<span className="text-gray-500">Disk:</span> {vm.disk} GB
</div>
</div>
</CardContent>
</Card>
))}
</div>
)
}

View File

@@ -0,0 +1,94 @@
'use client';
import { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
import { Sparkles, TrendingDown, TrendingUp, AlertCircle } from 'lucide-react';
export function OptimizationEngine() {
const [recommendations, setRecommendations] = useState([
{
id: '1',
type: 'cost',
title: 'Right-size underutilized VMs',
description: '3 VMs are running at less than 20% capacity',
savings: '$450/month',
impact: 'high',
action: 'Resize VMs',
},
{
id: '2',
type: 'performance',
title: 'Enable auto-scaling for web tier',
description: 'Web tier experiencing traffic spikes',
improvement: '30% better response time',
impact: 'medium',
action: 'Configure auto-scaling',
},
{
id: '3',
type: 'security',
title: 'Update security policies',
description: '5 resources have outdated security policies',
impact: 'high',
action: 'Review policies',
},
]);
return (
<Card className="bg-gray-800 border-gray-700">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<Sparkles className="h-5 w-5 text-orange-500" />
AI-Powered Optimization Recommendations
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{recommendations.map((rec) => (
<div
key={rec.id}
className="p-4 bg-gray-900 rounded-lg border border-gray-700"
>
<div className="flex items-start justify-between mb-2">
<div>
<h4 className="text-white font-semibold mb-1">{rec.title}</h4>
<p className="text-sm text-gray-400">{rec.description}</p>
</div>
<span
className={`px-2 py-1 text-xs rounded ${
rec.impact === 'high'
? 'bg-red-500/20 text-red-400'
: rec.impact === 'medium'
? 'bg-yellow-500/20 text-yellow-400'
: 'bg-blue-500/20 text-blue-400'
}`}
>
{rec.impact} impact
</span>
</div>
{rec.savings && (
<div className="flex items-center gap-2 text-green-400 mb-2">
<TrendingDown className="h-4 w-4" />
<span className="text-sm font-semibold">{rec.savings}</span>
</div>
)}
{rec.improvement && (
<div className="flex items-center gap-2 text-blue-400 mb-2">
<TrendingUp className="h-4 w-4" />
<span className="text-sm font-semibold">{rec.improvement}</span>
</div>
)}
<button className="mt-2 px-4 py-2 bg-orange-500 text-white rounded hover:bg-orange-600 transition-colors text-sm">
{rec.action}
</button>
</div>
))}
</div>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,76 @@
'use client';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
import { BarChart3, TrendingUp, Users, DollarSign } from 'lucide-react';
export function AdvancedAnalytics() {
return (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card className="bg-gray-800 border-gray-700">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-400">Total Revenue</p>
<p className="text-2xl font-bold text-white">$125,430</p>
<p className="text-xs text-green-400 mt-1">+12.5% from last month</p>
</div>
<DollarSign className="h-8 w-8 text-green-400" />
</div>
</CardContent>
</Card>
<Card className="bg-gray-800 border-gray-700">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-400">Active Users</p>
<p className="text-2xl font-bold text-white">2,450</p>
<p className="text-xs text-blue-400 mt-1">+8.2% from last month</p>
</div>
<Users className="h-8 w-8 text-blue-400" />
</div>
</CardContent>
</Card>
<Card className="bg-gray-800 border-gray-700">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-400">API Requests</p>
<p className="text-2xl font-bold text-white">12.5M</p>
<p className="text-xs text-purple-400 mt-1">+15.3% from last month</p>
</div>
<BarChart3 className="h-8 w-8 text-purple-400" />
</div>
</CardContent>
</Card>
<Card className="bg-gray-800 border-gray-700">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-400">Growth Rate</p>
<p className="text-2xl font-bold text-white">18.7%</p>
<p className="text-xs text-orange-400 mt-1">+2.1% from last month</p>
</div>
<TrendingUp className="h-8 w-8 text-orange-400" />
</div>
</CardContent>
</Card>
</div>
<Card className="bg-gray-800 border-gray-700">
<CardHeader>
<CardTitle className="text-white">Analytics Dashboard</CardTitle>
</CardHeader>
<CardContent>
<div className="h-64 bg-gray-900 rounded flex items-center justify-center">
<p className="text-gray-400">Advanced charts and visualizations would appear here</p>
</div>
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,165 @@
'use client'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { useSession } from 'next-auth/react'
import { createArgoCDClient, type ArgoCDApplication } from '@/lib/argocd-client'
import { Card, CardContent, CardHeader, CardTitle } from '../ui/Card'
import { Button } from '../ui/Button'
import { CheckCircle, XCircle, AlertCircle, RefreshCw, GitBranch } from 'lucide-react'
export default function ArgoCDApplications() {
const { data: session } = useSession()
const queryClient = useQueryClient()
const argocd = createArgoCDClient(session?.accessToken as string)
const { data: applications = [], isLoading } = useQuery({
queryKey: ['argocd-applications'],
queryFn: () => argocd.getApplications(),
refetchInterval: 30000, // Refresh every 30 seconds
})
const syncMutation = useMutation({
mutationFn: ({ name, namespace }: { name: string; namespace: string }) =>
argocd.syncApplication(name, namespace),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['argocd-applications'] })
},
})
const getHealthIcon = (status: string) => {
switch (status) {
case 'Healthy':
return <CheckCircle className="h-5 w-5 text-green-500" />
case 'Degraded':
case 'Missing':
return <XCircle className="h-5 w-5 text-red-500" />
case 'Progressing':
return <AlertCircle className="h-5 w-5 text-yellow-500" />
default:
return <AlertCircle className="h-5 w-5 text-gray-500" />
}
}
const getSyncStatusColor = (status: string) => {
switch (status) {
case 'Synced':
return 'text-green-500'
case 'OutOfSync':
return 'text-yellow-500'
default:
return 'text-gray-500'
}
}
if (isLoading) {
return (
<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>
)
}
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold">ArgoCD Applications</h2>
<Button
onClick={() => queryClient.invalidateQueries({ queryKey: ['argocd-applications'] })}
variant="outline"
>
<RefreshCw className="h-4 w-4 mr-2" />
Refresh
</Button>
</div>
{applications.length === 0 ? (
<Card>
<CardContent className="p-8 text-center text-gray-400">
No ArgoCD applications found
</CardContent>
</Card>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{applications.map((app: ArgoCDApplication) => (
<Card key={app.metadata.uid}>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="text-lg">{app.metadata.name}</CardTitle>
{getHealthIcon(app.status.health.status)}
</div>
</CardHeader>
<CardContent>
<div className="space-y-3">
<div>
<div className="text-sm text-gray-400">Namespace</div>
<div className="text-sm font-medium">{app.metadata.namespace}</div>
</div>
<div>
<div className="text-sm text-gray-400">Source</div>
<div className="text-sm font-medium flex items-center gap-2">
<GitBranch className="h-4 w-4" />
{app.spec.source.repoURL}
</div>
{app.spec.source.path && (
<div className="text-xs text-gray-500 mt-1">
Path: {app.spec.source.path}
</div>
)}
</div>
<div>
<div className="text-sm text-gray-400">Sync Status</div>
<div className={`text-sm font-medium ${getSyncStatusColor(app.status.sync.status)}`}>
{app.status.sync.status}
</div>
{app.status.sync.revision && (
<div className="text-xs text-gray-500 mt-1">
Revision: {app.status.sync.revision.substring(0, 7)}
</div>
)}
</div>
<div>
<div className="text-sm text-gray-400">Health</div>
<div className="text-sm font-medium">{app.status.health.status}</div>
{app.status.health.message && (
<div className="text-xs text-gray-500 mt-1">
{app.status.health.message}
</div>
)}
</div>
<Button
onClick={() =>
syncMutation.mutate({
name: app.metadata.name,
namespace: app.metadata.namespace,
})
}
disabled={syncMutation.isPending}
variant="outline"
className="w-full"
>
{syncMutation.isPending ? (
<>
<RefreshCw className="h-4 w-4 mr-2 animate-spin" />
Syncing...
</>
) : (
<>
<RefreshCw className="h-4 w-4 mr-2" />
Sync
</>
)}
</Button>
</div>
</CardContent>
</Card>
))}
</div>
)}
</div>
)
}

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

View File

@@ -0,0 +1,63 @@
'use client';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
import { Key, AlertCircle } from 'lucide-react';
import Link from 'next/link';
export function APIKeysTile() {
// Mock data - in production, this would come from API
const apiKeys = {
active: 5,
expired: 0,
expiringSoon: 1,
};
return (
<Card className="bg-gray-800 border-gray-700">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<Key className="h-5 w-5 text-orange-500" />
API Keys & Credentials
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="text-center">
<p className="text-4xl font-bold text-white mb-2">{apiKeys.active}</p>
<p className="text-sm text-gray-400">Active API Keys</p>
</div>
{apiKeys.expiringSoon > 0 && (
<div className="p-3 bg-yellow-500/10 border border-yellow-500/20 rounded-lg">
<div className="flex items-center gap-2">
<AlertCircle className="h-4 w-4 text-yellow-400" />
<span className="text-sm text-yellow-400 font-semibold">
{apiKeys.expiringSoon} key{apiKeys.expiringSoon > 1 ? 's' : ''} expiring soon
</span>
</div>
</div>
)}
{apiKeys.expired > 0 && (
<div className="p-3 bg-red-500/10 border border-red-500/20 rounded-lg">
<div className="flex items-center gap-2">
<AlertCircle className="h-4 w-4 text-red-400" />
<span className="text-sm text-red-400 font-semibold">
{apiKeys.expired} expired key{apiKeys.expired > 1 ? 's' : ''}
</span>
</div>
</div>
)}
<Link
href="/developer/api-keys"
className="block text-center text-sm text-orange-500 hover:text-orange-400 transition-colors"
>
Manage API Keys
</Link>
</div>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,58 @@
'use client';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
import { Activity, AlertCircle } from 'lucide-react';
export function APIUsageTile() {
// Mock data - in production, this would come from API
const usage = {
requests: { current: 1250000, limit: 2000000, percentage: 62.5 },
quota: { used: 62.5, remaining: 37.5 },
};
const isNearLimit = usage.quota.used > 80;
return (
<Card className="bg-gray-800 border-gray-700">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<Activity className="h-5 w-5 text-orange-500" />
API Usage & Quotas
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div>
<div className="flex items-center justify-between mb-2">
<span className="text-sm text-gray-400">Requests (this month)</span>
<span className="text-sm font-semibold text-white">
{usage.requests.current.toLocaleString()} / {usage.requests.limit.toLocaleString()}
</span>
</div>
<div className="w-full bg-gray-900 rounded-full h-3">
<div
className={`h-3 rounded-full ${isNearLimit ? 'bg-red-500' : 'bg-orange-500'}`}
style={{ width: `${usage.quota.used}%` }}
/>
</div>
<p className="text-xs text-gray-400 mt-1">
{usage.quota.remaining}% remaining
</p>
</div>
{isNearLimit && (
<div className="p-3 bg-yellow-500/10 border border-yellow-500/20 rounded-lg">
<div className="flex items-center gap-2">
<AlertCircle className="h-4 w-4 text-yellow-400" />
<span className="text-sm text-yellow-400 font-semibold">
Approaching quota limit
</span>
</div>
</div>
)}
</div>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,67 @@
'use client';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
import { CreditCard, FileText } from 'lucide-react';
import Link from 'next/link';
export function BillingTile() {
// Mock data - in production, this would come from API
const billing = {
currentInvoice: 45230,
nextBillingDate: '2024-02-15',
paymentMethod: 'Credit Card ending in 4242',
invoicesPending: 0,
};
return (
<Card className="bg-gray-800 border-gray-700">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<CreditCard className="h-5 w-5 text-orange-500" />
Billing & Invoices
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div>
<p className="text-2xl font-bold text-white mb-1">
${billing.currentInvoice.toLocaleString()}
</p>
<p className="text-sm text-gray-400">Current Invoice</p>
</div>
<div className="p-3 bg-gray-900 rounded-lg">
<p className="text-xs text-gray-400 mb-1">Next Billing Date</p>
<p className="text-sm font-semibold text-white">
{new Date(billing.nextBillingDate).toLocaleDateString()}
</p>
</div>
<div className="p-3 bg-gray-900 rounded-lg">
<p className="text-xs text-gray-400 mb-1">Payment Method</p>
<p className="text-sm text-white">{billing.paymentMethod}</p>
</div>
{billing.invoicesPending > 0 && (
<div className="p-3 bg-yellow-500/10 border border-yellow-500/20 rounded-lg">
<div className="flex items-center gap-2">
<FileText className="h-4 w-4 text-yellow-400" />
<span className="text-sm text-yellow-400 font-semibold">
{billing.invoicesPending} invoice{billing.invoicesPending > 1 ? 's' : ''} pending
</span>
</div>
</div>
)}
<Link
href="/admin/billing"
className="block text-center text-sm text-orange-500 hover:text-orange-400 transition-colors"
>
View All Invoices
</Link>
</div>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,60 @@
'use client';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
import { Shield, CheckCircle, AlertCircle } from 'lucide-react';
export function ComplianceStatusTile() {
// Mock data - in production, this would come from API
const compliance = {
soc2: { status: 'compliant', lastAudit: '2024-01-15' },
iso27001: { status: 'compliant', lastAudit: '2024-01-10' },
gdpr: { status: 'compliant', lastAudit: '2024-01-20' },
hipaa: { status: 'pending', lastAudit: null },
};
const getStatusIcon = (status: string) => {
if (status === 'compliant') {
return <CheckCircle className="h-4 w-4 text-green-400" />;
}
return <AlertCircle className="h-4 w-4 text-yellow-400" />;
};
const getStatusColor = (status: string) => {
if (status === 'compliant') return 'text-green-400';
return 'text-yellow-400';
};
return (
<Card className="bg-gray-800 border-gray-700">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<Shield className="h-5 w-5 text-orange-500" />
Compliance Status
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
{Object.entries(compliance).map(([key, value]) => (
<div key={key} className="p-3 bg-gray-900 rounded-lg">
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-2">
{getStatusIcon(value.status)}
<span className="text-sm text-gray-300 uppercase">{key}</span>
</div>
<span className={`text-xs font-semibold ${getStatusColor(value.status)}`}>
{value.status}
</span>
</div>
{value.lastAudit && (
<p className="text-xs text-gray-400 mt-1">
Last audit: {new Date(value.lastAudit).toLocaleDateString()}
</p>
)}
</div>
))}
</div>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,84 @@
'use client';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
import { TrendingUp, TrendingDown, DollarSign } from 'lucide-react';
export function CostForecastingTile() {
// Mock data - in production, this would come from API
const forecast = {
currentMonth: 45230,
nextMonth: 48500,
threeMonths: 52000,
sixMonths: 58000,
trend: 'up',
confidence: 85,
};
const TrendIcon = forecast.trend === 'up' ? TrendingUp : TrendingDown;
const trendColor = forecast.trend === 'up' ? 'text-red-400' : 'text-green-400';
return (
<Card className="bg-gray-800 border-gray-700">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<DollarSign className="h-5 w-5 text-orange-500" />
Cost Forecasting
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div>
<p className="text-sm text-gray-400 mb-1">Current Month</p>
<p className="text-2xl font-bold text-white">
${forecast.currentMonth.toLocaleString()}
</p>
</div>
<div className="space-y-3">
<div className="p-3 bg-gray-900 rounded-lg">
<div className="flex items-center justify-between mb-1">
<span className="text-sm text-gray-400">Next Month</span>
<div className="flex items-center gap-1">
<TrendIcon className={`h-4 w-4 ${trendColor}`} />
<span className={`text-sm font-semibold ${trendColor}`}>
{((forecast.nextMonth - forecast.currentMonth) / forecast.currentMonth * 100).toFixed(1)}%
</span>
</div>
</div>
<p className="text-lg font-semibold text-white">
${forecast.nextMonth.toLocaleString()}
</p>
</div>
<div className="p-3 bg-gray-900 rounded-lg">
<div className="flex items-center justify-between mb-1">
<span className="text-sm text-gray-400">3 Months</span>
<span className="text-sm text-gray-400">{forecast.confidence}% confidence</span>
</div>
<p className="text-lg font-semibold text-white">
${forecast.threeMonths.toLocaleString()}
</p>
</div>
<div className="p-3 bg-gray-900 rounded-lg">
<div className="flex items-center justify-between mb-1">
<span className="text-sm text-gray-400">6 Months</span>
<span className="text-sm text-gray-400">{forecast.confidence}% confidence</span>
</div>
<p className="text-lg font-semibold text-white">
${forecast.sixMonths.toLocaleString()}
</p>
</div>
</div>
<div className="pt-2 border-t border-gray-700">
<p className="text-xs text-gray-400">
Forecast based on historical usage patterns and trends
</p>
</div>
</div>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,53 @@
'use client';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
import { DollarSign, TrendingUp, TrendingDown } from 'lucide-react';
export function CostOverviewTile() {
// Mock data - in production, this would come from API
const costData = {
currentMonth: 45230,
lastMonth: 38950,
trend: 'up',
percentageChange: 16.1,
};
const trend = costData.trend === 'up' ? TrendingUp : TrendingDown;
const TrendIcon = trend;
const trendColor = costData.trend === 'up' ? 'text-red-400' : 'text-green-400';
return (
<Card className="bg-gray-800 border-gray-700">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<DollarSign className="h-5 w-5 text-orange-500" />
Cost Overview
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div>
<p className="text-3xl font-bold text-white mb-1">
${costData.currentMonth.toLocaleString()}
</p>
<p className="text-sm text-gray-400">Current Month</p>
</div>
<div className="flex items-center gap-2 p-3 bg-gray-900 rounded-lg">
<TrendIcon className={`h-4 w-4 ${trendColor}`} />
<span className={`text-sm font-semibold ${trendColor}`}>
{costData.percentageChange}% vs last month
</span>
</div>
<div className="pt-2 border-t border-gray-700">
<p className="text-xs text-gray-400">
Last month: <span className="text-white font-semibold">${costData.lastMonth.toLocaleString()}</span>
</p>
</div>
</div>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,131 @@
'use client';
import { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
import { GripVertical, X, Plus } from 'lucide-react';
import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd';
interface DashboardTile {
id: string;
component: string;
title: string;
visible: boolean;
}
interface DashboardCustomizerProps {
tiles: DashboardTile[];
onTilesChange: (tiles: DashboardTile[]) => void;
availableTiles: Array<{ id: string; title: string; component: string }>;
}
export function DashboardCustomizer({ tiles, onTilesChange, availableTiles }: DashboardCustomizerProps) {
const [isEditing, setIsEditing] = useState(false);
const onDragEnd = (result: DropResult) => {
if (!result.destination) return;
const newTiles = Array.from(tiles);
const [reorderedItem] = newTiles.splice(result.source.index, 1);
newTiles.splice(result.destination.index, 0, reorderedItem);
onTilesChange(newTiles);
};
const toggleTileVisibility = (tileId: string) => {
const newTiles = tiles.map(tile =>
tile.id === tileId ? { ...tile, visible: !tile.visible } : tile
);
onTilesChange(newTiles);
};
const addTile = (tileId: string) => {
const tile = availableTiles.find(t => t.id === tileId);
if (tile && !tiles.find(t => t.id === tileId)) {
onTilesChange([...tiles, { ...tile, visible: true }]);
}
};
return (
<div className="mb-4">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-bold text-white">Customize Dashboard</h2>
<button
onClick={() => setIsEditing(!isEditing)}
className="px-4 py-2 bg-orange-500 text-white rounded hover:bg-orange-600 transition-colors"
>
{isEditing ? 'Done' : 'Customize'}
</button>
</div>
{isEditing && (
<Card className="bg-gray-800 border-gray-700 mb-4">
<CardHeader>
<CardTitle className="text-white">Available Tiles</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
{availableTiles
.filter(t => !tiles.find(ct => ct.id === t.id))
.map(tile => (
<button
key={tile.id}
onClick={() => addTile(tile.id)}
className="p-3 bg-gray-900 rounded hover:bg-gray-700 transition-colors text-left"
>
<div className="flex items-center gap-2">
<Plus className="h-4 w-4 text-orange-500" />
<span className="text-sm text-white">{tile.title}</span>
</div>
</button>
))}
</div>
</CardContent>
</Card>
)}
{isEditing && (
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="dashboard-tiles">
{(provided) => (
<div {...provided.droppableProps} ref={provided.innerRef}>
{tiles.map((tile, index) => (
<Draggable key={tile.id} draggableId={tile.id} index={index}>
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
className={`mb-2 p-3 bg-gray-900 rounded flex items-center justify-between ${
snapshot.isDragging ? 'opacity-50' : ''
}`}
>
<div className="flex items-center gap-3">
<div {...provided.dragHandleProps}>
<GripVertical className="h-5 w-5 text-gray-400" />
</div>
<span className="text-white">{tile.title}</span>
</div>
<div className="flex items-center gap-2">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={tile.visible}
onChange={() => toggleTileVisibility(tile.id)}
className="w-4 h-4"
/>
<span className="text-sm text-gray-300">Visible</span>
</label>
</div>
</div>
)}
</Draggable>
))}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
)}
</div>
);
}

View File

@@ -0,0 +1,62 @@
'use client';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
import { GitBranch, PlayCircle, PauseCircle, AlertCircle } from 'lucide-react';
export function DataPipelineTile() {
// Mock data - in production, this would come from API
const pipelines = {
running: 12,
failed: 1,
pending: 3,
total: 16,
};
return (
<Card className="bg-gray-800 border-gray-700">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<GitBranch className="h-5 w-5 text-orange-500" />
Data Pipeline Status
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="p-3 bg-gray-900 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<PlayCircle className="h-4 w-4 text-green-400" />
<span className="text-sm text-gray-400">Running</span>
</div>
<p className="text-2xl font-bold text-white">{pipelines.running}</p>
</div>
<div className="p-3 bg-gray-900 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<PauseCircle className="h-4 w-4 text-yellow-400" />
<span className="text-sm text-gray-400">Pending</span>
</div>
<p className="text-2xl font-bold text-white">{pipelines.pending}</p>
</div>
</div>
{pipelines.failed > 0 && (
<div className="p-3 bg-red-500/10 border border-red-500/20 rounded-lg">
<div className="flex items-center gap-2">
<AlertCircle className="h-4 w-4 text-red-400" />
<span className="text-sm text-red-400 font-semibold">
{pipelines.failed} pipeline{pipelines.failed > 1 ? 's' : ''} failed
</span>
</div>
</div>
)}
<div className="pt-2 border-t border-gray-700">
<p className="text-xs text-gray-400">Total Pipelines: <span className="text-white font-semibold">{pipelines.total}</span></p>
</div>
</div>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,64 @@
'use client';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
import { Rocket, CheckCircle, Clock, XCircle } from 'lucide-react';
export function DeploymentsTile() {
// Mock data - in production, this would come from API
const deployments = {
active: 8,
syncing: 2,
failed: 0,
total: 10,
};
return (
<Card className="bg-gray-800 border-gray-700">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<Rocket className="h-5 w-5 text-orange-500" />
Active Deployments
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="text-center">
<p className="text-4xl font-bold text-white mb-2">{deployments.total}</p>
<p className="text-sm text-gray-400">Total Deployments</p>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between p-2 bg-gray-900 rounded">
<div className="flex items-center gap-2">
<CheckCircle className="h-4 w-4 text-green-400" />
<span className="text-sm text-gray-300">Synced</span>
</div>
<span className="text-sm font-semibold text-white">{deployments.active}</span>
</div>
{deployments.syncing > 0 && (
<div className="flex items-center justify-between p-2 bg-gray-900 rounded">
<div className="flex items-center gap-2">
<Clock className="h-4 w-4 text-yellow-400" />
<span className="text-sm text-gray-300">Syncing</span>
</div>
<span className="text-sm font-semibold text-white">{deployments.syncing}</span>
</div>
)}
{deployments.failed > 0 && (
<div className="flex items-center justify-between p-2 bg-gray-900 rounded">
<div className="flex items-center gap-2">
<XCircle className="h-4 w-4 text-red-400" />
<span className="text-sm text-gray-300">Failed</span>
</div>
<span className="text-sm font-semibold text-white">{deployments.failed}</span>
</div>
)}
</div>
</div>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,64 @@
'use client';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
import { Link2, CheckCircle, AlertCircle, Clock } from 'lucide-react';
export function IntegrationStatusTile() {
// Mock data - in production, this would come from API
const integrations = {
total: 45,
active: 42,
pending: 2,
failed: 1,
};
return (
<Card className="bg-gray-800 border-gray-700">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<Link2 className="h-5 w-5 text-orange-500" />
Active Integrations
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="text-center">
<p className="text-4xl font-bold text-white mb-2">{integrations.total}</p>
<p className="text-sm text-gray-400">Total Integrations</p>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between p-2 bg-gray-900 rounded">
<div className="flex items-center gap-2">
<CheckCircle className="h-4 w-4 text-green-400" />
<span className="text-sm text-gray-300">Active</span>
</div>
<span className="text-sm font-semibold text-white">{integrations.active}</span>
</div>
{integrations.pending > 0 && (
<div className="flex items-center justify-between p-2 bg-gray-900 rounded">
<div className="flex items-center gap-2">
<Clock className="h-4 w-4 text-yellow-400" />
<span className="text-sm text-gray-300">Pending</span>
</div>
<span className="text-sm font-semibold text-white">{integrations.pending}</span>
</div>
)}
{integrations.failed > 0 && (
<div className="flex items-center justify-between p-2 bg-gray-900 rounded">
<div className="flex items-center gap-2">
<AlertCircle className="h-4 w-4 text-red-400" />
<span className="text-sm text-gray-300">Failed</span>
</div>
<span className="text-sm font-semibold text-white">{integrations.failed}</span>
</div>
)}
</div>
</div>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,35 @@
'use client';
import { Suspense, lazy, ComponentType } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
import { Loader2 } from 'lucide-react';
interface LazyTileProps {
component: ComponentType<any>;
props?: any;
fallback?: React.ReactNode;
}
export function LazyTile({ component: Component, props, fallback }: LazyTileProps) {
const LazyComponent = lazy(() => Promise.resolve({ default: Component }));
const defaultFallback = (
<Card className="bg-gray-800 border-gray-700">
<CardHeader>
<CardTitle className="text-white">Loading...</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin text-gray-400" />
</div>
</CardContent>
</Card>
);
return (
<Suspense fallback={fallback || defaultFallback}>
<LazyComponent {...props} />
</Suspense>
);
}

View File

@@ -0,0 +1,59 @@
'use client';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
import Link from 'next/link';
import { Plus, Link as LinkIcon, FileText, Settings, Key, Rocket, Book, CreditCard, HelpCircle, Download } from 'lucide-react';
import * as LucideIcons from 'lucide-react';
interface QuickAction {
label: string;
href: string;
icon: string;
}
interface QuickActionsPanelProps {
actions: QuickAction[];
}
const iconMap: Record<string, any> = {
Plus,
Link: LinkIcon,
FileText,
Settings,
Key,
Rocket,
Book,
CreditCard,
HelpCircle,
Download,
};
export function QuickActionsPanel({ actions }: QuickActionsPanelProps) {
return (
<Card className="bg-gray-800 border-gray-700">
<CardHeader>
<CardTitle className="text-white">Quick Actions</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
{actions.map((action) => {
const Icon = iconMap[action.icon] || Plus;
return (
<Link
key={action.href}
href={action.href}
className="flex flex-col items-center justify-center p-4 bg-gray-900 rounded-lg hover:bg-gray-700 transition-colors group"
>
<Icon className="h-6 w-6 text-gray-400 group-hover:text-orange-500 mb-2 transition-colors" />
<span className="text-xs text-center text-gray-300 group-hover:text-white transition-colors">
{action.label}
</span>
</Link>
);
})}
</div>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,45 @@
'use client';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
import { BarChart3 } from 'lucide-react';
export function ResourceUsageTile() {
// Mock data - in production, this would come from API
const usageByDivision = [
{ name: 'Core Infrastructure', usage: 45, cost: 20350 },
{ name: 'Data Services', usage: 28, cost: 12600 },
{ name: 'Security', usage: 15, cost: 6750 },
{ name: 'AI/ML', usage: 12, cost: 5530 },
];
return (
<Card className="bg-gray-800 border-gray-700">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<BarChart3 className="h-5 w-5 text-orange-500" />
Resource Usage by Division
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
{usageByDivision.map((division) => (
<div key={division.name} className="p-3 bg-gray-900 rounded-lg">
<div className="flex items-center justify-between mb-2">
<span className="text-sm text-gray-300">{division.name}</span>
<span className="text-sm font-semibold text-white">{division.usage}%</span>
</div>
<div className="w-full bg-gray-800 rounded-full h-2 mb-1">
<div
className="bg-orange-500 h-2 rounded-full"
style={{ width: `${division.usage}%` }}
/>
</div>
<p className="text-xs text-gray-400">${division.cost.toLocaleString()}/month</p>
</div>
))}
</div>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,94 @@
'use client';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
import { Cpu, HardDrive, Activity } from 'lucide-react';
export function ResourceUtilizationTile() {
// Mock data - in production, this would come from API
const utilization = {
cpu: { used: 68, total: 100 },
memory: { used: 45, total: 100 },
storage: { used: 72, total: 100 },
};
const getColor = (percent: number) => {
if (percent >= 80) return 'text-red-400';
if (percent >= 60) return 'text-yellow-400';
return 'text-green-400';
};
const getBgColor = (percent: number) => {
if (percent >= 80) return 'bg-red-500/20';
if (percent >= 60) return 'bg-yellow-500/20';
return 'bg-green-500/20';
};
return (
<Card className="bg-gray-800 border-gray-700">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<Activity className="h-5 w-5 text-orange-500" />
Resource Utilization
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<Cpu className="h-4 w-4 text-blue-400" />
<span className="text-sm text-gray-400">CPU</span>
</div>
<span className={`text-sm font-semibold ${getColor(utilization.cpu.used)}`}>
{utilization.cpu.used}%
</span>
</div>
<div className="w-full bg-gray-900 rounded-full h-2">
<div
className={`h-2 rounded-full ${getBgColor(utilization.cpu.used)}`}
style={{ width: `${utilization.cpu.used}%` }}
/>
</div>
</div>
<div>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<Activity className="h-4 w-4 text-purple-400" />
<span className="text-sm text-gray-400">Memory</span>
</div>
<span className={`text-sm font-semibold ${getColor(utilization.memory.used)}`}>
{utilization.memory.used}%
</span>
</div>
<div className="w-full bg-gray-900 rounded-full h-2">
<div
className={`h-2 rounded-full ${getBgColor(utilization.memory.used)}`}
style={{ width: `${utilization.memory.used}%` }}
/>
</div>
</div>
<div>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<HardDrive className="h-4 w-4 text-cyan-400" />
<span className="text-sm text-gray-400">Storage</span>
</div>
<span className={`text-sm font-semibold ${getColor(utilization.storage.used)}`}>
{utilization.storage.used}%
</span>
</div>
<div className="w-full bg-gray-900 rounded-full h-2">
<div
className={`h-2 rounded-full ${getBgColor(utilization.storage.used)}`}
style={{ width: `${utilization.storage.used}%` }}
/>
</div>
</div>
</div>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,44 @@
'use client';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
import { TrendingUp } from 'lucide-react';
export function ServiceAdoptionTile() {
// Mock data - in production, this would come from API
const services = [
{ name: 'PhoenixCore Compute', adoption: 95, trend: 'up' },
{ name: 'OkraVault Storage', adoption: 87, trend: 'up' },
{ name: 'SankofaGrid Network', adoption: 72, trend: 'up' },
{ name: 'Firebird AI Engine', adoption: 45, trend: 'up' },
];
return (
<Card className="bg-gray-800 border-gray-700">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<TrendingUp className="h-5 w-5 text-orange-500" />
Service Adoption Metrics
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
{services.map((service) => (
<div key={service.name} className="p-3 bg-gray-900 rounded-lg">
<div className="flex items-center justify-between mb-2">
<span className="text-sm text-gray-300">{service.name}</span>
<span className="text-sm font-semibold text-white">{service.adoption}%</span>
</div>
<div className="w-full bg-gray-800 rounded-full h-2">
<div
className="bg-green-500 h-2 rounded-full"
style={{ width: `${service.adoption}%` }}
/>
</div>
</div>
))}
</div>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,173 @@
'use client';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
import { Activity, CheckCircle, AlertCircle, XCircle, Globe, Server } from 'lucide-react';
import { useSystemHealth } from '@/hooks/useDashboardData';
export function SystemHealthTile() {
const { data, loading, error } = useSystemHealth();
// Calculate health data from API response
const healthData = data ? {
regions: {
total: data.sites?.length || 0,
healthy: data.sites?.filter((s: any) => s.status === 'ACTIVE').length || 0,
warning: data.sites?.filter((s: any) => s.status === 'MAINTENANCE').length || 0,
critical: data.sites?.filter((s: any) => s.status === 'INACTIVE').length || 0,
},
clusters: {
total: data.resources?.length || 0,
healthy: data.resources?.filter((r: any) => r.status === 'RUNNING').length || 0,
warning: data.resources?.filter((r: any) => r.status === 'PROVISIONING').length || 0,
critical: data.resources?.filter((r: any) => r.status === 'ERROR').length || 0,
},
nodes: {
total: data.resources?.length || 0,
healthy: data.resources?.filter((r: any) => r.status === 'RUNNING').length || 0,
warning: data.resources?.filter((r: any) => r.status === 'PROVISIONING').length || 0,
critical: data.resources?.filter((r: any) => r.status === 'ERROR').length || 0,
},
} : {
regions: { total: 0, healthy: 0, warning: 0, critical: 0 },
clusters: { total: 0, healthy: 0, warning: 0, critical: 0 },
nodes: { total: 0, healthy: 0, warning: 0, critical: 0 },
};
if (loading) {
return (
<Card className="bg-gray-800 border-gray-700">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<Activity className="h-5 w-5 text-orange-500" />
System Health Overview
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-center text-gray-400 py-4">Loading...</div>
</CardContent>
</Card>
);
}
if (error) {
return (
<Card className="bg-gray-800 border-gray-700">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<Activity className="h-5 w-5 text-orange-500" />
System Health Overview
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-center text-red-400 py-4">Error loading health data</div>
</CardContent>
</Card>
);
}
const overallHealth = healthData.regions.healthy / healthData.regions.total > 0.95 ? 'healthy' :
healthData.regions.healthy / healthData.regions.total > 0.85 ? 'warning' : 'critical';
return (
<Card className="bg-gray-800 border-gray-700">
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="text-white flex items-center gap-2">
<Activity className="h-5 w-5 text-orange-500" />
System Health Overview
</CardTitle>
<div className={`px-3 py-1 rounded-full text-xs font-semibold ${
overallHealth === 'healthy' ? 'bg-green-500/20 text-green-400' :
overallHealth === 'warning' ? 'bg-yellow-500/20 text-yellow-400' :
'bg-red-500/20 text-red-400'
}`}>
{overallHealth === 'healthy' ? 'Healthy' : overallHealth === 'warning' ? 'Warning' : 'Critical'}
</div>
</div>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex items-center justify-between p-3 bg-gray-900 rounded-lg">
<div className="flex items-center gap-3">
<Globe className="h-5 w-5 text-blue-400" />
<div>
<p className="text-sm text-gray-400">Regions</p>
<p className="text-lg font-semibold text-white">{healthData.regions.total}</p>
</div>
</div>
<div className="flex gap-2">
<div className="flex items-center gap-1 text-green-400">
<CheckCircle className="h-4 w-4" />
<span className="text-sm">{healthData.regions.healthy}</span>
</div>
{healthData.regions.warning > 0 && (
<div className="flex items-center gap-1 text-yellow-400">
<AlertCircle className="h-4 w-4" />
<span className="text-sm">{healthData.regions.warning}</span>
</div>
)}
{healthData.regions.critical > 0 && (
<div className="flex items-center gap-1 text-red-400">
<XCircle className="h-4 w-4" />
<span className="text-sm">{healthData.regions.critical}</span>
</div>
)}
</div>
</div>
<div className="flex items-center justify-between p-3 bg-gray-900 rounded-lg">
<div className="flex items-center gap-3">
<Server className="h-5 w-5 text-purple-400" />
<div>
<p className="text-sm text-gray-400">Clusters</p>
<p className="text-lg font-semibold text-white">{healthData.clusters.total}</p>
</div>
</div>
<div className="flex gap-2">
<div className="flex items-center gap-1 text-green-400">
<CheckCircle className="h-4 w-4" />
<span className="text-sm">{healthData.clusters.healthy}</span>
</div>
{healthData.clusters.warning > 0 && (
<div className="flex items-center gap-1 text-yellow-400">
<AlertCircle className="h-4 w-4" />
<span className="text-sm">{healthData.clusters.warning}</span>
</div>
)}
</div>
</div>
<div className="flex items-center justify-between p-3 bg-gray-900 rounded-lg">
<div className="flex items-center gap-3">
<Activity className="h-5 w-5 text-cyan-400" />
<div>
<p className="text-sm text-gray-400">Nodes</p>
<p className="text-lg font-semibold text-white">{healthData.nodes.total}</p>
</div>
</div>
<div className="flex gap-2">
<div className="flex items-center gap-1 text-green-400">
<CheckCircle className="h-4 w-4" />
<span className="text-sm">{healthData.nodes.healthy}</span>
</div>
{healthData.nodes.warning > 0 && (
<div className="flex items-center gap-1 text-yellow-400">
<AlertCircle className="h-4 w-4" />
<span className="text-sm">{healthData.nodes.warning}</span>
</div>
)}
{healthData.nodes.critical > 0 && (
<div className="flex items-center gap-1 text-red-400">
<XCircle className="h-4 w-4" />
<span className="text-sm">{healthData.nodes.critical}</span>
</div>
)}
</div>
</div>
</div>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,53 @@
'use client';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
import { TestTube, PlayCircle, PauseCircle } from 'lucide-react';
export function TestEnvironmentsTile() {
// Mock data - in production, this would come from API
const environments = {
active: 3,
stopped: 1,
total: 4,
};
return (
<Card className="bg-gray-800 border-gray-700">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<TestTube className="h-5 w-5 text-orange-500" />
Test Environments
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="text-center">
<p className="text-4xl font-bold text-white mb-2">{environments.total}</p>
<p className="text-sm text-gray-400">Total Environments</p>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between p-2 bg-gray-900 rounded">
<div className="flex items-center gap-2">
<PlayCircle className="h-4 w-4 text-green-400" />
<span className="text-sm text-gray-300">Active</span>
</div>
<span className="text-sm font-semibold text-white">{environments.active}</span>
</div>
{environments.stopped > 0 && (
<div className="flex items-center justify-between p-2 bg-gray-900 rounded">
<div className="flex items-center gap-2">
<PauseCircle className="h-4 w-4 text-gray-400" />
<span className="text-sm text-gray-300">Stopped</span>
</div>
<span className="text-sm font-semibold text-white">{environments.stopped}</span>
</div>
)}
</div>
</div>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,344 @@
'use client';
import { useState, useMemo } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { Input } from '@/components/ui/Input';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { InfoIcon, AlertTriangleIcon, CheckCircleIcon } from 'lucide-react';
// Import orchestration engine (would be from API in production)
import type {
OrchestrationRequest,
OrchestrationResult,
OutputType,
InputSpec,
TimelineSpec
} from '@/lib/fairness-orchestration';
import { orchestrate, getAvailableOutputs, getUserMessage } from '@/lib/fairness-orchestration';
export default function FairnessOrchestrationWizard() {
const [selectedOutputs, setSelectedOutputs] = useState<string[]>([]);
const [inputSpec, setInputSpec] = useState<InputSpec>({
dataset: '',
sensitiveAttributes: [],
dateRange: undefined,
filters: {}
});
const [timeline, setTimeline] = useState<TimelineSpec>({
mode: 'now',
sla: '2 hours'
});
const [orchestrationResult, setOrchestrationResult] = useState<OrchestrationResult | null>(null);
const availableOutputs = getAvailableOutputs();
// Calculate orchestration when inputs change
const result = useMemo(() => {
if (!inputSpec.dataset || selectedOutputs.length === 0) {
return null;
}
const request: OrchestrationRequest = {
input: inputSpec,
outputs: selectedOutputs,
timeline
};
return orchestrate(request);
}, [inputSpec, selectedOutputs, timeline]);
const handleOutputToggle = (outputId: string) => {
setSelectedOutputs(prev =>
prev.includes(outputId)
? prev.filter(id => id !== outputId)
: [...prev, outputId]
);
};
const handleRun = () => {
if (result) {
setOrchestrationResult(result);
}
};
return (
<div className="space-y-6 p-6">
<div>
<h1 className="text-3xl font-bold">Fairness Audit Orchestration</h1>
<p className="text-muted-foreground mt-2">
Design from the end result backwards. Choose your outputs, inputs, and timeline.
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Step 1: Output Selection */}
<Card>
<CardHeader>
<CardTitle>1. Output</CardTitle>
<CardDescription>What do you want to generate?</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{availableOutputs.map((output) => (
<div key={output.id} className="flex items-start space-x-3">
<Checkbox
id={output.id}
checked={selectedOutputs.includes(output.id)}
onCheckedChange={() => handleOutputToggle(output.id)}
/>
<div className="flex-1">
<Label htmlFor={output.id} className="font-medium cursor-pointer">
{output.name}
</Label>
<p className="text-sm text-muted-foreground">
{output.description}
</p>
<p className="text-xs text-muted-foreground mt-1">
Weight: {output.weight} units
</p>
</div>
</div>
))}
{selectedOutputs.length > 0 && (
<div className="pt-4 border-t">
<p className="text-sm">
<strong>Total Output Load:</strong>{' '}
{result?.outputLoad.toFixed(1) || '0'} units
</p>
</div>
)}
</CardContent>
</Card>
{/* Step 2: Input Specification */}
<Card>
<CardHeader>
<CardTitle>2. Input</CardTitle>
<CardDescription>What data are you analyzing?</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div>
<Label htmlFor="dataset">Dataset</Label>
<Input
id="dataset"
value={inputSpec.dataset}
onChange={(e) => setInputSpec(prev => ({ ...prev, dataset: e.target.value }))}
placeholder="Select or enter dataset name"
/>
</div>
<div>
<Label htmlFor="dateRange">Date Range (Optional)</Label>
<div className="grid grid-cols-2 gap-2">
<Input
type="date"
value={inputSpec.dateRange?.start || ''}
onChange={(e) => setInputSpec(prev => ({
...prev,
dateRange: { ...prev.dateRange, start: e.target.value } as any
}))}
/>
<Input
type="date"
value={inputSpec.dateRange?.end || ''}
onChange={(e) => setInputSpec(prev => ({
...prev,
dateRange: { ...prev.dateRange, end: e.target.value } as any
}))}
/>
</div>
</div>
<div>
<Label htmlFor="sensitiveAttributes">Sensitive Attributes</Label>
<Input
id="sensitiveAttributes"
value={inputSpec.sensitiveAttributes.join(', ')}
onChange={(e) => setInputSpec(prev => ({
...prev,
sensitiveAttributes: e.target.value.split(',').map(s => s.trim()).filter(Boolean)
}))}
placeholder="race, gender, age (comma-separated)"
/>
</div>
{result && (
<div className="pt-4 border-t">
<p className="text-sm">
<strong>Estimated Input Load:</strong>{' '}
{result.inputLoad.toFixed(0)} units
</p>
</div>
)}
</CardContent>
</Card>
{/* Step 3: Timeline */}
<Card>
<CardHeader>
<CardTitle>3. Timeline</CardTitle>
<CardDescription>When does this need to be ready?</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div>
<Label htmlFor="mode">Execution Mode</Label>
<Select
value={timeline.mode}
onValueChange={(value: 'now' | 'scheduled' | 'continuous') =>
setTimeline(prev => ({ ...prev, mode: value }))
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="now">Run Now</SelectItem>
<SelectItem value="scheduled">Schedule</SelectItem>
<SelectItem value="continuous">Continuous</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="sla">SLA / Time Limit</Label>
<Input
id="sla"
value={timeline.sla || ''}
onChange={(e) => setTimeline(prev => ({ ...prev, sla: e.target.value }))}
placeholder="2 hours, 1 day, 30 minutes"
/>
</div>
{result && (
<div className="pt-4 border-t space-y-2">
<p className="text-sm">
<strong>Estimated Time:</strong>{' '}
{formatTime(result.estimatedTime)}
</p>
<p className="text-sm">
<strong>Total Process Load:</strong>{' '}
{result.totalLoad.toFixed(1)} units
</p>
</div>
)}
</CardContent>
</Card>
</div>
{/* Orchestration Result */}
{result && (
<Card>
<CardHeader>
<CardTitle>Orchestration Analysis</CardTitle>
<CardDescription>
The engine calculates: Total Load Output + 2×Input 3.2×Input
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-3 gap-4">
<div>
<p className="text-sm text-muted-foreground">Input Load</p>
<p className="text-2xl font-bold">{result.inputLoad.toFixed(0)}</p>
<p className="text-xs text-muted-foreground">× 2 = {result.inputLoad * 2} (passes)</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Output Load</p>
<p className="text-2xl font-bold">{result.outputLoad.toFixed(1)}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Total Load</p>
<p className="text-2xl font-bold">{result.totalLoad.toFixed(1)}</p>
<p className="text-xs text-muted-foreground">
{result.inputLoad.toFixed(0)} × 3.2 = {(result.inputLoad * 3.2).toFixed(1)}
</p>
</div>
</div>
{result.warnings.length > 0 && (
<Alert variant={result.feasible ? 'default' : 'destructive'}>
<AlertTriangleIcon className="h-4 w-4" />
<AlertDescription>
<div className="space-y-2">
{result.warnings.map((warning, i) => (
<p key={i}>{warning}</p>
))}
</div>
</AlertDescription>
</Alert>
)}
{result.suggestions.length > 0 && (
<Alert>
<InfoIcon className="h-4 w-4" />
<AlertDescription>
<div className="space-y-1">
<p className="font-medium">Suggestions:</p>
{result.suggestions.map((suggestion, i) => (
<p key={i} className="text-sm"> {suggestion}</p>
))}
</div>
</AlertDescription>
</Alert>
)}
{result.feasible && result.warnings.length === 0 && (
<Alert variant="default" className="border-green-500">
<CheckCircleIcon className="h-4 w-4 text-green-500" />
<AlertDescription className="text-green-700">
This configuration is feasible and ready to run.
</AlertDescription>
</Alert>
)}
<div className="pt-4">
<p className="text-sm text-muted-foreground mb-2">
{getUserMessage(result, {
input: inputSpec,
outputs: selectedOutputs,
timeline
})}
</p>
</div>
<Button
onClick={handleRun}
disabled={!result.feasible || selectedOutputs.length === 0 || !inputSpec.dataset}
className="w-full"
>
{result.feasible ? 'Run Fairness Audit' : 'Adjust Configuration'}
</Button>
</CardContent>
</Card>
)}
{/* Info Box */}
<Card className="bg-muted/50">
<CardContent className="pt-6">
<div className="space-y-2">
<h3 className="font-semibold">How It Works</h3>
<p className="text-sm text-muted-foreground">
<strong>We design from the end result backwards.</strong> First we list every output you want
(reports, files, metrics), then we calculate how much input validation and enrichment is needed
roughly <strong>2× your input effort</strong> and size the total job at about <strong>3.2× the input</strong>.
</p>
<p className="text-sm text-muted-foreground">
You only choose: (1) What goes in, (2) What comes out, (3) When it needs to be ready.
The orchestration engine does everything in between.
</p>
</div>
</CardContent>
</Card>
</div>
);
}
function formatTime(seconds: number): string {
if (seconds < 60) return `${Math.ceil(seconds)}s`;
if (seconds < 3600) return `${(seconds / 60).toFixed(1)}m`;
if (seconds < 86400) return `${(seconds / 3600).toFixed(1)}h`;
return `${(seconds / 86400).toFixed(1)}d`;
}

View File

@@ -0,0 +1,132 @@
'use client'
import { useQuery } from '@tanstack/react-query'
import { useSession } from 'next-auth/react'
import { createKubernetesClient } from '@/lib/kubernetes-client'
import { Card, CardContent, CardHeader, CardTitle } from '../ui/Card'
import { Server, CheckCircle, XCircle } from 'lucide-react'
export default function KubernetesClusters() {
const { data: session } = useSession()
const k8s = createKubernetesClient(session?.accessToken as string)
const { data: clusterInfo, isLoading } = useQuery({
queryKey: ['kubernetes-cluster'],
queryFn: () => k8s.getClusterInfo(),
refetchInterval: 30000,
})
const { data: namespaces = [] } = useQuery({
queryKey: ['kubernetes-namespaces'],
queryFn: () => k8s.listNamespaces(),
enabled: !!clusterInfo,
})
const { data: pods = [] } = useQuery({
queryKey: ['kubernetes-pods'],
queryFn: () => k8s.listPods(),
enabled: !!clusterInfo,
})
const { data: deployments = [] } = useQuery({
queryKey: ['kubernetes-deployments'],
queryFn: () => k8s.listDeployments(),
enabled: !!clusterInfo,
})
if (isLoading) {
return (
<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>
)
}
if (!clusterInfo) {
return (
<Card>
<CardContent className="p-8 text-center text-gray-400">
Unable to connect to Kubernetes cluster
</CardContent>
</Card>
)
}
const nodeHealthPercentage =
clusterInfo.nodes && clusterInfo.nodes.total > 0
? Math.round((clusterInfo.nodes.ready / clusterInfo.nodes.total) * 100)
: 0
return (
<div className="space-y-6">
<h2 className="text-2xl font-bold">Kubernetes Clusters</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<Card>
<CardHeader>
<CardTitle className="text-sm font-medium text-gray-400">Cluster</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center gap-2">
<Server className="h-5 w-5 text-blue-500" />
<div>
<div className="text-lg font-bold">{clusterInfo.name}</div>
<div className="text-sm text-gray-400">{clusterInfo.version}</div>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-sm font-medium text-gray-400">Nodes</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center gap-2">
{nodeHealthPercentage === 100 ? (
<CheckCircle className="h-5 w-5 text-green-500" />
) : (
<XCircle className="h-5 w-5 text-yellow-500" />
)}
<div>
<div className="text-lg font-bold">
{clusterInfo.nodes?.ready || 0} / {clusterInfo.nodes?.total || 0}
</div>
<div className="text-sm text-gray-400">{nodeHealthPercentage}% healthy</div>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-sm font-medium text-gray-400">Namespaces</CardTitle>
</CardHeader>
<CardContent>
<div className="text-lg font-bold">{namespaces.length}</div>
<div className="text-sm text-gray-400">Total namespaces</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-sm font-medium text-gray-400">Workloads</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-1">
<div className="flex justify-between">
<span className="text-sm text-gray-400">Pods:</span>
<span className="text-sm font-medium">{pods.length}</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-gray-400">Deployments:</span>
<span className="text-sm font-medium">{deployments.length}</span>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
)
}

View File

@@ -0,0 +1,77 @@
'use client';
import { useState } from 'react';
import { usePathname } from 'next/navigation';
import Link from 'next/link';
import {
LayoutDashboard,
Server,
Network,
Settings,
FileText,
Activity,
Users,
CreditCard,
Shield,
Menu,
X
} from 'lucide-react';
const navigation = [
{ name: 'Dashboard', href: '/dashboard', icon: LayoutDashboard },
{ name: 'Resources', href: '/resources', icon: Server },
{ name: 'Virtual Machines', href: '/vms', icon: Server },
{ name: 'Networking', href: '/network', icon: Network },
{ name: 'Monitoring', href: '/dashboards', icon: Activity },
{ name: 'Users & Access', href: '/users', icon: Users },
{ name: 'Billing', href: '/billing', icon: CreditCard },
{ name: 'Security', href: '/security', icon: Shield },
{ name: 'Settings', href: '/settings', icon: Settings },
];
export function MobileNavigation() {
const [isOpen, setIsOpen] = useState(false);
const pathname = usePathname();
return (
<>
{/* Mobile menu button */}
<button
onClick={() => setIsOpen(!isOpen)}
className="md:hidden fixed bottom-4 right-4 z-50 p-4 bg-orange-500 text-white rounded-full shadow-lg"
aria-label="Toggle menu"
>
{isOpen ? <X className="h-6 w-6" /> : <Menu className="h-6 w-6" />}
</button>
{/* Mobile menu overlay */}
{isOpen && (
<div className="md:hidden fixed inset-0 z-40 bg-gray-900/95 backdrop-blur">
<nav className="flex flex-col h-full p-4 pt-20">
{navigation.map((item) => {
const Icon = item.icon;
const isActive = pathname === item.href || pathname?.startsWith(item.href + '/');
return (
<Link
key={item.name}
href={item.href}
onClick={() => setIsOpen(false)}
className={`flex items-center gap-4 p-4 rounded-lg mb-2 transition-colors ${
isActive
? 'bg-orange-500/20 text-orange-500 border border-orange-500/20'
: 'text-gray-400 hover:bg-gray-800 hover:text-white'
}`}
>
<Icon className="h-6 w-6" />
<span className="text-lg font-medium">{item.name}</span>
</Link>
);
})}
</nav>
</div>
)}
</>
);
}

View File

@@ -0,0 +1,55 @@
'use client'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { ChevronRight, Home } from 'lucide-react'
export function PortalBreadcrumbs() {
const pathname = usePathname()
const segments = pathname.split('/').filter(Boolean)
if (segments.length <= 1) return null
return (
<nav
aria-label="Breadcrumb"
className="flex items-center space-x-2 text-sm text-gray-400 px-4 py-2 bg-gray-900 border-b border-gray-800"
>
<Link
href="/"
className="hover:text-orange-500 transition-colors"
aria-label="Home"
>
<Home className="h-4 w-4" />
</Link>
{segments.map((segment, index) => {
const isLast = index === segments.length - 1
const href = '/' + segments.slice(0, index + 1).join('/')
const label = segment
.split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ')
return (
<div key={href} className="flex items-center space-x-2">
<ChevronRight className="h-4 w-4 text-gray-600" />
{isLast ? (
<span className="text-white font-medium" aria-current="page">
{label}
</span>
) : (
<Link
href={href}
className="hover:text-orange-500 transition-colors"
>
{label}
</Link>
)}
</div>
)
})}
</nav>
)
}

View File

@@ -0,0 +1,72 @@
'use client'
import Link from 'next/link'
import { useSession } from 'next-auth/react'
import { Search, Bell, Settings, User, LogOut } from 'lucide-react'
import { signOut } from 'next-auth/react'
export function PortalHeader() {
const { data: session } = useSession()
return (
<header className="sticky top-0 z-50 w-full border-b border-gray-800 bg-gray-900/95 backdrop-blur supports-[backdrop-filter]:bg-gray-900/60">
<div className="container flex h-16 items-center">
<Link href="/" className="mr-6 flex items-center space-x-2">
<span className="text-xl font-bold bg-gradient-to-r from-orange-500 to-yellow-500 bg-clip-text text-transparent">
Nexus Console
</span>
</Link>
{/* Search Bar - Enterprise-class search-first UX */}
<div className="flex flex-1 items-center justify-center max-w-2xl mx-8">
<div className="relative w-full">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" />
<input
type="search"
placeholder="Search resources, settings, docs..."
className="w-full rounded-md border border-gray-700 bg-gray-800 py-2 pl-10 pr-4 text-sm text-white placeholder-gray-400 focus:border-orange-500 focus:outline-none focus:ring-1 focus:ring-orange-500"
/>
</div>
</div>
<nav className="flex items-center space-x-4">
<button className="relative p-2 text-gray-400 hover:text-white transition-colors">
<Bell className="h-5 w-5" />
<span className="absolute top-1 right-1 h-2 w-2 rounded-full bg-orange-500" />
</button>
<Link
href="/settings"
className="p-2 text-gray-400 hover:text-white transition-colors"
>
<Settings className="h-5 w-5" />
</Link>
<div className="flex items-center space-x-2 border-l border-gray-700 pl-4">
<div className="flex items-center space-x-2">
<div className="h-8 w-8 rounded-full bg-gradient-to-br from-orange-500 to-yellow-500 flex items-center justify-center">
<User className="h-4 w-4 text-white" />
</div>
<div className="hidden md:block text-sm">
<div className="text-white font-medium">
{session?.user?.name || session?.user?.email || 'User'}
</div>
<div className="text-xs text-gray-400">
{session?.user?.role || 'Admin'}
</div>
</div>
</div>
<button
onClick={() => signOut()}
className="p-2 text-gray-400 hover:text-white transition-colors"
title="Sign out"
>
<LogOut className="h-5 w-5" />
</button>
</div>
</nav>
</div>
</header>
)
}

View File

@@ -0,0 +1,82 @@
'use client'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import {
LayoutDashboard,
Server,
Network,
Settings,
FileText,
Activity,
Users,
CreditCard,
Shield,
HelpCircle
} from 'lucide-react'
import { cn } from '@/lib/utils'
const navigation = [
{ name: 'Dashboard', href: '/', icon: LayoutDashboard },
{ name: 'Resources', href: '/resources', icon: Server },
{ name: 'Virtual Machines', href: '/vms', icon: Server },
{ name: 'Networking', href: '/network', icon: Network },
{ name: 'Monitoring', href: '/dashboards', icon: Activity },
{ name: 'Users & Access', href: '/users', icon: Users },
{ name: 'Billing', href: '/billing', icon: CreditCard },
{ name: 'Security', href: '/security', icon: Shield },
{ name: 'Settings', href: '/settings', icon: Settings },
]
const helpLinks = [
{ name: 'Documentation', href: '/help/docs', icon: FileText },
{ name: 'Support', href: '/help/support', icon: HelpCircle },
]
export function PortalSidebar() {
const pathname = usePathname()
return (
<aside className="fixed left-0 top-16 h-[calc(100vh-4rem)] w-64 border-r border-gray-800 bg-gray-900 overflow-y-auto">
<nav className="p-4 space-y-1">
{navigation.map((item) => {
const isActive = pathname === item.href || pathname?.startsWith(item.href + '/')
return (
<Link
key={item.name}
href={item.href}
className={cn(
'flex items-center space-x-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
isActive
? 'bg-orange-500/10 text-orange-500 border border-orange-500/20'
: 'text-gray-400 hover:bg-gray-800 hover:text-white'
)}
>
<item.icon className="h-5 w-5" />
<span>{item.name}</span>
</Link>
)
})}
</nav>
<div className="border-t border-gray-800 mt-4 pt-4 px-4">
<div className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">
Help & Support
</div>
<nav className="space-y-1">
{helpLinks.map((item) => (
<Link
key={item.name}
href={item.href}
className="flex items-center space-x-3 rounded-lg px-3 py-2 text-sm font-medium text-gray-400 hover:bg-gray-800 hover:text-white transition-colors"
>
<item.icon className="h-5 w-5" />
<span>{item.name}</span>
</Link>
))}
</nav>
</div>
</aside>
)
}

View File

@@ -0,0 +1,57 @@
'use client'
import { useEffect, useRef } from 'react'
interface GrafanaPanelProps {
dashboardUID: string
panelId: number
from?: string
to?: string
refresh?: string
theme?: 'light' | 'dark'
height?: string
width?: string
}
export default function GrafanaPanel({
dashboardUID,
panelId,
from = 'now-6h',
to = 'now',
refresh = '30s',
theme = 'dark',
height = '400px',
width = '100%',
}: GrafanaPanelProps) {
const iframeRef = useRef<HTMLIFrameElement>(null)
const grafanaURL = process.env.NEXT_PUBLIC_GRAFANA_URL || 'http://localhost:3001'
useEffect(() => {
if (iframeRef.current) {
const url = new URL(`${grafanaURL}/d/${dashboardUID}`)
url.searchParams.set('orgId', '1')
url.searchParams.set('from', from)
url.searchParams.set('to', to)
url.searchParams.set('refresh', refresh)
url.searchParams.set('theme', theme)
url.searchParams.set('panelId', panelId.toString())
url.searchParams.set('kiosk', 'tv')
iframeRef.current.src = url.toString()
}
}, [dashboardUID, panelId, from, to, refresh, theme, grafanaURL])
return (
<div className="w-full rounded-lg overflow-hidden border border-gray-700">
<iframe
ref={iframeRef}
width={width}
height={height}
frameBorder="0"
className="bg-gray-900"
title={`Grafana Panel ${panelId}`}
/>
</div>
)
}

View File

@@ -0,0 +1,153 @@
'use client'
import { useState, useEffect } from 'react'
import { useQuery } from '@tanstack/react-query'
import { Card, CardContent, CardHeader, CardTitle } from '../ui/Card'
import { Button } from '../ui/Button'
import { Input } from '../ui/Input'
import { RefreshCw, Search, Download } from 'lucide-react'
import axios from 'axios'
interface LogEntry {
stream: Record<string, string>
values: Array<[string, string]>
}
interface LokiQueryResponse {
status: string
data: {
resultType: string
result: LogEntry[]
}
}
interface LokiLogViewerProps {
query?: string
limit?: number
since?: string
}
export default function LokiLogViewer({
query = '{job="default"}',
limit = 100,
since = '1h',
}: LokiLogViewerProps) {
const [searchQuery, setSearchQuery] = useState(query)
const [logLines, setLogLines] = useState<Array<{ timestamp: string; message: string; labels: Record<string, string> }>>([])
const lokiURL = process.env.NEXT_PUBLIC_LOKI_URL || 'http://localhost:3100'
const { data, isLoading, refetch } = useQuery({
queryKey: ['loki-logs', searchQuery, limit, since],
queryFn: async () => {
const end = Math.floor(Date.now() / 1000)
const start = end - (since === '1h' ? 3600 : since === '6h' ? 21600 : 86400)
const response = await axios.get<LokiQueryResponse>(`${lokiURL}/loki/api/v1/query_range`, {
params: {
query: searchQuery,
start,
end,
limit,
},
})
const logs: Array<{ timestamp: string; message: string; labels: Record<string, string> }> = []
response.data.data.result.forEach((entry: LogEntry) => {
entry.values.forEach(([timestamp, message]) => {
logs.push({
timestamp: new Date(parseInt(timestamp) / 1000000).toISOString(),
message,
labels: entry.stream,
})
})
})
return logs.sort((a, b) => b.timestamp.localeCompare(a.timestamp))
},
refetchInterval: 10000, // Refresh every 10 seconds
})
useEffect(() => {
if (data) {
setLogLines(data)
}
}, [data])
const handleExport = () => {
const logText = logLines
.map((log) => `[${log.timestamp}] ${JSON.stringify(log.labels)} ${log.message}`)
.join('\n')
const blob = new Blob([logText], { type: 'text/plain' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `logs-${Date.now()}.txt`
a.click()
URL.revokeObjectURL(url)
}
return (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>Log Viewer (Loki)</CardTitle>
<div className="flex gap-2">
<Button onClick={handleExport} variant="outline" size="sm">
<Download className="h-4 w-4 mr-2" />
Export
</Button>
<Button onClick={() => refetch()} variant="outline" size="sm">
<RefreshCw className="h-4 w-4 mr-2" />
Refresh
</Button>
</div>
</div>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex gap-2">
<Input
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="LogQL query (e.g., {job='default'})"
className="flex-1"
/>
<Button onClick={() => refetch()}>
<Search className="h-4 w-4 mr-2" />
Search
</Button>
</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>
) : logLines.length === 0 ? (
<div className="text-center text-gray-400 p-8">No logs found</div>
) : (
<div className="bg-gray-900 rounded-lg p-4 font-mono text-sm overflow-auto max-h-96">
{logLines.map((log, index) => (
<div key={index} className="mb-2 border-b border-gray-800 pb-2 last:border-0">
<div className="flex items-start gap-2">
<span className="text-gray-500 text-xs whitespace-nowrap">
{new Date(log.timestamp).toLocaleTimeString()}
</span>
<span className="text-blue-400 text-xs">
{Object.entries(log.labels)
.map(([k, v]) => `${k}=${v}`)
.join(' ')}
</span>
</div>
<div className="text-gray-300 mt-1 break-words">{log.message}</div>
</div>
))}
</div>
)}
</div>
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,139 @@
'use client';
import { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
import { CheckCircle, ArrowRight, ArrowLeft } from 'lucide-react';
import { useRouter } from 'next/navigation';
interface OnboardingStep {
id: string;
title: string;
description: string;
component: React.ComponentType<any>;
}
interface OnboardingWizardProps {
steps: OnboardingStep[];
onComplete: () => void;
}
export function OnboardingWizard({ steps, onComplete }: OnboardingWizardProps) {
const [currentStep, setCurrentStep] = useState(0);
const [completedSteps, setCompletedSteps] = useState<Set<string>>(new Set());
const router = useRouter();
const handleNext = () => {
if (currentStep < steps.length - 1) {
setCurrentStep(currentStep + 1);
} else {
onComplete();
router.push('/dashboard');
}
};
const handlePrevious = () => {
if (currentStep > 0) {
setCurrentStep(currentStep - 1);
}
};
const handleStepComplete = (stepId: string) => {
setCompletedSteps(new Set([...completedSteps, stepId]));
};
const CurrentStepComponent = steps[currentStep].component;
return (
<div className="fixed inset-0 z-50 bg-gray-900/95 backdrop-blur flex items-center justify-center p-4">
<Card className="bg-gray-800 border-gray-700 max-w-4xl w-full">
<CardHeader>
<div className="mb-6">
<div className="flex items-center justify-between mb-4">
<CardTitle className="text-white text-2xl">
Welcome to Nexus Console
</CardTitle>
<span className="text-sm text-gray-400">
Step {currentStep + 1} of {steps.length}
</span>
</div>
{/* Progress bar */}
<div className="w-full bg-gray-900 rounded-full h-2">
<div
className="bg-orange-500 h-2 rounded-full transition-all"
style={{ width: `${((currentStep + 1) / steps.length) * 100}%` }}
/>
</div>
</div>
{/* Step indicators */}
<div className="flex items-center justify-between mb-6">
{steps.map((step, index) => (
<div key={step.id} className="flex items-center flex-1">
<div className="flex flex-col items-center">
<div
className={`w-10 h-10 rounded-full flex items-center justify-center border-2 ${
index < currentStep
? 'bg-green-500 border-green-500'
: index === currentStep
? 'bg-orange-500 border-orange-500'
: 'bg-gray-700 border-gray-600'
}`}
>
{index < currentStep ? (
<CheckCircle className="h-6 w-6 text-white" />
) : (
<span className="text-white font-semibold">{index + 1}</span>
)}
</div>
<span className="text-xs text-gray-400 mt-2 text-center max-w-20">
{step.title}
</span>
</div>
{index < steps.length - 1 && (
<div
className={`flex-1 h-0.5 mx-2 ${
index < currentStep ? 'bg-green-500' : 'bg-gray-700'
}`}
/>
)}
</div>
))}
</div>
</CardHeader>
<CardContent>
<div className="mb-6">
<h3 className="text-xl font-semibold text-white mb-2">
{steps[currentStep].title}
</h3>
<p className="text-gray-400 mb-6">
{steps[currentStep].description}
</p>
<CurrentStepComponent
onComplete={() => handleStepComplete(steps[currentStep].id)}
/>
</div>
<div className="flex items-center justify-between pt-4 border-t border-gray-700">
<button
onClick={handlePrevious}
disabled={currentStep === 0}
className="px-6 py-2 bg-gray-700 text-white rounded-lg hover:bg-gray-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
>
<ArrowLeft className="h-4 w-4" />
Previous
</button>
<button
onClick={handleNext}
className="px-6 py-2 bg-orange-500 text-white rounded-lg hover:bg-orange-600 transition-colors flex items-center gap-2"
>
{currentStep === steps.length - 1 ? 'Complete' : 'Next'}
<ArrowRight className="h-4 w-4" />
</button>
</div>
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,49 @@
'use client';
import { useState } from 'react';
export function PreferencesStep({ onComplete }: { onComplete: () => void }) {
const [notifications, setNotifications] = useState(true);
const [theme, setTheme] = useState('dark');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// Save preferences
onComplete();
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={notifications}
onChange={(e) => setNotifications(e.target.checked)}
className="w-4 h-4"
/>
<span className="text-sm text-gray-300">Enable email notifications</span>
</label>
</div>
<div>
<label className="block text-sm text-gray-300 mb-2">Theme</label>
<select
value={theme}
onChange={(e) => setTheme(e.target.value)}
className="w-full px-4 py-2 bg-gray-900 border border-gray-700 rounded text-white"
>
<option value="dark">Dark</option>
<option value="light">Light</option>
<option value="auto">Auto</option>
</select>
</div>
<button
type="submit"
className="w-full px-4 py-2 bg-orange-500 text-white rounded hover:bg-orange-600 transition-colors"
>
Continue
</button>
</form>
);
}

View File

@@ -0,0 +1,49 @@
'use client';
import { useState } from 'react';
export function ProfileStep({ onComplete }: { onComplete: () => void }) {
const [name, setName] = useState('');
const [role, setRole] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// Save profile data
onComplete();
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm text-gray-300 mb-2">Full Name</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full px-4 py-2 bg-gray-900 border border-gray-700 rounded text-white"
placeholder="Enter your name"
/>
</div>
<div>
<label className="block text-sm text-gray-300 mb-2">Role</label>
<select
value={role}
onChange={(e) => setRole(e.target.value)}
className="w-full px-4 py-2 bg-gray-900 border border-gray-700 rounded text-white"
>
<option value="">Select your role</option>
<option value="technical">Technical Admin</option>
<option value="business">Business Owner</option>
<option value="developer">Developer</option>
</select>
</div>
<button
type="submit"
className="w-full px-4 py-2 bg-orange-500 text-white rounded hover:bg-orange-600 transition-colors"
>
Continue
</button>
</form>
);
}

View File

@@ -0,0 +1,25 @@
'use client';
import { useEffect } from 'react';
export function WelcomeStep({ onComplete }: { onComplete: () => void }) {
useEffect(() => {
// Auto-complete after a brief delay
const timer = setTimeout(() => {
onComplete();
}, 1000);
return () => clearTimeout(timer);
}, [onComplete]);
return (
<div className="text-center py-8">
<h2 className="text-2xl font-bold text-white mb-4">
Welcome to Sankofa Phoenix Nexus Console
</h2>
<p className="text-gray-400 mb-6">
Let's get you set up in just a few steps. This will only take a minute.
</p>
</div>
);
}

View File

@@ -0,0 +1,39 @@
import * as React from 'react'
import { cn } from '@/lib/utils'
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'default' | 'primary' | 'secondary' | 'outline' | 'ghost' | 'destructive'
size?: 'sm' | 'md' | 'lg'
}
export function Button({
className,
variant = 'default',
size = 'md',
...props
}: ButtonProps) {
const baseStyles = 'inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50'
const variants = {
default: 'bg-gray-700 text-white hover:bg-gray-600',
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-600 text-white hover:bg-gray-500',
outline: 'border border-gray-600 bg-transparent hover:bg-gray-800',
ghost: 'hover:bg-gray-800',
destructive: 'bg-red-600 text-white hover:bg-red-700',
}
const sizes = {
sm: 'h-8 px-3 text-sm',
md: 'h-10 px-4 text-base',
lg: 'h-12 px-6 text-lg',
}
return (
<button
className={cn(baseStyles, variants[variant], sizes[size], className)}
{...props}
/>
)
}

View File

@@ -0,0 +1,22 @@
import * as React from 'react'
import { cn } from '@/lib/utils'
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
'flex h-10 w-full rounded-md border border-gray-600 bg-gray-900 px-3 py-2 text-sm text-white ring-offset-gray-900 file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = 'Input'

View File

@@ -0,0 +1,53 @@
import * as React from 'react'
import * as TabsPrimitive from '@radix-ui/react-tabs'
import { cn } from '@/lib/utils'
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
'inline-flex h-10 items-center justify-center rounded-md bg-gray-800 p-1 text-gray-400',
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-gray-900 transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-gray-700 data-[state=active]:text-white data-[state=active]:shadow-sm',
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
'mt-2 ring-offset-gray-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2',
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }

View File

@@ -0,0 +1,29 @@
import * as React from 'react'
import { cn } from '@/lib/utils'
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement> {
variant?: 'default' | 'secondary' | 'outline' | 'success' | 'warning' | 'error'
}
export function Badge({ className, variant = 'default', ...props }: BadgeProps) {
const variants = {
default: 'bg-gray-700 text-white',
secondary: 'bg-gray-600 text-white',
outline: 'border border-gray-600 bg-transparent',
success: 'bg-green-600 text-white',
warning: 'bg-yellow-600 text-white',
error: 'bg-red-600 text-white',
}
return (
<div
className={cn(
'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold transition-colors',
variants[variant],
className
)}
{...props}
/>
)
}

View File

@@ -0,0 +1,90 @@
import * as React from 'react'
import * as SelectPrimitive from '@radix-ui/react-select'
import { Check, ChevronDown, ChevronUp } from 'lucide-react'
import { cn } from '@/lib/utils'
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
'flex h-10 w-full items-center justify-between rounded-md border border-gray-600 bg-gray-900 px-3 py-2 text-sm text-white ring-offset-gray-900 placeholder:text-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = 'popper', ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
'relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border border-gray-600 bg-gray-900 text-white shadow-md',
position === 'popper' &&
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
className
)}
position={position}
{...props}
>
<SelectPrimitive.Viewport
className={cn(
'p-1',
position === 'popper' &&
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]'
)}
>
{children}
</SelectPrimitive.Viewport>
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-gray-800 focus:text-white data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectItem,
}