portal: Apollo dashboard queries, strict TypeScript build, UI primitives
- Add GraphQL dashboard operations, ApolloProvider, CardDescription, label/checkbox/alert - Fix case-sensitive UI imports, Crossplane VM metadata uid, VMList spec parsing - Extend next-auth session user (id, role); fairness filters as unknown; ESLint relax to warnings - Remove unused session destructure across pages; next.config without skip TS/ESLint api: GraphQL/WebSocket hardening, logger import in websocket service Made-with: Cursor
This commit is contained in:
@@ -41,7 +41,7 @@ export default function Dashboard() {
|
||||
const { data: session } = useSession();
|
||||
const crossplane = createCrossplaneClient(session?.accessToken as string);
|
||||
|
||||
const { data: vms = [] } = useQuery({
|
||||
const { data: vms = [], isLoading: vmsLoading } = useQuery({
|
||||
queryKey: ['vms'],
|
||||
queryFn: () => crossplane.getVMs(),
|
||||
});
|
||||
@@ -84,7 +84,7 @@ export default function Dashboard() {
|
||||
<Server className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{isLoading ? '...' : totalVMs}</div>
|
||||
<div className="text-2xl font-bold">{vmsLoading ? '...' : totalVMs}</div>
|
||||
<p className="text-xs text-muted-foreground">Across all sites</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -95,7 +95,7 @@ export default function Dashboard() {
|
||||
<CheckCircle className="h-4 w-4 text-green-500" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{isLoading ? '...' : runningVMs}</div>
|
||||
<div className="text-2xl font-bold">{vmsLoading ? '...' : runningVMs}</div>
|
||||
<p className="text-xs text-muted-foreground">Active virtual machines</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -106,7 +106,7 @@ export default function Dashboard() {
|
||||
<AlertCircle className="h-4 w-4 text-yellow-500" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{isLoading ? '...' : stoppedVMs}</div>
|
||||
<div className="text-2xl font-bold">{vmsLoading ? '...' : stoppedVMs}</div>
|
||||
<p className="text-xs text-muted-foreground">Inactive virtual machines</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useState, type ChangeEvent } from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
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'
|
||||
|
||||
@@ -96,7 +96,7 @@ export function ResourceExplorer() {
|
||||
<Input
|
||||
placeholder="Search resources..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => setSearch(e.target.value)}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Select value={filterProvider} onValueChange={setFilterProvider}>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
'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 { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { Server, Play, Pause, Trash2 } from 'lucide-react'
|
||||
|
||||
interface VM {
|
||||
@@ -15,34 +14,28 @@ interface VM {
|
||||
disk: number
|
||||
}
|
||||
|
||||
function specToNumber(v: string | number | undefined): number {
|
||||
if (v == null) return 0
|
||||
if (typeof v === 'number') return v
|
||||
const m = /^(\d+(?:\.\d+)?)/.exec(String(v).trim())
|
||||
return m ? parseFloat(m[1]) : 0
|
||||
}
|
||||
|
||||
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 || '',
|
||||
|
||||
return vms.map((vm) => ({
|
||||
id: vm.metadata?.name || 'unknown',
|
||||
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,
|
||||
memory: specToNumber(vm.spec?.forProvider?.memory),
|
||||
disk: specToNumber(vm.spec?.forProvider?.disk),
|
||||
}))
|
||||
},
|
||||
})
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
||||
import { Sparkles, TrendingDown, TrendingUp, AlertCircle } from 'lucide-react';
|
||||
import { Sparkles, TrendingDown, TrendingUp } from 'lucide-react';
|
||||
|
||||
export function OptimizationEngine() {
|
||||
const [recommendations, setRecommendations] = useState([
|
||||
const [recommendations] = useState([
|
||||
{
|
||||
id: '1',
|
||||
type: 'cost',
|
||||
|
||||
@@ -8,7 +8,6 @@ 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 {
|
||||
@@ -17,7 +16,7 @@ interface CrossplaneResource {
|
||||
metadata: {
|
||||
name: string
|
||||
namespace: string
|
||||
uid: string
|
||||
uid?: string
|
||||
creationTimestamp: string
|
||||
labels?: Record<string, string>
|
||||
}
|
||||
@@ -46,7 +45,12 @@ export default function CrossplaneResourceBrowser() {
|
||||
return vms.map((vm) => ({
|
||||
apiVersion: process.env.NEXT_PUBLIC_CROSSPLANE_API_GROUP || 'proxmox.sankofa.nexus/v1alpha1',
|
||||
kind: 'ProxmoxVM',
|
||||
metadata: vm.metadata,
|
||||
metadata: {
|
||||
...vm.metadata,
|
||||
uid:
|
||||
(vm.metadata as { uid?: string }).uid ??
|
||||
`${vm.metadata.namespace}/${vm.metadata.name}`,
|
||||
},
|
||||
spec: vm.spec,
|
||||
status: vm.status,
|
||||
}))
|
||||
@@ -74,10 +78,10 @@ export default function CrossplaneResourceBrowser() {
|
||||
return <Server className="h-5 w-5 text-gray-500" />
|
||||
}
|
||||
|
||||
const getResourceStatusColor = (status: any) => {
|
||||
const getResourceStatusColor = (status: Record<string, unknown> | undefined) => {
|
||||
if (!status) return 'bg-gray-500'
|
||||
|
||||
const state = status.state || status.phase || 'Unknown'
|
||||
const state = String(status.state ?? status.phase ?? 'Unknown')
|
||||
switch (state.toLowerCase()) {
|
||||
case 'running':
|
||||
case 'ready':
|
||||
@@ -94,7 +98,7 @@ export default function CrossplaneResourceBrowser() {
|
||||
}
|
||||
}
|
||||
|
||||
const filteredResources = resources.filter((resource) => {
|
||||
const filteredResources = resources.filter((resource: CrossplaneResource) => {
|
||||
const matchesSearch = resource.metadata.name.toLowerCase().includes(search.toLowerCase())
|
||||
const matchesKind = filterKind === 'all' || resource.kind === filterKind
|
||||
const matchesNamespace =
|
||||
@@ -124,7 +128,7 @@ export default function CrossplaneResourceBrowser() {
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Types</SelectItem>
|
||||
{uniqueKinds.map((kind) => (
|
||||
{uniqueKinds.map((kind: string) => (
|
||||
<SelectItem key={kind} value={kind}>
|
||||
{kind}
|
||||
</SelectItem>
|
||||
@@ -137,7 +141,7 @@ export default function CrossplaneResourceBrowser() {
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Namespaces</SelectItem>
|
||||
{uniqueNamespaces.map((ns) => (
|
||||
{uniqueNamespaces.map((ns: string) => (
|
||||
<SelectItem key={ns} value={ns}>
|
||||
{ns}
|
||||
</SelectItem>
|
||||
@@ -158,8 +162,8 @@ export default function CrossplaneResourceBrowser() {
|
||||
</Card>
|
||||
) : (
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{filteredResources.map((resource) => (
|
||||
<Card key={resource.metadata.uid}>
|
||||
{filteredResources.map((resource: CrossplaneResource) => (
|
||||
<Card key={resource.metadata.uid ?? `${resource.metadata.namespace}/${resource.metadata.name}`}>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -204,7 +208,7 @@ export default function CrossplaneResourceBrowser() {
|
||||
<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}
|
||||
{key}={String(value)}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
||||
import { GripVertical, X, Plus } from 'lucide-react';
|
||||
import { GripVertical, Plus } from 'lucide-react';
|
||||
import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd';
|
||||
|
||||
interface DashboardTile {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
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;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useMemo } from 'react';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
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';
|
||||
@@ -11,13 +11,7 @@ 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 type { OrchestrationRequest, InputSpec, TimelineSpec } from '@/lib/fairness-orchestration';
|
||||
import { orchestrate, getAvailableOutputs, getUserMessage } from '@/lib/fairness-orchestration';
|
||||
|
||||
export default function FairnessOrchestrationWizard() {
|
||||
@@ -32,8 +26,6 @@ export default function FairnessOrchestrationWizard() {
|
||||
mode: 'now',
|
||||
sla: '2 hours'
|
||||
});
|
||||
const [orchestrationResult, setOrchestrationResult] = useState<OrchestrationResult | null>(null);
|
||||
|
||||
const availableOutputs = getAvailableOutputs();
|
||||
|
||||
// Calculate orchestration when inputs change
|
||||
@@ -60,9 +52,8 @@ export default function FairnessOrchestrationWizard() {
|
||||
};
|
||||
|
||||
const handleRun = () => {
|
||||
if (result) {
|
||||
setOrchestrationResult(result);
|
||||
}
|
||||
if (!result?.feasible) return;
|
||||
// In production: POST orchestration job to API using `result` + inputSpec
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,18 +3,17 @@
|
||||
import { useState } from 'react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Server,
|
||||
Network,
|
||||
Settings,
|
||||
FileText,
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Server,
|
||||
Network,
|
||||
Settings,
|
||||
Activity,
|
||||
Users,
|
||||
CreditCard,
|
||||
Shield,
|
||||
Menu,
|
||||
X
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
|
||||
const navigation = [
|
||||
|
||||
@@ -33,3 +33,7 @@ export function CardContent({ children, className = '' }: CardProps) {
|
||||
return <div className={`p-6 pt-0 ${className}`}>{children}</div>;
|
||||
}
|
||||
|
||||
export function CardDescription({ children, className = '' }: CardProps) {
|
||||
return <p className={`text-sm text-muted-foreground ${className}`}>{children}</p>;
|
||||
}
|
||||
|
||||
|
||||
34
portal/src/components/ui/alert.tsx
Normal file
34
portal/src/components/ui/alert.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import * as React from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface AlertProps {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
variant?: 'default' | 'destructive'
|
||||
}
|
||||
|
||||
export function Alert({ children, className, variant = 'default' }: AlertProps) {
|
||||
return (
|
||||
<div
|
||||
role="alert"
|
||||
className={cn(
|
||||
'relative w-full rounded-lg border p-4 flex gap-3',
|
||||
variant === 'destructive'
|
||||
? 'border-red-500/50 bg-red-950/30 text-red-200'
|
||||
: 'border-gray-700 bg-gray-800/50 text-gray-200',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface AlertDescriptionProps {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function AlertDescription({ children, className }: AlertDescriptionProps) {
|
||||
return <div className={cn('text-sm flex-1', className)}>{children}</div>
|
||||
}
|
||||
28
portal/src/components/ui/checkbox.tsx
Normal file
28
portal/src/components/ui/checkbox.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export interface CheckboxProps {
|
||||
id?: string
|
||||
checked?: boolean
|
||||
onCheckedChange?: () => void
|
||||
className?: string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export function Checkbox({ id, checked, onCheckedChange, className, disabled }: CheckboxProps) {
|
||||
return (
|
||||
<input
|
||||
id={id}
|
||||
type="checkbox"
|
||||
checked={!!checked}
|
||||
disabled={disabled}
|
||||
onChange={() => onCheckedChange?.()}
|
||||
className={cn(
|
||||
'h-4 w-4 rounded border border-gray-600 bg-gray-900 text-blue-600 focus:ring-2 focus:ring-blue-500',
|
||||
className
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
13
portal/src/components/ui/label.tsx
Normal file
13
portal/src/components/ui/label.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import * as React from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export interface LabelProps extends React.LabelHTMLAttributes<HTMLLabelElement> {}
|
||||
|
||||
export function Label({ className, ...props }: LabelProps) {
|
||||
return (
|
||||
<label
|
||||
className={cn('text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react'
|
||||
import * as SelectPrimitive from '@radix-ui/react-select'
|
||||
import { Check, ChevronDown, ChevronUp } from 'lucide-react'
|
||||
import { Check, ChevronDown } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const Select = SelectPrimitive.Root
|
||||
|
||||
Reference in New Issue
Block a user