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

@@ -0,0 +1,113 @@
'use client';
import { useSession } from 'next-auth/react';
import { signIn } from 'next-auth/react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
import { Building2, Users, CreditCard, Shield, ArrowRight } from 'lucide-react';
import Link from 'next/link';
export default function AdminPortalPage() {
const { data: session, status } = useSession();
if (status === 'loading') {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-900">
<div className="text-center">
<div className="mb-4 h-8 w-8 animate-spin rounded-full border-4 border-gray-300 border-t-blue-600 mx-auto"></div>
<p className="text-gray-400">Loading...</p>
</div>
</div>
);
}
if (status === 'unauthenticated') {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-900">
<div className="text-center max-w-md mx-auto p-8">
<h1 className="text-2xl font-bold text-white mb-4">Welcome to Admin Portal</h1>
<p className="text-gray-400 mb-6">Please sign in to continue</p>
<button
onClick={() => signIn()}
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
Sign In
</button>
</div>
</div>
);
}
const adminSections = [
{
title: 'Organization Management',
description: 'Manage multi-tenant organizations, tenant isolation, and resource quotas',
icon: Building2,
href: '/admin/organizations',
features: ['Multi-tenant view', 'Tenant isolation', 'Resource quotas'],
},
{
title: 'User Management',
description: 'Manage users, roles, and permissions across your organization',
icon: Users,
href: '/admin/users',
features: ['User list', 'Role assignment', 'Permission management'],
},
{
title: 'Billing & Subscriptions',
description: 'View subscriptions, usage-based billing, invoices, and payment methods',
icon: CreditCard,
href: '/admin/billing',
features: ['Subscriptions', 'Usage billing', 'Invoice history'],
},
{
title: 'Compliance & Reporting',
description: 'Compliance dashboard, audit logs, and export reports',
icon: Shield,
href: '/admin/compliance',
features: ['Compliance dashboard', 'Audit logs', 'Export reports'],
},
];
return (
<div className="container mx-auto px-4 py-8">
<div className="mb-8">
<h1 className="text-3xl font-bold text-white mb-2">Customer / Tenant Admin Portal</h1>
<p className="text-gray-400">Manage your organization, users, billing, and compliance</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{adminSections.map((section) => {
const Icon = section.icon;
return (
<Card key={section.href} className="bg-gray-800 border-gray-700 hover:border-orange-500 transition-colors">
<CardHeader>
<div className="flex items-center gap-3 mb-2">
<Icon className="h-6 w-6 text-orange-500" />
<CardTitle className="text-white">{section.title}</CardTitle>
</div>
<p className="text-sm text-gray-400">{section.description}</p>
</CardHeader>
<CardContent>
<ul className="space-y-2 mb-4">
{section.features.map((feature) => (
<li key={feature} className="flex items-center gap-2 text-sm text-gray-300">
<span className="text-orange-500"></span>
<span>{feature}</span>
</li>
))}
</ul>
<Link
href={section.href}
className="inline-flex items-center gap-2 text-sm text-orange-500 hover:text-orange-400 transition-colors"
>
Manage <ArrowRight className="h-4 w-4" />
</Link>
</CardContent>
</Card>
);
})}
</div>
</div>
);
}

View File

@@ -0,0 +1,49 @@
'use client';
import { useSession } from 'next-auth/react';
import { signIn } from 'next-auth/react';
import { AdvancedAnalytics } from '@/components/analytics/AdvancedAnalytics';
export default function AnalyticsPage() {
const { data: session, status } = useSession();
if (status === 'loading') {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-900">
<div className="text-center">
<div className="mb-4 h-8 w-8 animate-spin rounded-full border-4 border-gray-300 border-t-blue-600 mx-auto"></div>
<p className="text-gray-400">Loading...</p>
</div>
</div>
);
}
if (status === 'unauthenticated') {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-900">
<div className="text-center max-w-md mx-auto p-8">
<h1 className="text-2xl font-bold text-white mb-4">Welcome to Analytics</h1>
<p className="text-gray-400 mb-6">Please sign in to continue</p>
<button
onClick={() => signIn()}
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
Sign In
</button>
</div>
</div>
);
}
return (
<div className="container mx-auto px-4 py-8">
<div className="mb-8">
<h1 className="text-3xl font-bold text-white mb-2">Advanced Analytics</h1>
<p className="text-gray-400">Comprehensive analytics and insights for your infrastructure</p>
</div>
<AdvancedAnalytics />
</div>
);
}

View File

@@ -0,0 +1,28 @@
'use client'
import { useSession } from 'next-auth/react'
import { redirect } from 'next/navigation'
import ArgoCDApplications from '@/components/argocd/ArgoCDApplications'
export default function ArgoCDPage() {
const { data: session, status } = useSession()
if (status === 'loading') {
return (
<div className="flex min-h-screen items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
)
}
if (!session) {
redirect('/api/auth/signin')
}
return (
<div className="container mx-auto px-4 py-8">
<ArgoCDApplications />
</div>
)
}

View File

@@ -0,0 +1,28 @@
'use client'
import { useSession } from 'next-auth/react'
import { redirect } from 'next/navigation'
import CrossplaneResourceBrowser from '@/components/crossplane/CrossplaneResourceBrowser'
export default function CrossplanePage() {
const { data: session, status } = useSession()
if (status === 'loading') {
return (
<div className="flex min-h-screen items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
)
}
if (!session) {
redirect('/api/auth/signin')
}
return (
<div className="container mx-auto px-4 py-8">
<CrossplaneResourceBrowser />
</div>
)
}

View File

@@ -0,0 +1,78 @@
'use client';
import { useSession } from 'next-auth/react';
import { signIn } from 'next-auth/react';
import { CostOverviewTile } from '@/components/dashboard/CostOverviewTile';
import { CostForecastingTile } from '@/components/dashboard/CostForecastingTile';
import { ResourceUsageTile } from '@/components/dashboard/ResourceUsageTile';
import { BillingTile } from '@/components/dashboard/BillingTile';
import { ServiceAdoptionTile } from '@/components/dashboard/ServiceAdoptionTile';
import { ComplianceStatusTile } from '@/components/dashboard/ComplianceStatusTile';
import { QuickActionsPanel } from '@/components/dashboard/QuickActionsPanel';
export default function BusinessDashboardPage() {
const { data: session, status } = useSession();
if (status === 'loading') {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-900">
<div className="text-center">
<div className="mb-4 h-8 w-8 animate-spin rounded-full border-4 border-gray-300 border-t-blue-600 mx-auto"></div>
<p className="text-gray-400">Loading...</p>
</div>
</div>
);
}
if (status === 'unauthenticated') {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-900">
<div className="text-center max-w-md mx-auto p-8">
<h1 className="text-2xl font-bold text-white mb-4">Welcome to Nexus Console</h1>
<p className="text-gray-400 mb-6">Please sign in to continue</p>
<button
onClick={() => signIn()}
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
Sign In
</button>
</div>
</div>
);
}
return (
<div className="container mx-auto px-4 py-8">
<div className="mb-6">
<h1 className="text-3xl font-bold text-white">Business Owner Dashboard</h1>
<p className="text-gray-400 mt-2">Cost, usage, billing, and compliance overview</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<CostOverviewTile />
<CostForecastingTile />
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<ResourceUsageTile />
<BillingTile />
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
<BillingTile />
<ServiceAdoptionTile />
<ComplianceStatusTile />
</div>
<QuickActionsPanel
actions={[
{ label: 'View Reports', href: '/reports', icon: 'FileText' },
{ label: 'Manage Billing', href: '/admin/billing', icon: 'CreditCard' },
{ label: 'Contact Support', href: '/support', icon: 'HelpCircle' },
{ label: 'Export Data', href: '/reports/export', icon: 'Download' },
]}
/>
</div>
);
}

View File

@@ -0,0 +1,70 @@
'use client';
import { useSession } from 'next-auth/react';
import { signIn } from 'next-auth/react';
import { APIUsageTile } from '@/components/dashboard/APIUsageTile';
import { DeploymentsTile } from '@/components/dashboard/DeploymentsTile';
import { TestEnvironmentsTile } from '@/components/dashboard/TestEnvironmentsTile';
import { APIKeysTile } from '@/components/dashboard/APIKeysTile';
import { QuickActionsPanel } from '@/components/dashboard/QuickActionsPanel';
export default function DeveloperDashboardPage() {
const { data: session, status } = useSession();
if (status === 'loading') {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-900">
<div className="text-center">
<div className="mb-4 h-8 w-8 animate-spin rounded-full border-4 border-gray-300 border-t-blue-600 mx-auto"></div>
<p className="text-gray-400">Loading...</p>
</div>
</div>
);
}
if (status === 'unauthenticated') {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-900">
<div className="text-center max-w-md mx-auto p-8">
<h1 className="text-2xl font-bold text-white mb-4">Welcome to Nexus Console</h1>
<p className="text-gray-400 mb-6">Please sign in to continue</p>
<button
onClick={() => signIn()}
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
Sign In
</button>
</div>
</div>
);
}
return (
<div className="container mx-auto px-4 py-8">
<div className="mb-6">
<h1 className="text-3xl font-bold text-white">Developer Dashboard</h1>
<p className="text-gray-400 mt-2">API usage, deployments, test environments, and developer tools</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<APIUsageTile />
<DeploymentsTile />
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<TestEnvironmentsTile />
<APIKeysTile />
</div>
<QuickActionsPanel
actions={[
{ label: 'Create API Key', href: '/developer/api-keys/new', icon: 'Key' },
{ label: 'Deploy Service', href: '/developer/deploy', icon: 'Rocket' },
{ label: 'View Docs', href: '/docs', icon: 'Book' },
{ label: 'View Logs', href: '/developer/logs', icon: 'FileText' },
]}
/>
</div>
);
}

View File

@@ -0,0 +1,61 @@
'use client';
import { useSession } from 'next-auth/react';
import { signIn } from 'next-auth/react';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { getDashboardRoute } from '@/lib/roles';
export default function DashboardPage() {
const { data: session, status } = useSession();
const router = useRouter();
useEffect(() => {
if (status === 'authenticated' && session) {
const route = getDashboardRoute(session);
router.push(route);
}
}, [status, session, router]);
if (status === 'loading') {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-900">
<div className="text-center">
<div className="mb-4 h-8 w-8 animate-spin rounded-full border-4 border-gray-300 border-t-blue-600 mx-auto"></div>
<p className="text-gray-400">Loading...</p>
</div>
</div>
);
}
if (status === 'unauthenticated') {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-900">
<div className="text-center max-w-md mx-auto p-8">
<h1 className="text-2xl font-bold text-white mb-4">Welcome to Nexus Console</h1>
<p className="text-gray-400 mb-6">Please sign in to continue</p>
<button
onClick={() => signIn()}
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
Sign In
</button>
<p className="text-sm text-gray-500 mt-4">
Development mode: Use any email/password
</p>
</div>
</div>
);
}
// Redirecting...
return (
<div className="flex min-h-screen items-center justify-center bg-gray-900">
<div className="text-center">
<div className="mb-4 h-8 w-8 animate-spin rounded-full border-4 border-gray-300 border-t-blue-600 mx-auto"></div>
<p className="text-gray-400">Redirecting to your dashboard...</p>
</div>
</div>
);
}

View File

@@ -0,0 +1,79 @@
'use client';
import { useSession } from 'next-auth/react';
import { signIn } from 'next-auth/react';
import { SystemHealthTile } from '@/components/dashboard/SystemHealthTile';
import { IntegrationStatusTile } from '@/components/dashboard/IntegrationStatusTile';
import { DataPipelineTile } from '@/components/dashboard/DataPipelineTile';
import { ResourceUtilizationTile } from '@/components/dashboard/ResourceUtilizationTile';
import { OptimizationEngine } from '@/components/ai/OptimizationEngine';
import { QuickActionsPanel } from '@/components/dashboard/QuickActionsPanel';
export default function TechnicalDashboardPage() {
const { data: session, status } = useSession();
if (status === 'loading') {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-900">
<div className="text-center">
<div className="mb-4 h-8 w-8 animate-spin rounded-full border-4 border-gray-300 border-t-blue-600 mx-auto"></div>
<p className="text-gray-400">Loading...</p>
</div>
</div>
);
}
if (status === 'unauthenticated') {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-900">
<div className="text-center max-w-md mx-auto p-8">
<h1 className="text-2xl font-bold text-white mb-4">Welcome to Nexus Console</h1>
<p className="text-gray-400 mb-6">Please sign in to continue</p>
<button
onClick={() => signIn()}
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
Sign In
</button>
</div>
</div>
);
}
return (
<div className="container mx-auto px-4 py-8">
<div className="mb-6">
<h1 className="text-3xl font-bold text-white">Technical Admin Dashboard</h1>
<p className="text-gray-400 mt-2">Operational view of infrastructure, integrations, and system health</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
<div className="lg:col-span-2">
<SystemHealthTile />
</div>
<div>
<IntegrationStatusTile />
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<DataPipelineTile />
<ResourceUtilizationTile />
</div>
<div className="mb-6">
<OptimizationEngine />
</div>
<QuickActionsPanel
actions={[
{ label: 'Add Integration', href: '/integrations/new', icon: 'Plus' },
{ label: 'Create Connection', href: '/connections/new', icon: 'Link' },
{ label: 'View Logs', href: '/monitoring/logs', icon: 'FileText' },
{ label: 'System Settings', href: '/settings/system', icon: 'Settings' },
]}
/>
</div>
);
}

View File

@@ -0,0 +1,59 @@
'use client';
import { useSession } from 'next-auth/react';
import { signIn } from 'next-auth/react';
// Metrics dashboard - component to be implemented
// import Dashboard from '@/components/dashboards/Dashboard';
export default function DashboardsPage() {
const { data: session, status } = useSession();
if (status === 'loading') {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-900">
<div className="text-center">
<div className="mb-4 h-8 w-8 animate-spin rounded-full border-4 border-gray-300 border-t-blue-600 mx-auto"></div>
<p className="text-gray-400">Loading...</p>
</div>
</div>
);
}
if (status === 'unauthenticated') {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-900">
<div className="text-center max-w-md mx-auto p-8">
<h1 className="text-2xl font-bold text-white mb-4">Access Denied</h1>
<p className="text-gray-400 mb-6">Please sign in to view dashboards</p>
<button
onClick={() => signIn()}
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
Sign In
</button>
</div>
</div>
);
}
return (
<div className="container mx-auto p-6">
<h1 className="text-3xl font-bold mb-6 text-white">Metrics Dashboards</h1>
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
<div className="border rounded-lg p-4 bg-gray-800">
<h2 className="text-xl font-semibold mb-2 text-white">CPU Usage</h2>
<p className="text-gray-400">CPU metrics dashboard coming soon</p>
</div>
<div className="border rounded-lg p-4 bg-gray-800">
<h2 className="text-xl font-semibold mb-2 text-white">Memory Usage</h2>
<p className="text-gray-400">Memory metrics dashboard coming soon</p>
</div>
<div className="border rounded-lg p-4 bg-gray-800">
<h2 className="text-xl font-semibold mb-2 text-white">Network Throughput</h2>
<p className="text-gray-400">Network metrics dashboard coming soon</p>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,127 @@
'use client';
import { useSession } from 'next-auth/react';
import { signIn } from 'next-auth/react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
import { Key, Book, TestTube, BarChart3, Webhook, Download, ArrowRight } from 'lucide-react';
import Link from 'next/link';
export default function DeveloperPortalPage() {
const { data: session, status } = useSession();
if (status === 'loading') {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-900">
<div className="text-center">
<div className="mb-4 h-8 w-8 animate-spin rounded-full border-4 border-gray-300 border-t-blue-600 mx-auto"></div>
<p className="text-gray-400">Loading...</p>
</div>
</div>
);
}
if (status === 'unauthenticated') {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-900">
<div className="text-center max-w-md mx-auto p-8">
<h1 className="text-2xl font-bold text-white mb-4">Welcome to Developer Portal</h1>
<p className="text-gray-400 mb-6">Please sign in to continue</p>
<button
onClick={() => signIn()}
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
Sign In
</button>
</div>
</div>
);
}
const developerSections = [
{
title: 'API Key Management',
description: 'Create, manage, and revoke API keys for your applications',
icon: Key,
href: '/developer/api-keys',
features: ['Create API keys', 'View usage', 'Revoke keys'],
},
{
title: 'API Documentation',
description: 'Interactive API documentation with examples and code snippets',
icon: Book,
href: '/developer/docs',
features: ['REST API docs', 'GraphQL schema', 'Code examples'],
},
{
title: 'Test Environments',
description: 'Provision and manage sandbox environments for testing',
icon: TestTube,
href: '/developer/environments',
features: ['Create environments', 'Sandbox access', 'Auto-cleanup'],
},
{
title: 'Usage Analytics',
description: 'Monitor API usage, quotas, and performance metrics',
icon: BarChart3,
href: '/developer/analytics',
features: ['API usage stats', 'Quota monitoring', 'Performance metrics'],
},
{
title: 'Webhook Configuration',
description: 'Configure webhooks for real-time event notifications',
icon: Webhook,
href: '/developer/webhooks',
features: ['Create webhooks', 'Event subscriptions', 'Delivery logs'],
},
{
title: 'SDK Downloads',
description: 'Download SDKs and client libraries for popular languages',
icon: Download,
href: '/developer/sdks',
features: ['TypeScript SDK', 'Python SDK', 'Go SDK'],
},
];
return (
<div className="container mx-auto px-4 py-8">
<div className="mb-8">
<h1 className="text-3xl font-bold text-white mb-2">Developer Portal</h1>
<p className="text-gray-400">API keys, documentation, test environments, and developer tools</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{developerSections.map((section) => {
const Icon = section.icon;
return (
<Card key={section.href} className="bg-gray-800 border-gray-700 hover:border-orange-500 transition-colors">
<CardHeader>
<div className="flex items-center gap-3 mb-2">
<Icon className="h-6 w-6 text-orange-500" />
<CardTitle className="text-white">{section.title}</CardTitle>
</div>
<p className="text-sm text-gray-400">{section.description}</p>
</CardHeader>
<CardContent>
<ul className="space-y-2 mb-4">
{section.features.map((feature) => (
<li key={feature} className="flex items-center gap-2 text-sm text-gray-300">
<span className="text-orange-500"></span>
<span>{feature}</span>
</li>
))}
</ul>
<Link
href={section.href}
className="inline-flex items-center gap-2 text-sm text-orange-500 hover:text-orange-400 transition-colors"
>
Access <ArrowRight className="h-4 w-4" />
</Link>
</CardContent>
</Card>
);
})}
</div>
</div>
);
}

View File

@@ -0,0 +1,191 @@
'use client';
import { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
import { Send, CheckCircle, XCircle, Clock } from 'lucide-react';
export default function WebhookTestingPage() {
const [url, setUrl] = useState('');
const [method, setMethod] = useState('POST');
const [headers, setHeaders] = useState('{"Content-Type": "application/json"}');
const [payload, setPayload] = useState('{"event": "test", "data": {}}');
const [testResult, setTestResult] = useState<any>(null);
const [isTesting, setIsTesting] = useState(false);
const testWebhook = async () => {
setIsTesting(true);
setTestResult(null);
try {
const startTime = Date.now();
const response = await fetch('/api/webhooks/test', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url,
method,
headers: JSON.parse(headers),
payload: JSON.parse(payload),
}),
});
const endTime = Date.now();
const data = await response.json();
setTestResult({
success: response.ok,
status: response.status,
statusText: response.statusText,
responseTime: endTime - startTime,
response: data,
timestamp: new Date().toISOString(),
});
} catch (error: any) {
setTestResult({
success: false,
error: error.message,
timestamp: new Date().toISOString(),
});
} finally {
setIsTesting(false);
}
};
return (
<div className="container mx-auto px-4 py-8">
<div className="mb-8">
<h1 className="text-3xl font-bold text-white mb-2">Webhook Testing Tool</h1>
<p className="text-gray-400">Test your webhook endpoints before configuring them</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card className="bg-gray-800 border-gray-700">
<CardHeader>
<CardTitle className="text-white">Webhook Configuration</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<label className="block text-sm text-gray-300 mb-2">Webhook URL</label>
<input
type="url"
value={url}
onChange={(e) => setUrl(e.target.value)}
placeholder="https://example.com/webhook"
className="w-full px-4 py-2 bg-gray-900 border border-gray-700 rounded text-white"
/>
</div>
<div>
<label className="block text-sm text-gray-300 mb-2">HTTP Method</label>
<select
value={method}
onChange={(e) => setMethod(e.target.value)}
className="w-full px-4 py-2 bg-gray-900 border border-gray-700 rounded text-white"
>
<option value="POST">POST</option>
<option value="GET">GET</option>
<option value="PUT">PUT</option>
<option value="PATCH">PATCH</option>
<option value="DELETE">DELETE</option>
</select>
</div>
<div>
<label className="block text-sm text-gray-300 mb-2">Headers (JSON)</label>
<textarea
value={headers}
onChange={(e) => setHeaders(e.target.value)}
className="w-full h-24 px-4 py-2 bg-gray-900 border border-gray-700 rounded text-white font-mono text-sm"
placeholder='{"Content-Type": "application/json"}'
/>
</div>
<div>
<label className="block text-sm text-gray-300 mb-2">Payload (JSON)</label>
<textarea
value={payload}
onChange={(e) => setPayload(e.target.value)}
className="w-full h-32 px-4 py-2 bg-gray-900 border border-gray-700 rounded text-white font-mono text-sm"
placeholder='{"event": "test", "data": {}}'
/>
</div>
<button
onClick={testWebhook}
disabled={!url || isTesting}
className="w-full px-6 py-3 bg-orange-500 text-white rounded-lg hover:bg-orange-600 transition-colors disabled:opacity-50 flex items-center justify-center gap-2"
>
<Send className="h-4 w-4" />
{isTesting ? 'Testing...' : 'Test Webhook'}
</button>
</CardContent>
</Card>
<Card className="bg-gray-800 border-gray-700">
<CardHeader>
<CardTitle className="text-white">Test Results</CardTitle>
</CardHeader>
<CardContent>
{!testResult ? (
<div className="text-center py-12 text-gray-400">
<p>Test results will appear here</p>
</div>
) : (
<div className="space-y-4">
<div className="flex items-center gap-2">
{testResult.success ? (
<CheckCircle className="h-5 w-5 text-green-400" />
) : (
<XCircle className="h-5 w-5 text-red-400" />
)}
<span className={`font-semibold ${testResult.success ? 'text-green-400' : 'text-red-400'}`}>
{testResult.success ? 'Success' : 'Failed'}
</span>
</div>
{testResult.status && (
<div>
<p className="text-sm text-gray-400 mb-1">Status</p>
<p className="text-white font-mono">
{testResult.status} {testResult.statusText}
</p>
</div>
)}
{testResult.responseTime && (
<div>
<p className="text-sm text-gray-400 mb-1">Response Time</p>
<p className="text-white font-mono">{testResult.responseTime}ms</p>
</div>
)}
{testResult.timestamp && (
<div>
<p className="text-sm text-gray-400 mb-1">Timestamp</p>
<p className="text-white text-sm">{new Date(testResult.timestamp).toLocaleString()}</p>
</div>
)}
{testResult.response && (
<div>
<p className="text-sm text-gray-400 mb-1">Response</p>
<pre className="p-3 bg-gray-900 rounded text-white font-mono text-xs overflow-auto max-h-64">
{JSON.stringify(testResult.response, null, 2)}
</pre>
</div>
)}
{testResult.error && (
<div>
<p className="text-sm text-gray-400 mb-1">Error</p>
<p className="text-red-400 font-mono text-sm">{testResult.error}</p>
</div>
)}
</div>
)}
</CardContent>
</Card>
</div>
</div>
);
}

50
portal/src/app/error.tsx Normal file
View File

@@ -0,0 +1,50 @@
'use client'
import { useEffect } from 'react'
import Link from 'next/link'
import { Home, RefreshCw, AlertCircle } from 'lucide-react'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
// Log error to error reporting service
console.error('Application error:', error)
}, [error])
return (
<div className="flex min-h-screen items-center justify-center bg-gray-900">
<div className="text-center max-w-md mx-auto p-8">
<AlertCircle className="h-16 w-16 text-red-500 mx-auto mb-4" />
<h1 className="text-3xl font-bold text-white mb-4">Something went wrong!</h1>
<p className="text-gray-400 mb-2">
An unexpected error occurred. Please try again.
</p>
{error.digest && (
<p className="text-sm text-gray-500 mb-8">Error ID: {error.digest}</p>
)}
<div className="flex gap-4 justify-center">
<button
onClick={reset}
className="flex items-center gap-2 px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
<RefreshCw className="h-5 w-5" />
Try Again
</button>
<Link
href="/"
className="flex items-center gap-2 px-6 py-3 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors"
>
<Home className="h-5 w-5" />
Go Home
</Link>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,28 @@
'use client'
import { useSession } from 'next-auth/react'
import { redirect } from 'next/navigation'
import KubernetesClusters from '@/components/kubernetes/KubernetesClusters'
export default function KubernetesPage() {
const { data: session, status } = useSession()
if (status === 'loading') {
return (
<div className="flex min-h-screen items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
)
}
if (!session) {
redirect('/api/auth/signin')
}
return (
<div className="container mx-auto px-4 py-8">
<KubernetesClusters />
</div>
)
}

View File

@@ -1,26 +1,44 @@
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';
import { Providers } from './providers';
'use client'
const inter = Inter({ subsets: ['latin'] });
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'
import { Providers } from './providers'
import { PortalHeader } from '@/components/layout/PortalHeader'
import { PortalSidebar } from '@/components/layout/PortalSidebar'
import { PortalBreadcrumbs } from '@/components/layout/PortalBreadcrumbs'
import { MobileNavigation } from '@/components/layout/MobileNavigation'
import { KeyboardShortcutsProvider } from '@/components/KeyboardShortcutsProvider'
import { SessionProvider } from 'next-auth/react'
export const metadata: Metadata = {
title: 'Hybrid Cloud Control Plane',
description: 'Unified management portal for hybrid cloud infrastructure',
};
const inter = Inter({ subsets: ['latin'] })
export default function RootLayout({
children,
}: {
children: React.ReactNode;
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={inter.className}>
<Providers>{children}</Providers>
<SessionProvider>
<Providers>
<KeyboardShortcutsProvider>
<div className="flex min-h-screen flex-col">
<PortalHeader />
<PortalBreadcrumbs />
<div className="flex flex-1">
<PortalSidebar />
<main className="flex-1 ml-0 md:ml-64">
{children}
</main>
</div>
<MobileNavigation />
</div>
</KeyboardShortcutsProvider>
</Providers>
</SessionProvider>
</body>
</html>
);
)
}

View File

@@ -0,0 +1,29 @@
'use client'
import { useSession } from 'next-auth/react'
export default function MLPage() {
const { data: session, status } = useSession()
if (status === 'loading') {
return <div>Loading...</div>
}
if (status === 'unauthenticated') {
return <div>Please sign in</div>
}
return (
<div className="container mx-auto p-6">
<h1 className="text-3xl font-bold mb-6">AI Foundry</h1>
<p className="text-muted-foreground mb-4">
ML platform for model training, inference, and pipeline management
</p>
{/* ML platform UI will be implemented here */}
<div className="border rounded-lg p-4">
<p className="text-sm text-muted-foreground">ML platform interface coming soon</p>
</div>
</div>
)
}

View File

@@ -0,0 +1,62 @@
'use client'
import { useSession } from 'next-auth/react'
import { redirect } from 'next/navigation'
import GrafanaPanel from '@/components/monitoring/GrafanaPanel'
import LokiLogViewer from '@/components/monitoring/LokiLogViewer'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/Tabs'
export default function MonitoringPage() {
const { data: session, status } = useSession()
if (status === 'loading') {
return (
<div className="flex min-h-screen items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
)
}
if (!session) {
redirect('/api/auth/signin')
}
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-6">Monitoring</h1>
<Tabs defaultValue="grafana" className="space-y-4">
<TabsList>
<TabsTrigger value="grafana">Grafana Dashboards</TabsTrigger>
<TabsTrigger value="logs">Logs (Loki)</TabsTrigger>
</TabsList>
<TabsContent value="grafana" className="space-y-4">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div>
<h2 className="text-xl font-semibold mb-4">System Metrics</h2>
<GrafanaPanel
dashboardUID="system"
panelId={1}
height="400px"
/>
</div>
<div>
<h2 className="text-xl font-semibold mb-4">Application Metrics</h2>
<GrafanaPanel
dashboardUID="application"
panelId={2}
height="400px"
/>
</div>
</div>
</TabsContent>
<TabsContent value="logs">
<LokiLogViewer />
</TabsContent>
</Tabs>
</div>
)
}

View File

@@ -0,0 +1,51 @@
'use client';
import { useSession } from 'next-auth/react';
import { signIn } from 'next-auth/react';
// Network topology view - component to be implemented
// import { NetworkTopologyView } from '@/components/network/NetworkTopologyView';
export default function NetworkPage() {
const { data: session, status } = useSession();
if (status === 'loading') {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-900">
<div className="text-center">
<div className="mb-4 h-8 w-8 animate-spin rounded-full border-4 border-gray-300 border-t-blue-600 mx-auto"></div>
<p className="text-gray-400">Loading...</p>
</div>
</div>
);
}
if (status === 'unauthenticated') {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-900">
<div className="text-center max-w-md mx-auto p-8">
<h1 className="text-2xl font-bold text-white mb-4">Access Denied</h1>
<p className="text-gray-400 mb-6">Please sign in to view network topology</p>
<button
onClick={() => signIn()}
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
Sign In
</button>
</div>
</div>
);
}
return (
<div className="container mx-auto p-6">
<h1 className="text-3xl font-bold mb-6 text-white">Network Topology</h1>
<div className="border rounded-lg p-4 bg-gray-800">
<p className="text-gray-400">Network topology visualization coming soon</p>
<p className="text-sm text-gray-500 mt-2">
This will display the network graph showing relationships between resources across Proxmox, Kubernetes, and Cloudflare.
</p>
</div>
</div>
);
}

View File

@@ -0,0 +1,35 @@
'use client'
import Link from 'next/link'
import { Home, ArrowLeft } from 'lucide-react'
export default function NotFound() {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-900">
<div className="text-center max-w-md mx-auto p-8">
<h1 className="text-6xl font-bold text-white mb-4">404</h1>
<h2 className="text-2xl font-semibold text-gray-300 mb-4">Page Not Found</h2>
<p className="text-gray-400 mb-8">
The page you're looking for doesn't exist or has been moved.
</p>
<div className="flex gap-4 justify-center">
<Link
href="/"
className="flex items-center gap-2 px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
<Home className="h-5 w-5" />
Go Home
</Link>
<button
onClick={() => window.history.back()}
className="flex items-center gap-2 px-6 py-3 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors"
>
<ArrowLeft className="h-5 w-5" />
Go Back
</button>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,37 @@
'use client';
import { OnboardingWizard } from '@/components/onboarding/OnboardingWizard';
import { WelcomeStep } from '@/components/onboarding/steps/WelcomeStep';
import { ProfileStep } from '@/components/onboarding/steps/ProfileStep';
import { PreferencesStep } from '@/components/onboarding/steps/PreferencesStep';
const onboardingSteps = [
{
id: 'welcome',
title: 'Welcome',
description: 'Get started with Nexus Console',
component: WelcomeStep,
},
{
id: 'profile',
title: 'Profile',
description: 'Set up your profile information',
component: ProfileStep,
},
{
id: 'preferences',
title: 'Preferences',
description: 'Configure your preferences',
component: PreferencesStep,
},
];
export default function OnboardingPage() {
const handleComplete = () => {
// Mark onboarding as complete
localStorage.setItem('onboarding-complete', 'true');
};
return <OnboardingWizard steps={onboardingSteps} onComplete={handleComplete} />;
}

View File

@@ -0,0 +1,113 @@
'use client';
import { useSession } from 'next-auth/react';
import { signIn } from 'next-auth/react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
import { Handshake, TrendingUp, BookOpen, Package, ArrowRight } from 'lucide-react';
import Link from 'next/link';
export default function PartnerPortalPage() {
const { data: session, status } = useSession();
if (status === 'loading') {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-900">
<div className="text-center">
<div className="mb-4 h-8 w-8 animate-spin rounded-full border-4 border-gray-300 border-t-blue-600 mx-auto"></div>
<p className="text-gray-400">Loading...</p>
</div>
</div>
);
}
if (status === 'unauthenticated') {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-900">
<div className="text-center max-w-md mx-auto p-8">
<h1 className="text-2xl font-bold text-white mb-4">Welcome to Partner Portal</h1>
<p className="text-gray-400 mb-6">Please sign in to continue</p>
<button
onClick={() => signIn()}
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
Sign In
</button>
</div>
</div>
);
}
const partnerSections = [
{
title: 'Co-Sell Deal Management',
description: 'Register and manage co-sell deals with Sankofa',
icon: Handshake,
href: '/partner/deals',
features: ['Deal registration', 'Deal tracking', 'Revenue sharing'],
},
{
title: 'Technical Onboarding',
description: 'Access technical resources and onboarding materials',
icon: BookOpen,
href: '/partner/onboarding',
features: ['Technical training', 'Architecture guides', 'Best practices'],
},
{
title: 'Solution Registration',
description: 'Register your solutions in the marketplace',
icon: Package,
href: '/partner/solutions',
features: ['Solution listing', 'Certification', 'Marketplace access'],
},
{
title: 'Partner Resources',
description: 'Marketing materials, sales enablement, and training resources',
icon: TrendingUp,
href: '/partner/resources',
features: ['Marketing assets', 'Sales materials', 'Training programs'],
},
];
return (
<div className="container mx-auto px-4 py-8">
<div className="mb-8">
<h1 className="text-3xl font-bold text-white mb-2">Partner Portal</h1>
<p className="text-gray-400">Co-sell deals, technical onboarding, and solution registration</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{partnerSections.map((section) => {
const Icon = section.icon;
return (
<Card key={section.href} className="bg-gray-800 border-gray-700 hover:border-orange-500 transition-colors">
<CardHeader>
<div className="flex items-center gap-3 mb-2">
<Icon className="h-6 w-6 text-orange-500" />
<CardTitle className="text-white">{section.title}</CardTitle>
</div>
<p className="text-sm text-gray-400">{section.description}</p>
</CardHeader>
<CardContent>
<ul className="space-y-2 mb-4">
{section.features.map((feature) => (
<li key={feature} className="flex items-center gap-2 text-sm text-gray-300">
<span className="text-orange-500"></span>
<span>{feature}</span>
</li>
))}
</ul>
<Link
href={section.href}
className="inline-flex items-center gap-2 text-sm text-orange-500 hover:text-orange-400 transition-colors"
>
Access <ArrowRight className="h-4 w-4" />
</Link>
</CardContent>
</Card>
);
})}
</div>
</div>
);
}

View File

@@ -0,0 +1,29 @@
'use client'
import { useSession } from 'next-auth/react'
export default function PoliciesPage() {
const { data: session, status } = useSession()
if (status === 'loading') {
return <div>Loading...</div>
}
if (status === 'unauthenticated') {
return <div>Please sign in</div>
}
return (
<div className="container mx-auto p-6">
<h1 className="text-3xl font-bold mb-6">Policy Management</h1>
<p className="text-muted-foreground mb-4">
Manage compliance, security, and cost optimization policies
</p>
{/* Policy management UI will be implemented here */}
<div className="border rounded-lg p-4">
<p className="text-sm text-muted-foreground">Policy management interface coming soon</p>
</div>
</div>
)
}

View File

@@ -0,0 +1,29 @@
'use client'
import { useSession } from 'next-auth/react'
export default function ResourceGraphPage() {
const { data: session, status } = useSession()
if (status === 'loading') {
return <div>Loading...</div>
}
if (status === 'unauthenticated') {
return <div>Please sign in</div>
}
return (
<div className="container mx-auto p-6">
<h1 className="text-3xl font-bold mb-6">Resource Graph</h1>
<p className="text-muted-foreground mb-4">
Visualize resource relationships and dependencies
</p>
{/* React Flow graph visualization will be implemented here */}
<div className="border rounded-lg p-4 h-96">
<p className="text-sm text-muted-foreground">Resource graph visualization coming soon</p>
</div>
</div>
)
}

View File

@@ -0,0 +1,30 @@
'use client'
import { useState } from 'react'
import { useSession } from 'next-auth/react'
export default function ResourcesPage() {
const { data: session, status } = useSession()
if (status === 'loading') {
return <div>Loading...</div>
}
if (status === 'unauthenticated') {
return <div>Please sign in</div>
}
return (
<div className="container mx-auto p-6">
<h1 className="text-3xl font-bold mb-6">Resource Inventory</h1>
<p className="text-muted-foreground mb-4">
Unified view of all resources across Proxmox, Kubernetes, and Cloudflare
</p>
{/* Resource inventory UI will be implemented here */}
<div className="border rounded-lg p-4">
<p className="text-sm text-muted-foreground">Resource inventory table coming soon</p>
</div>
</div>
)
}

View File

@@ -0,0 +1,157 @@
'use client';
import { useSession } from 'next-auth/react';
import { useState } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/Card';
import { Shield, CheckCircle, XCircle } from 'lucide-react';
import QRCode from 'qrcode';
export default function TwoFactorAuthPage() {
const { data: session } = useSession();
const [isEnabled, setIsEnabled] = useState(false);
const [qrCode, setQrCode] = useState<string | null>(null);
const [secret, setSecret] = useState<string | null>(null);
const [verificationCode, setVerificationCode] = useState('');
const [isVerifying, setIsVerifying] = useState(false);
const enable2FA = async () => {
try {
// In production, this would call your backend API
const response = await fetch('/api/auth/2fa/setup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
});
const data = await response.json();
setSecret(data.secret);
const qr = await QRCode.toDataURL(data.qrCodeUrl);
setQrCode(qr);
} catch (error) {
console.error('Failed to enable 2FA:', error);
}
};
const verifyAndEnable = async () => {
setIsVerifying(true);
try {
const response = await fetch('/api/auth/2fa/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code: verificationCode, secret }),
});
if (response.ok) {
setIsEnabled(true);
setQrCode(null);
setSecret(null);
}
} catch (error) {
console.error('Verification failed:', error);
} finally {
setIsVerifying(false);
}
};
const disable2FA = async () => {
try {
await fetch('/api/auth/2fa/disable', { method: 'POST' });
setIsEnabled(false);
} catch (error) {
console.error('Failed to disable 2FA:', error);
}
};
return (
<div className="container mx-auto px-4 py-8">
<div className="mb-8">
<h1 className="text-3xl font-bold text-white mb-2">Two-Factor Authentication</h1>
<p className="text-gray-400">Add an extra layer of security to your account</p>
</div>
<Card className="bg-gray-800 border-gray-700 max-w-2xl">
<CardHeader>
<div className="flex items-center gap-3">
<Shield className="h-6 w-6 text-orange-500" />
<div>
<CardTitle className="text-white">2FA Status</CardTitle>
<CardDescription className="text-gray-400">
{isEnabled ? 'Two-factor authentication is enabled' : 'Two-factor authentication is disabled'}
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent>
{!isEnabled && !qrCode && (
<div>
<p className="text-gray-300 mb-4">
Enable two-factor authentication to protect your account with an additional security layer.
</p>
<button
onClick={enable2FA}
className="px-6 py-3 bg-orange-500 text-white rounded-lg hover:bg-orange-600 transition-colors"
>
Enable 2FA
</button>
</div>
)}
{qrCode && secret && (
<div className="space-y-4">
<div>
<p className="text-gray-300 mb-2">Scan this QR code with your authenticator app:</p>
<div className="flex justify-center p-4 bg-white rounded">
<img src={qrCode} alt="2FA QR Code" className="w-48 h-48" />
</div>
</div>
<div>
<p className="text-gray-300 mb-2">Or enter this secret manually:</p>
<code className="block p-3 bg-gray-900 rounded text-white font-mono text-sm break-all">
{secret}
</code>
</div>
<div>
<label className="block text-sm text-gray-300 mb-2">
Enter verification code from your app:
</label>
<input
type="text"
value={verificationCode}
onChange={(e) => setVerificationCode(e.target.value)}
placeholder="000000"
maxLength={6}
className="w-full px-4 py-2 bg-gray-900 border border-gray-700 rounded text-white"
/>
</div>
<button
onClick={verifyAndEnable}
disabled={verificationCode.length !== 6 || isVerifying}
className="px-6 py-3 bg-orange-500 text-white rounded-lg hover:bg-orange-600 transition-colors disabled:opacity-50"
>
{isVerifying ? 'Verifying...' : 'Verify and Enable'}
</button>
</div>
)}
{isEnabled && (
<div className="space-y-4">
<div className="flex items-center gap-2 text-green-400">
<CheckCircle className="h-5 w-5" />
<span>Two-factor authentication is active</span>
</div>
<button
onClick={disable2FA}
className="px-6 py-3 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors"
>
Disable 2FA
</button>
</div>
)}
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,132 @@
'use client';
import { useSession } from 'next-auth/react';
import { signIn } from 'next-auth/react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
import { Settings, User, Bell, Shield, Key } from 'lucide-react';
export default function SettingsPage() {
const { data: session, status } = useSession();
if (status === 'loading') {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-900">
<div className="text-center">
<div className="mb-4 h-8 w-8 animate-spin rounded-full border-4 border-gray-300 border-t-blue-600 mx-auto"></div>
<p className="text-gray-400">Loading...</p>
</div>
</div>
);
}
if (status === 'unauthenticated') {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-900">
<div className="text-center max-w-md mx-auto p-8">
<h1 className="text-2xl font-bold text-white mb-4">Access Denied</h1>
<p className="text-gray-400 mb-6">Please sign in to access settings</p>
<button
onClick={() => signIn()}
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
Sign In
</button>
</div>
</div>
);
}
return (
<div className="container mx-auto p-6">
<h1 className="text-3xl font-bold mb-6 text-white">Settings</h1>
<div className="grid gap-6 md:grid-cols-2">
<Card>
<CardHeader>
<div className="flex items-center gap-3">
<User className="h-5 w-5" />
<CardTitle>Profile Settings</CardTitle>
</div>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div>
<label className="text-sm font-medium text-gray-300">Email</label>
<p className="text-white">{session?.user?.email || 'Not available'}</p>
</div>
<div>
<label className="text-sm font-medium text-gray-300">Name</label>
<p className="text-white">{session?.user?.name || 'Not available'}</p>
</div>
<button className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
Edit Profile
</button>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<div className="flex items-center gap-3">
<Bell className="h-5 w-5" />
<CardTitle>Notifications</CardTitle>
</div>
</CardHeader>
<CardContent>
<div className="space-y-4">
<label className="flex items-center gap-2">
<input type="checkbox" className="rounded" />
<span className="text-white">Email notifications</span>
</label>
<label className="flex items-center gap-2">
<input type="checkbox" className="rounded" />
<span className="text-white">Alert notifications</span>
</label>
<label className="flex items-center gap-2">
<input type="checkbox" className="rounded" />
<span className="text-white">Weekly reports</span>
</label>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<div className="flex items-center gap-3">
<Shield className="h-5 w-5" />
<CardTitle>Security</CardTitle>
</div>
</CardHeader>
<CardContent>
<div className="space-y-4">
<button className="w-full px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
Change Password
</button>
<button className="w-full px-4 py-2 bg-gray-600 text-white rounded hover:bg-gray-700">
Enable Two-Factor Authentication
</button>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<div className="flex items-center gap-3">
<Key className="h-5 w-5" />
<CardTitle>API Keys</CardTitle>
</div>
</CardHeader>
<CardContent>
<div className="space-y-4">
<p className="text-sm text-gray-400">Manage your API keys and tokens</p>
<button className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
Create API Key
</button>
</div>
</CardContent>
</Card>
</div>
</div>
);
}

View File

@@ -0,0 +1,38 @@
'use client'
import Link from 'next/link'
import { Shield, Home, Lock } from 'lucide-react'
export default function Unauthorized() {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-900">
<div className="text-center max-w-md mx-auto p-8">
<Shield className="h-16 w-16 text-yellow-500 mx-auto mb-4" />
<h1 className="text-3xl font-bold text-white mb-4">Access Denied</h1>
<p className="text-gray-400 mb-2">
You don't have permission to access this resource.
</p>
<p className="text-sm text-gray-500 mb-8">
Please contact your administrator if you believe this is an error.
</p>
<div className="flex gap-4 justify-center">
<Link
href="/"
className="flex items-center gap-2 px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
<Home className="h-5 w-5" />
Go Home
</Link>
<button
onClick={() => window.history.back()}
className="flex items-center gap-2 px-6 py-3 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors"
>
<Lock className="h-5 w-5" />
Go Back
</button>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,29 @@
'use client'
import { useSession } from 'next-auth/react'
export default function VMScaleSetsPage() {
const { data: session, status } = useSession()
if (status === 'loading') {
return <div>Loading...</div>
}
if (status === 'unauthenticated') {
return <div>Please sign in</div>
}
return (
<div className="container mx-auto p-6">
<h1 className="text-3xl font-bold mb-6">VM Scale Sets</h1>
<p className="text-muted-foreground mb-4">
Manage auto-scaling VM groups with metrics and scaling policies
</p>
{/* VM Scale Sets UI will be implemented here */}
<div className="border rounded-lg p-4">
<p className="text-sm text-muted-foreground">VM Scale Sets management interface coming soon</p>
</div>
</div>
)
}

View File

@@ -0,0 +1,71 @@
'use client';
import { useSession } from 'next-auth/react';
import { signIn } from 'next-auth/react';
// Well-Architected Framework dashboard - component to be implemented
// import WAFDashboard from '@/components/well-architected/WAFDashboard';
export default function WellArchitectedPage() {
const { data: session, status } = useSession();
if (status === 'loading') {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-900">
<div className="text-center">
<div className="mb-4 h-8 w-8 animate-spin rounded-full border-4 border-gray-300 border-t-blue-600 mx-auto"></div>
<p className="text-gray-400">Loading...</p>
</div>
</div>
);
}
if (status === 'unauthenticated') {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-900">
<div className="text-center max-w-md mx-auto p-8">
<h1 className="text-2xl font-bold text-white mb-4">Access Denied</h1>
<p className="text-gray-400 mb-6">Please sign in to view Well-Architected Framework</p>
<button
onClick={() => signIn()}
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
Sign In
</button>
</div>
</div>
);
}
return (
<div className="container mx-auto p-6">
<h1 className="text-3xl font-bold mb-6 text-white">Well-Architected Framework</h1>
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
<div className="border rounded-lg p-4 bg-gray-800">
<h2 className="text-xl font-semibold mb-2 text-white">Security</h2>
<p className="text-gray-400">Security pillar assessment coming soon</p>
</div>
<div className="border rounded-lg p-4 bg-gray-800">
<h2 className="text-xl font-semibold mb-2 text-white">Reliability</h2>
<p className="text-gray-400">Reliability pillar assessment coming soon</p>
</div>
<div className="border rounded-lg p-4 bg-gray-800">
<h2 className="text-xl font-semibold mb-2 text-white">Performance</h2>
<p className="text-gray-400">Performance pillar assessment coming soon</p>
</div>
<div className="border rounded-lg p-4 bg-gray-800">
<h2 className="text-xl font-semibold mb-2 text-white">Cost Optimization</h2>
<p className="text-gray-400">Cost optimization assessment coming soon</p>
</div>
<div className="border rounded-lg p-4 bg-gray-800">
<h2 className="text-xl font-semibold mb-2 text-white">Operational Excellence</h2>
<p className="text-gray-400">Operational excellence assessment coming soon</p>
</div>
<div className="border rounded-lg p-4 bg-gray-800">
<h2 className="text-xl font-semibold mb-2 text-white">Sustainability</h2>
<p className="text-gray-400">Sustainability assessment coming soon</p>
</div>
</div>
</div>
);
}

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

View File

@@ -0,0 +1,85 @@
import { useQuery } from '@apollo/client'
import { GET_SYSTEM_HEALTH, GET_COST_OVERVIEW, GET_BILLING_INFO, GET_API_USAGE, GET_DEPLOYMENTS, GET_TEST_ENVIRONMENTS, GET_API_KEYS } from '@/lib/graphql/queries/dashboard'
import { useSession } from 'next-auth/react'
export function useSystemHealth() {
return useQuery(GET_SYSTEM_HEALTH, {
pollInterval: 30000, // Poll every 30 seconds
errorPolicy: 'all',
})
}
export function useCostOverview(tenantId?: string) {
const { data: session } = useSession()
const defaultTenantId = tenantId || (session as any)?.tenantId
const endDate = new Date()
const startDate = new Date()
startDate.setDate(startDate.getDate() - 30)
return useQuery(GET_COST_OVERVIEW, {
variables: {
tenantId: defaultTenantId || 'default',
timeRange: {
start: startDate.toISOString(),
end: endDate.toISOString(),
},
},
skip: !defaultTenantId,
errorPolicy: 'all',
})
}
export function useBillingInfo(tenantId?: string) {
const { data: session } = useSession()
const defaultTenantId = tenantId || (session as any)?.tenantId
return useQuery(GET_BILLING_INFO, {
variables: {
tenantId: defaultTenantId || 'default',
},
skip: !defaultTenantId,
errorPolicy: 'all',
})
}
export function useAPIUsage() {
const endDate = new Date()
const startDate = new Date()
startDate.setDate(startDate.getDate() - 7)
return useQuery(GET_API_USAGE, {
variables: {
timeRange: {
start: startDate.toISOString(),
end: endDate.toISOString(),
},
},
pollInterval: 60000, // Poll every minute
errorPolicy: 'all',
})
}
export function useDeployments(filter?: { status?: string; limit?: number }) {
return useQuery(GET_DEPLOYMENTS, {
variables: {
filter: filter || { limit: 10 },
},
pollInterval: 30000,
errorPolicy: 'all',
})
}
export function useTestEnvironments() {
return useQuery(GET_TEST_ENVIRONMENTS, {
pollInterval: 30000,
errorPolicy: 'all',
})
}
export function useAPIKeys() {
return useQuery(GET_API_KEYS, {
errorPolicy: 'all',
})
}

View File

@@ -0,0 +1,59 @@
'use client';
import { useState, useEffect } from 'react';
export interface DashboardTile {
id: string;
component: string;
title: string;
visible: boolean;
order: number;
}
export function useDashboardLayout(role: string) {
const storageKey = `dashboard-layout-${role}`;
const [tiles, setTiles] = useState<DashboardTile[]>(() => {
if (typeof window === 'undefined') return [];
const saved = localStorage.getItem(storageKey);
return saved ? JSON.parse(saved) : getDefaultTiles(role);
});
useEffect(() => {
if (typeof window !== 'undefined') {
localStorage.setItem(storageKey, JSON.stringify(tiles));
}
}, [tiles, storageKey]);
const updateTiles = (newTiles: DashboardTile[]) => {
setTiles(newTiles.map((tile, index) => ({ ...tile, order: index })));
};
return { tiles, updateTiles };
}
function getDefaultTiles(role: string): DashboardTile[] {
const roleTiles: Record<string, DashboardTile[]> = {
technical: [
{ id: 'system-health', component: 'SystemHealthTile', title: 'System Health', visible: true, order: 0 },
{ id: 'integrations', component: 'IntegrationStatusTile', title: 'Integrations', visible: true, order: 1 },
{ id: 'pipelines', component: 'DataPipelineTile', title: 'Data Pipelines', visible: true, order: 2 },
{ id: 'resources', component: 'ResourceUtilizationTile', title: 'Resource Utilization', visible: true, order: 3 },
],
business: [
{ id: 'cost', component: 'CostOverviewTile', title: 'Cost Overview', visible: true, order: 0 },
{ id: 'usage', component: 'ResourceUsageTile', title: 'Resource Usage', visible: true, order: 1 },
{ id: 'billing', component: 'BillingTile', title: 'Billing', visible: true, order: 2 },
{ id: 'compliance', component: 'ComplianceStatusTile', title: 'Compliance', visible: true, order: 3 },
],
developer: [
{ id: 'api-usage', component: 'APIUsageTile', title: 'API Usage', visible: true, order: 0 },
{ id: 'deployments', component: 'DeploymentsTile', title: 'Deployments', visible: true, order: 1 },
{ id: 'environments', component: 'TestEnvironmentsTile', title: 'Test Environments', visible: true, order: 2 },
{ id: 'api-keys', component: 'APIKeysTile', title: 'API Keys', visible: true, order: 3 },
],
};
return roleTiles[role] || roleTiles.technical;
}

View File

@@ -0,0 +1,81 @@
'use client';
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
interface KeyboardShortcut {
key: string;
ctrlKey?: boolean;
shiftKey?: boolean;
altKey?: boolean;
action: () => void;
description: string;
}
export function useKeyboardShortcuts(shortcuts: KeyboardShortcut[]) {
const router = useRouter();
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
// Don't trigger shortcuts when typing in inputs
if (
event.target instanceof HTMLInputElement ||
event.target instanceof HTMLTextAreaElement ||
(event.target instanceof HTMLElement && event.target.isContentEditable)
) {
return;
}
for (const shortcut of shortcuts) {
if (
event.key === shortcut.key &&
(shortcut.ctrlKey === undefined || event.ctrlKey === shortcut.ctrlKey) &&
(shortcut.shiftKey === undefined || event.shiftKey === shortcut.shiftKey) &&
(shortcut.altKey === undefined || event.altKey === shortcut.altKey)
) {
event.preventDefault();
shortcut.action();
break;
}
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [shortcuts]);
}
export function useGlobalKeyboardShortcuts() {
const router = useRouter();
useKeyboardShortcuts([
{
key: '/',
action: () => {
const searchInput = document.querySelector('input[type="search"]') as HTMLInputElement;
if (searchInput) {
searchInput.focus();
}
},
description: 'Focus search',
},
{
key: 'g',
ctrlKey: true,
action: () => router.push('/dashboard'),
description: 'Go to dashboard',
},
{
key: '?',
action: () => {
// Show keyboard shortcuts help
const helpModal = document.getElementById('keyboard-shortcuts-help');
if (helpModal) {
(helpModal as any).showModal?.();
}
},
description: 'Show keyboard shortcuts',
},
]);
}

View File

@@ -0,0 +1,175 @@
/**
* ArgoCD API Client
* Handles communication with ArgoCD for GitOps status and management
*/
import axios, { AxiosInstance } from 'axios'
export interface ArgoCDApplication {
metadata: {
name: string
namespace: string
uid: string
}
spec: {
project: string
source: {
repoURL: string
path?: string
targetRevision?: string
}
destination: {
server: string
namespace: string
}
syncPolicy?: {
automated?: {
prune: boolean
selfHeal: boolean
}
syncOptions?: string[]
}
}
status: {
health: {
status: 'Healthy' | 'Degraded' | 'Progressing' | 'Suspended' | 'Missing' | 'Unknown'
message?: string
}
sync: {
status: 'Synced' | 'OutOfSync' | 'Unknown'
revision?: string
}
conditions?: Array<{
type: string
message: string
lastTransitionTime: string
}>
}
}
export interface ArgoCDSyncResult {
resources: Array<{
kind: string
namespace: string
name: string
status: string
message?: string
}>
}
class ArgoCDClient {
private client: AxiosInstance
private baseURL: string
constructor(accessToken?: string) {
this.baseURL = process.env.NEXT_PUBLIC_ARGOCD_URL || 'http://localhost:8080'
this.client = axios.create({
baseURL: this.baseURL,
headers: {
'Content-Type': 'application/json',
...(accessToken && { Authorization: `Bearer ${accessToken}` }),
},
})
}
/**
* Get all ArgoCD applications
*/
async getApplications(): Promise<ArgoCDApplication[]> {
try {
const response = await this.client.get('/api/v1/applications')
return response.data.items || []
} catch (error: any) {
console.error('Failed to fetch ArgoCD applications:', error)
throw new Error(`Failed to fetch applications: ${error.message}`)
}
}
/**
* Get a specific ArgoCD application
*/
async getApplication(name: string, namespace: string = 'argocd'): Promise<ArgoCDApplication> {
try {
const response = await this.client.get(`/api/v1/applications/${name}`, {
params: { namespace },
})
return response.data
} catch (error: any) {
console.error(`Failed to fetch ArgoCD application ${name}:`, error)
throw new Error(`Failed to fetch application: ${error.message}`)
}
}
/**
* Sync an ArgoCD application
*/
async syncApplication(
name: string,
namespace: string = 'argocd',
prune: boolean = false,
dryRun: boolean = false
): Promise<ArgoCDSyncResult> {
try {
const response = await this.client.post(
`/api/v1/applications/${name}/sync`,
{
prune,
dryRun,
},
{
params: { namespace },
}
)
return response.data
} catch (error: any) {
console.error(`Failed to sync ArgoCD application ${name}:`, error)
throw new Error(`Failed to sync application: ${error.message}`)
}
}
/**
* Get application logs
*/
async getApplicationLogs(
name: string,
namespace: string = 'argocd',
tailLines: number = 100
): Promise<string[]> {
try {
const response = await this.client.get(`/api/v1/applications/${name}/logs`, {
params: {
namespace,
tailLines,
},
})
return response.data.logs || []
} catch (error: any) {
console.error(`Failed to fetch logs for ${name}:`, error)
throw new Error(`Failed to fetch logs: ${error.message}`)
}
}
/**
* Get application resource tree
*/
async getApplicationResourceTree(
name: string,
namespace: string = 'argocd'
): Promise<any> {
try {
const response = await this.client.get(`/api/v1/applications/${name}/resource-tree`, {
params: { namespace },
})
return response.data
} catch (error: any) {
console.error(`Failed to fetch resource tree for ${name}:`, error)
throw new Error(`Failed to fetch resource tree: ${error.message}`)
}
}
}
export function createArgoCDClient(accessToken?: string): ArgoCDClient {
return new ArgoCDClient(accessToken)
}

View File

@@ -94,7 +94,8 @@ class CrossplaneClientImpl implements CrossplaneClient {
async getVMs(): Promise<VM[]> {
try {
const response = await this.client.get('/apis/proxmox.yourorg.io/v1alpha1/proxmoxvms');
const apiGroup = process.env.NEXT_PUBLIC_CROSSPLANE_API_GROUP || 'proxmox.sankofa.nexus'
const response = await this.client.get(`/apis/${apiGroup}/v1alpha1/proxmoxvms`);
return response.data.items || [];
} catch (error) {
handleAxiosError(error);
@@ -103,8 +104,9 @@ class CrossplaneClientImpl implements CrossplaneClient {
async getVM(name: string): Promise<VM> {
try {
const apiGroup = process.env.NEXT_PUBLIC_CROSSPLANE_API_GROUP || 'proxmox.sankofa.nexus'
const response = await this.client.get(
`/apis/proxmox.yourorg.io/v1alpha1/proxmoxvms/${name}`
`/apis/${apiGroup}/v1alpha1/proxmoxvms/${name}`
);
return response.data;
} catch (error) {
@@ -114,8 +116,9 @@ class CrossplaneClientImpl implements CrossplaneClient {
async createVM(spec: VMSpec): Promise<VM> {
try {
const response = await this.client.post('/apis/proxmox.yourorg.io/v1alpha1/proxmoxvms', {
apiVersion: 'proxmox.yourorg.io/v1alpha1',
const apiGroup = process.env.NEXT_PUBLIC_CROSSPLANE_API_GROUP || 'proxmox.sankofa.nexus'
const response = await this.client.post(`/apis/${apiGroup}/v1alpha1/proxmoxvms`, {
apiVersion: `${apiGroup}/v1alpha1`,
kind: 'ProxmoxVM',
metadata: {
name: spec.forProvider.name,
@@ -131,8 +134,9 @@ class CrossplaneClientImpl implements CrossplaneClient {
async updateVM(name: string, spec: Partial<VMSpec>): Promise<VM> {
try {
const apiGroup = process.env.NEXT_PUBLIC_CROSSPLANE_API_GROUP || 'proxmox.sankofa.nexus'
const response = await this.client.patch(
`/apis/proxmox.yourorg.io/v1alpha1/proxmoxvms/${name}`,
`/apis/${apiGroup}/v1alpha1/proxmoxvms/${name}`,
{
spec,
},
@@ -150,7 +154,8 @@ class CrossplaneClientImpl implements CrossplaneClient {
async deleteVM(name: string): Promise<void> {
try {
await this.client.delete(`/apis/proxmox.yourorg.io/v1alpha1/proxmoxvms/${name}`);
const apiGroup = process.env.NEXT_PUBLIC_CROSSPLANE_API_GROUP || 'proxmox.sankofa.nexus'
await this.client.delete(`/apis/${apiGroup}/v1alpha1/proxmoxvms/${name}`);
} catch (error) {
handleAxiosError(error);
}

View File

@@ -0,0 +1,264 @@
/**
* Fairness Orchestration Engine Client
* This is a client-side wrapper that would call the API in production
*/
export interface OutputType {
id: string;
name: string;
weight: number;
description: string;
}
export interface InputSpec {
dataset: string;
dateRange?: { start: string; end: string };
filters?: Record<string, any>;
sensitiveAttributes: string[];
estimatedSize?: number;
}
export interface TimelineSpec {
mode: 'now' | 'scheduled' | 'continuous';
sla?: string;
deadline?: string;
}
export interface OrchestrationRequest {
input: InputSpec;
outputs: string[];
timeline: TimelineSpec;
}
export interface OrchestrationResult {
totalLoad: number;
inputLoad: number;
outputLoad: number;
estimatedTime: number;
feasible: boolean;
warnings: string[];
suggestions: string[];
}
// Output type definitions
export const OUTPUT_TYPES: Record<string, OutputType> = {
'fairness-audit-pdf': {
id: 'fairness-audit-pdf',
name: 'Fairness Audit PDF',
weight: 2.5,
description: 'Comprehensive fairness audit report in PDF format'
},
'metrics-export': {
id: 'metrics-export',
name: 'Metrics Export (SPD, TPR, FPR)',
weight: 1.0,
description: 'Statistical parity difference, true positive rate, false positive rate metrics'
},
'flagged-cases-csv': {
id: 'flagged-cases-csv',
name: 'Flagged Cases CSV',
weight: 1.5,
description: 'Export of cases flagged for potential bias issues'
},
'exec-summary-slides': {
id: 'exec-summary-slides',
name: 'Executive Summary Slide Pack',
weight: 2.0,
description: 'Executive presentation slides with key findings'
},
'detailed-report-json': {
id: 'detailed-report-json',
name: 'Detailed Report (JSON)',
weight: 1.2,
description: 'Machine-readable detailed fairness analysis'
},
'alerts-config': {
id: 'alerts-config',
name: 'Alert Configuration',
weight: 0.8,
description: 'Automated alert rules for ongoing monitoring'
},
'dashboard-export': {
id: 'dashboard-export',
name: 'Dashboard Export',
weight: 1.8,
description: 'Interactive dashboard with fairness metrics'
},
'compliance-report': {
id: 'compliance-report',
name: 'Compliance Report',
weight: 2.2,
description: 'Regulatory compliance documentation'
}
};
const INPUT_PASS_MULTIPLIER = 2.0;
const TOTAL_LOAD_MULTIPLIER = 3.2;
const OUTPUT_TARGET_MULTIPLIER = 1.2;
const BASE_PROCESSING_RATE = 10;
const INPUT_PROCESSING_RATE = 15;
const OUTPUT_PROCESSING_RATE = 8;
export function calculateInputLoad(input: InputSpec): number {
if (input.estimatedSize) {
return input.estimatedSize;
}
let baseSize = 100;
baseSize += input.sensitiveAttributes.length * 20;
if (input.dateRange) {
const start = new Date(input.dateRange.start);
const end = new Date(input.dateRange.end);
const days = Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24));
baseSize += days * 5;
}
if (input.filters) {
baseSize += Object.keys(input.filters).length * 10;
}
return baseSize;
}
export function calculateOutputLoad(outputIds: string[]): number {
return outputIds.reduce((total, outputId) => {
const output = OUTPUT_TYPES[outputId];
if (!output) return total;
return total + output.weight;
}, 0);
}
export function calculateTotalLoad(inputLoad: number, outputLoad: number): number {
const inputPasses = inputLoad * INPUT_PASS_MULTIPLIER;
return outputLoad + inputPasses;
}
export function estimateProcessingTime(totalLoad: number): number {
const avgRate = (INPUT_PROCESSING_RATE + OUTPUT_PROCESSING_RATE) / 2;
return totalLoad / avgRate;
}
function parseSLAToSeconds(sla: string): number {
const match = sla.match(/(\d+)\s*(hour|hours|day|days|minute|minutes|min|mins|second|seconds|sec|secs)/i);
if (!match) return 3600;
const value = parseInt(match[1], 10);
const unit = match[2].toLowerCase();
const multipliers: Record<string, number> = {
second: 1, seconds: 1, sec: 1, secs: 1,
minute: 60, minutes: 60, min: 60, mins: 60,
hour: 3600, hours: 3600,
day: 86400, days: 86400
};
return value * (multipliers[unit] || 3600);
}
function formatTime(seconds: number): string {
if (seconds < 60) return `${Math.ceil(seconds)} seconds`;
if (seconds < 3600) return `${(seconds / 60).toFixed(1)} minutes`;
if (seconds < 86400) return `${(seconds / 3600).toFixed(1)} hours`;
return `${(seconds / 86400).toFixed(1)} days`;
}
export function checkFeasibility(
totalLoad: number,
estimatedTime: number,
timeline: TimelineSpec
): { feasible: boolean; warnings: string[]; suggestions: string[] } {
const warnings: string[] = [];
const suggestions: string[] = [];
let maxTimeSeconds: number | null = null;
if (timeline.sla) {
maxTimeSeconds = parseSLAToSeconds(timeline.sla);
} else if (timeline.deadline) {
const now = Date.now();
const deadline = new Date(timeline.deadline).getTime();
maxTimeSeconds = Math.max(0, (deadline - now) / 1000);
}
const inputLoad = totalLoad / TOTAL_LOAD_MULTIPLIER;
const outputLoad = totalLoad - (inputLoad * INPUT_PASS_MULTIPLIER);
const targetOutputLoad = inputLoad * OUTPUT_TARGET_MULTIPLIER;
if (outputLoad > targetOutputLoad * 1.5) {
warnings.push(
`Output complexity (${outputLoad.toFixed(1)} units) is significantly higher than recommended (${targetOutputLoad.toFixed(1)} units)`
);
suggestions.push('Consider reducing the number of outputs or simplifying output requirements');
}
if (maxTimeSeconds !== null) {
if (estimatedTime > maxTimeSeconds) {
warnings.push(
`Estimated processing time (${formatTime(estimatedTime)}) exceeds requested timeline (${formatTime(maxTimeSeconds)})`
);
suggestions.push(`Consider extending timeline to ${formatTime(estimatedTime * 1.2)} or reducing outputs`);
} else if (estimatedTime > maxTimeSeconds * 0.8) {
warnings.push(
`Estimated processing time (${formatTime(estimatedTime)}) is close to timeline limit (${formatTime(maxTimeSeconds)})`
);
suggestions.push('Consider adding buffer time or reducing outputs for safety');
}
}
const expectedTotalLoad = inputLoad * TOTAL_LOAD_MULTIPLIER;
if (totalLoad > expectedTotalLoad * 1.3) {
warnings.push(
`Total process load (${totalLoad.toFixed(1)} units) is higher than expected (${expectedTotalLoad.toFixed(1)} units)`
);
}
const feasible = warnings.length === 0 || warnings.every(w => !w.includes('exceeds'));
return { feasible, warnings, suggestions };
}
export function orchestrate(request: OrchestrationRequest): OrchestrationResult {
const inputLoad = calculateInputLoad(request.input);
const outputLoad = calculateOutputLoad(request.outputs);
const totalLoad = calculateTotalLoad(inputLoad, outputLoad);
const estimatedTime = estimateProcessingTime(totalLoad);
const { feasible, warnings, suggestions } = checkFeasibility(
totalLoad,
estimatedTime,
request.timeline
);
return {
totalLoad,
inputLoad,
outputLoad,
estimatedTime,
feasible,
warnings,
suggestions
};
}
export function getUserMessage(result: OrchestrationResult, request: OrchestrationRequest): string {
const { inputLoad, outputLoad, estimatedTime, feasible, warnings } = result;
if (feasible && warnings.length === 0) {
return `This fairness audit will process approximately ${inputLoad.toFixed(0)} input units and generate ${outputLoad.toFixed(1)} output units, taking approximately ${formatTime(estimatedTime)} to complete.`;
}
if (feasible) {
return `This audit is feasible but has some considerations: ${warnings.join('; ')}. Estimated time: ${formatTime(estimatedTime)}.`;
}
return `This audit configuration may not be feasible within the requested timeline. ${warnings.join('; ')}. Estimated time: ${formatTime(estimatedTime)}.`;
}
export function getAvailableOutputs(): OutputType[] {
return Object.values(OUTPUT_TYPES);
}
export function getOutputType(id: string): OutputType | undefined {
return OUTPUT_TYPES[id];
}

View File

@@ -0,0 +1,175 @@
/**
* Kubernetes API Client
* Handles communication with Kubernetes API for cluster management
*/
import axios, { AxiosInstance } from 'axios'
export interface KubernetesCluster {
name: string
server: string
context: string
namespace: string
version?: string
nodes?: {
total: number
ready: number
}
}
export interface KubernetesResource {
kind: string
apiVersion: string
metadata: {
name: string
namespace?: string
uid: string
creationTimestamp: string
labels?: Record<string, string>
annotations?: Record<string, string>
}
spec?: any
status?: any
}
export interface KubernetesNamespace {
metadata: {
name: string
uid: string
creationTimestamp: string
labels?: Record<string, string>
}
status: {
phase: string
}
}
class KubernetesClient {
private client: AxiosInstance
private baseURL: string
constructor(accessToken?: string, clusterURL?: string) {
this.baseURL = clusterURL || process.env.NEXT_PUBLIC_KUBERNETES_API || 'http://localhost:8001'
this.client = axios.create({
baseURL: this.baseURL,
headers: {
'Content-Type': 'application/json',
...(accessToken && { Authorization: `Bearer ${accessToken}` }),
},
})
}
/**
* Get cluster information
*/
async getClusterInfo(): Promise<KubernetesCluster> {
try {
const versionResponse = await this.client.get('/version')
const nodesResponse = await this.client.get('/api/v1/nodes')
const nodes = nodesResponse.data.items || []
const readyNodes = nodes.filter((node: any) => {
const conditions = node.status?.conditions || []
return conditions.some((c: any) => c.type === 'Ready' && c.status === 'True')
})
return {
name: 'default-cluster',
server: this.baseURL,
context: 'default',
namespace: 'default',
version: versionResponse.data.gitVersion,
nodes: {
total: nodes.length,
ready: readyNodes.length,
},
}
} catch (error: any) {
console.error('Failed to fetch cluster info:', error)
throw new Error(`Failed to fetch cluster info: ${error.message}`)
}
}
/**
* List namespaces
*/
async listNamespaces(): Promise<KubernetesNamespace[]> {
try {
const response = await this.client.get('/api/v1/namespaces')
return response.data.items || []
} catch (error: any) {
console.error('Failed to list namespaces:', error)
throw new Error(`Failed to list namespaces: ${error.message}`)
}
}
/**
* List resources by kind
*/
async listResources(
kind: string,
namespace?: string,
apiVersion: string = 'v1'
): Promise<KubernetesResource[]> {
try {
const path = namespace
? `/api/${apiVersion}/namespaces/${namespace}/${kind.toLowerCase()}s`
: `/api/${apiVersion}/${kind.toLowerCase()}s`
const response = await this.client.get(path)
return response.data.items || []
} catch (error: any) {
console.error(`Failed to list ${kind} resources:`, error)
throw new Error(`Failed to list resources: ${error.message}`)
}
}
/**
* Get a specific resource
*/
async getResource(
kind: string,
name: string,
namespace?: string,
apiVersion: string = 'v1'
): Promise<KubernetesResource> {
try {
const path = namespace
? `/api/${apiVersion}/namespaces/${namespace}/${kind.toLowerCase()}s/${name}`
: `/api/${apiVersion}/${kind.toLowerCase()}s/${name}`
const response = await this.client.get(path)
return response.data
} catch (error: any) {
console.error(`Failed to get ${kind} ${name}:`, error)
throw new Error(`Failed to get resource: ${error.message}`)
}
}
/**
* List pods
*/
async listPods(namespace?: string): Promise<KubernetesResource[]> {
return this.listResources('Pod', namespace)
}
/**
* List deployments
*/
async listDeployments(namespace?: string): Promise<KubernetesResource[]> {
return this.listResources('Deployment', namespace, 'apps/v1')
}
/**
* List services
*/
async listServices(namespace?: string): Promise<KubernetesResource[]> {
return this.listResources('Service', namespace)
}
}
export function createKubernetesClient(accessToken?: string, clusterURL?: string): KubernetesClient {
return new KubernetesClient(accessToken, clusterURL)
}

42
portal/src/lib/roles.ts Normal file
View File

@@ -0,0 +1,42 @@
import { Session } from 'next-auth'
export type UserRole = 'technical' | 'business' | 'developer' | 'admin'
/**
* Determines the primary role for a user based on their session roles
* Priority: technical > business > developer > admin (default)
*/
export function getUserRole(session: Session | null): UserRole {
if (!session?.roles || session.roles.length === 0) {
return 'admin' // Default role
}
const roles = session.roles.map(r => r.toLowerCase())
// Check for technical admin roles
if (roles.some(r => r.includes('technical') || r.includes('admin') || r.includes('operator'))) {
return 'technical'
}
// Check for business owner roles
if (roles.some(r => r.includes('business') || r.includes('owner') || r.includes('manager'))) {
return 'business'
}
// Check for developer roles
if (roles.some(r => r.includes('developer') || r.includes('dev') || r.includes('engineer'))) {
return 'developer'
}
// Default to admin/technical
return 'technical'
}
/**
* Gets the dashboard route for a user based on their role
*/
export function getDashboardRoute(session: Session | null): string {
const role = getUserRole(session)
return `/dashboard/${role}`
}

6
portal/src/lib/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}