chore: initial commit for phase05

This commit is contained in:
hibna
2026-02-21 16:59:21 +03:00
parent 218452706c
commit 0941a9ba46
43 changed files with 4431 additions and 17 deletions
+277
View File
@@ -0,0 +1,277 @@
import { useState } from 'react';
import { useParams, useNavigate } from 'react-router';
import { useQuery, useMutation } from '@tanstack/react-query';
import { api } from '@/lib/api';
import { formatBytes } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
interface Game {
id: string;
name: string;
slug: string;
dockerImage: string;
}
interface Node {
id: string;
name: string;
fqdn: string;
memoryTotal: number;
diskTotal: number;
}
interface Allocation {
id: string;
ip: string;
port: number;
serverId: string | null;
}
interface PaginatedResponse<T> {
data: T[];
meta: { total: number };
}
export function CreateServerPage() {
const { orgId } = useParams();
const navigate = useNavigate();
const [step, setStep] = useState(1);
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const [gameId, setGameId] = useState('');
const [nodeId, setNodeId] = useState('');
const [allocationId, setAllocationId] = useState('');
const [memoryLimit, setMemoryLimit] = useState(1024);
const [diskLimit, setDiskLimit] = useState(5120);
const [cpuLimit, setCpuLimit] = useState(100);
const { data: gamesData } = useQuery({
queryKey: ['admin-games'],
queryFn: () => api.get<PaginatedResponse<Game>>('/admin/games'),
});
const { data: nodesData } = useQuery({
queryKey: ['nodes', orgId],
queryFn: () => api.get<PaginatedResponse<Node>>(`/organizations/${orgId}/nodes`),
});
const { data: allocationsData } = useQuery({
queryKey: ['allocations', orgId, nodeId],
queryFn: () =>
api.get<PaginatedResponse<Allocation>>(
`/organizations/${orgId}/nodes/${nodeId}/allocations`,
),
enabled: !!nodeId,
});
const freeAllocations = (allocationsData?.data ?? []).filter((a) => !a.serverId);
const createMutation = useMutation({
mutationFn: (body: Record<string, unknown>) =>
api.post(`/organizations/${orgId}/servers`, body),
onSuccess: () => {
navigate(`/org/${orgId}/dashboard`);
},
});
const games = gamesData?.data ?? [];
const nodes = nodesData?.data ?? [];
const handleCreate = () => {
createMutation.mutate({
name,
description: description || undefined,
gameId,
nodeId,
allocationId,
memoryLimit: memoryLimit * 1024 * 1024,
diskLimit: diskLimit * 1024 * 1024,
cpuLimit,
});
};
return (
<div className="mx-auto max-w-2xl space-y-6">
<div>
<h1 className="text-2xl font-bold">Create Server</h1>
<p className="text-muted-foreground">Set up a new game server</p>
</div>
<div className="flex gap-2">
{[1, 2, 3].map((s) => (
<div
key={s}
className={`h-1.5 flex-1 rounded-full ${s <= step ? 'bg-primary' : 'bg-muted'}`}
/>
))}
</div>
{step === 1 && (
<Card>
<CardHeader>
<CardTitle>Basic Information</CardTitle>
<CardDescription>Choose a name and game for your server</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label>Server Name</Label>
<Input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="My Awesome Server"
/>
</div>
<div className="space-y-2">
<Label>Description (optional)</Label>
<Input
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="A short description"
/>
</div>
<div className="space-y-2">
<Label>Game</Label>
<Select value={gameId} onValueChange={setGameId}>
<SelectTrigger>
<SelectValue placeholder="Select a game" />
</SelectTrigger>
<SelectContent>
{games.map((game) => (
<SelectItem key={game.id} value={game.id}>
{game.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Button onClick={() => setStep(2)} disabled={!name || !gameId}>
Next
</Button>
</CardContent>
</Card>
)}
{step === 2 && (
<Card>
<CardHeader>
<CardTitle>Node & Allocation</CardTitle>
<CardDescription>Choose where to host your server</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label>Node</Label>
<Select
value={nodeId}
onValueChange={(v) => {
setNodeId(v);
setAllocationId('');
}}
>
<SelectTrigger>
<SelectValue placeholder="Select a node" />
</SelectTrigger>
<SelectContent>
{nodes.map((node) => (
<SelectItem key={node.id} value={node.id}>
{node.name} ({node.fqdn})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{nodeId && (
<div className="space-y-2">
<Label>Port Allocation</Label>
<Select value={allocationId} onValueChange={setAllocationId}>
<SelectTrigger>
<SelectValue placeholder="Select a port" />
</SelectTrigger>
<SelectContent>
{freeAllocations.map((a) => (
<SelectItem key={a.id} value={a.id}>
{a.ip}:{a.port}
</SelectItem>
))}
</SelectContent>
</Select>
{freeAllocations.length === 0 && nodeId && (
<p className="text-sm text-destructive">No free allocations on this node</p>
)}
</div>
)}
<div className="flex gap-2">
<Button variant="outline" onClick={() => setStep(1)}>
Back
</Button>
<Button onClick={() => setStep(3)} disabled={!nodeId || !allocationId}>
Next
</Button>
</div>
</CardContent>
</Card>
)}
{step === 3 && (
<Card>
<CardHeader>
<CardTitle>Resources</CardTitle>
<CardDescription>Set resource limits for this server</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label>Memory (MB)</Label>
<Input
type="number"
value={memoryLimit}
onChange={(e) => setMemoryLimit(Number(e.target.value))}
min={128}
/>
<p className="text-xs text-muted-foreground">
{formatBytes(memoryLimit * 1024 * 1024)}
</p>
</div>
<div className="space-y-2">
<Label>Disk (MB)</Label>
<Input
type="number"
value={diskLimit}
onChange={(e) => setDiskLimit(Number(e.target.value))}
min={256}
/>
<p className="text-xs text-muted-foreground">
{formatBytes(diskLimit * 1024 * 1024)}
</p>
</div>
<div className="space-y-2">
<Label>CPU Limit (%)</Label>
<Input
type="number"
value={cpuLimit}
onChange={(e) => setCpuLimit(Number(e.target.value))}
min={10}
max={10000}
/>
<p className="text-xs text-muted-foreground">100% = 1 core</p>
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={() => setStep(2)}>
Back
</Button>
<Button onClick={handleCreate} disabled={createMutation.isPending}>
{createMutation.isPending ? 'Creating...' : 'Create Server'}
</Button>
</div>
{createMutation.isError && (
<p className="text-sm text-destructive">Failed to create server. Please try again.</p>
)}
</CardContent>
</Card>
)}
</div>
);
}