mirror of
https://github.com/YuzuZensai/Minikura.git
synced 2026-03-30 23:29:45 +00:00
✨ feat: topology, and improves handling
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { AlertCircle, CheckCircle2, XCircle } from "lucide-react";
|
||||
import type {
|
||||
CustomResourceSummary,
|
||||
DeploymentInfo,
|
||||
@@ -10,6 +9,7 @@ import type {
|
||||
PodInfo,
|
||||
StatefulSetInfo,
|
||||
} from "@minikura/api";
|
||||
import { AlertCircle, CheckCircle2, XCircle } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { api } from "@/lib/api";
|
||||
import { api } from "@/lib/api-client";
|
||||
|
||||
export default function K8sResourcesPage() {
|
||||
const [status, setStatus] = useState<K8sStatus | null>(null);
|
||||
@@ -64,50 +64,34 @@ export default function K8sResourcesPage() {
|
||||
|
||||
if (statusRes.status === "fulfilled" && statusRes.value.data) {
|
||||
setStatus(statusRes.value.data as K8sStatus);
|
||||
} else if (statusRes.status === "rejected") {
|
||||
console.error("Failed to fetch status:", statusRes.reason);
|
||||
}
|
||||
|
||||
if (podsRes.status === "fulfilled" && podsRes.value.data) {
|
||||
setPods(podsRes.value.data as PodInfo[]);
|
||||
} else if (podsRes.status === "rejected") {
|
||||
console.error("Failed to fetch pods:", podsRes.reason);
|
||||
}
|
||||
|
||||
if (deploymentsRes.status === "fulfilled" && deploymentsRes.value.data) {
|
||||
setDeployments(deploymentsRes.value.data as DeploymentInfo[]);
|
||||
} else if (deploymentsRes.status === "rejected") {
|
||||
console.error("Failed to fetch deployments:", deploymentsRes.reason);
|
||||
}
|
||||
|
||||
if (statefulSetsRes.status === "fulfilled" && statefulSetsRes.value.data) {
|
||||
setStatefulSets(statefulSetsRes.value.data as StatefulSetInfo[]);
|
||||
} else if (statefulSetsRes.status === "rejected") {
|
||||
console.error("Failed to fetch statefulsets:", statefulSetsRes.reason);
|
||||
}
|
||||
|
||||
if (servicesRes.status === "fulfilled" && servicesRes.value.data) {
|
||||
setServices(servicesRes.value.data as K8sServiceSummary[]);
|
||||
} else if (servicesRes.status === "rejected") {
|
||||
console.error("Failed to fetch services:", servicesRes.reason);
|
||||
}
|
||||
|
||||
if (configMapsRes.status === "fulfilled" && configMapsRes.value.data) {
|
||||
setConfigMaps(configMapsRes.value.data as K8sConfigMapSummary[]);
|
||||
} else if (configMapsRes.status === "rejected") {
|
||||
console.error("Failed to fetch configmaps:", configMapsRes.reason);
|
||||
}
|
||||
|
||||
if (minecraftServersRes.status === "fulfilled" && minecraftServersRes.value.data) {
|
||||
setMinecraftServers(minecraftServersRes.value.data as CustomResourceSummary[]);
|
||||
} else if (minecraftServersRes.status === "rejected") {
|
||||
console.error("Failed to fetch minecraft servers:", minecraftServersRes.reason);
|
||||
}
|
||||
|
||||
if (reverseProxyServersRes.status === "fulfilled" && reverseProxyServersRes.value.data) {
|
||||
setReverseProxyServers(reverseProxyServersRes.value.data as CustomResourceSummary[]);
|
||||
} else if (reverseProxyServersRes.status === "rejected") {
|
||||
console.error("Failed to fetch reverse proxy servers:", reverseProxyServersRes.reason);
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const errorMessage =
|
||||
|
||||
@@ -6,7 +6,15 @@ import { useRouter } from "next/navigation";
|
||||
import { ServerForm, type ServerFormData } from "@/components/server-form";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { api } from "@/lib/api";
|
||||
import { api } from "@/lib/api-client";
|
||||
|
||||
const toDifficultyUppercase = (value: string): "PEACEFUL" | "EASY" | "NORMAL" | "HARD" => {
|
||||
return value.toUpperCase() as "PEACEFUL" | "EASY" | "NORMAL" | "HARD";
|
||||
};
|
||||
|
||||
const toModeUppercase = (value: string): "SURVIVAL" | "CREATIVE" | "ADVENTURE" | "SPECTATOR" => {
|
||||
return value.toUpperCase() as "SURVIVAL" | "CREATIVE" | "ADVENTURE" | "SPECTATOR";
|
||||
};
|
||||
|
||||
export default function CreateServerPage() {
|
||||
const router = useRouter();
|
||||
@@ -121,8 +129,8 @@ export default function CreateServerPage() {
|
||||
jvm_opts: data.jvmOpts || undefined,
|
||||
use_aikar_flags: data.useAikarFlags || undefined,
|
||||
use_meowice_flags: data.useMeowiceFlags || undefined,
|
||||
difficulty: data.difficulty,
|
||||
game_mode: data.mode,
|
||||
difficulty: toDifficultyUppercase(data.difficulty),
|
||||
game_mode: toModeUppercase(data.mode),
|
||||
max_players: data.maxPlayers ? Number(data.maxPlayers) : undefined,
|
||||
pvp: data.pvp,
|
||||
online_mode: data.onlineMode,
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useEffect, useState } from "react";
|
||||
import { ServerForm, type ServerFormData, type ServerType } from "@/components/server-form";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { api } from "@/lib/api";
|
||||
import { api } from "@/lib/api-client";
|
||||
import { getReverseProxyApi } from "@/lib/api-helpers";
|
||||
|
||||
export default function EditServerPage() {
|
||||
@@ -48,8 +48,7 @@ export default function EditServerPage() {
|
||||
|
||||
setError("Server not found");
|
||||
setLoading(false);
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch server:", err);
|
||||
} catch (_err) {
|
||||
setError("Failed to load server data");
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -81,11 +80,12 @@ export default function EditServerPage() {
|
||||
return "survival";
|
||||
};
|
||||
|
||||
const toServerType = (value?: string | null): ServerType => {
|
||||
if (value === "VANILLA" || value === "CUSTOM") {
|
||||
return value;
|
||||
}
|
||||
return "PAPER";
|
||||
const toDifficultyUppercase = (value: string): "PEACEFUL" | "EASY" | "NORMAL" | "HARD" => {
|
||||
return value.toUpperCase() as "PEACEFUL" | "EASY" | "NORMAL" | "HARD";
|
||||
};
|
||||
|
||||
const toModeUppercase = (value: string): "SURVIVAL" | "CREATIVE" | "ADVENTURE" | "SPECTATOR" => {
|
||||
return value.toUpperCase() as "SURVIVAL" | "CREATIVE" | "ADVENTURE" | "SPECTATOR";
|
||||
};
|
||||
|
||||
const parseEnvVariables = (envVars?: Array<{ key: string; value: string }>) => {
|
||||
@@ -205,8 +205,8 @@ export default function EditServerPage() {
|
||||
jvm_opts: data.jvmOpts || undefined,
|
||||
use_aikar_flags: data.useAikarFlags || undefined,
|
||||
use_meowice_flags: data.useMeowiceFlags || undefined,
|
||||
difficulty: data.difficulty,
|
||||
game_mode: data.mode,
|
||||
difficulty: toDifficultyUppercase(data.difficulty),
|
||||
game_mode: toModeUppercase(data.mode),
|
||||
max_players: data.maxPlayers ? Number(data.maxPlayers) : undefined,
|
||||
pvp: data.pvp,
|
||||
online_mode: data.onlineMode,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import type { ConnectionInfo, NormalServer, PodInfo, ReverseProxyServer } from "@minikura/api";
|
||||
import {
|
||||
AlertCircle,
|
||||
Check,
|
||||
@@ -25,15 +26,6 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -42,14 +34,10 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import type { ConnectionInfo, NormalServer, PodInfo, ReverseProxyServer } from "@minikura/api";
|
||||
import { api } from "@/lib/api-client";
|
||||
import { getReverseProxyApi } from "@/lib/api-helpers";
|
||||
import { api } from "@/lib/api";
|
||||
|
||||
// Server status component
|
||||
function ServerStatusCell({ serverId, type }: { serverId: string; type: "normal" | "proxy" }) {
|
||||
const [pods, setPods] = useState<PodInfo[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -59,14 +47,13 @@ function ServerStatusCell({ serverId, type }: { serverId: string; type: "normal"
|
||||
try {
|
||||
const endpoint =
|
||||
type === "normal"
|
||||
? api.api.k8s["servers"]({ serverId }).pods.get
|
||||
? api.api.k8s.servers({ serverId }).pods.get
|
||||
: api.api.k8s["reverse-proxy"]({ serverId }).pods.get;
|
||||
const res = await endpoint();
|
||||
if (res.data) {
|
||||
setPods(res.data as PodInfo[]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch pods:", error);
|
||||
} catch (_error) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -93,9 +80,7 @@ function ServerStatusCell({ serverId, type }: { serverId: string; type: "normal"
|
||||
}
|
||||
|
||||
const allRunning = pods.every((pod) => pod.status === "Running");
|
||||
const readyCount = pods.filter(
|
||||
(pod) => pod.ready === "1/1" || pod.ready === "1/1" || pod.ready === "1/1"
|
||||
).length;
|
||||
const readyCount = pods.filter((pod) => pod.ready === "1/1").length;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -111,7 +96,6 @@ function ServerStatusCell({ serverId, type }: { serverId: string; type: "normal"
|
||||
);
|
||||
}
|
||||
|
||||
// Connection info component
|
||||
function ConnectionInfoCell({ serverId, type }: { serverId: string; type: "normal" | "proxy" }) {
|
||||
const [connectionInfo, setConnectionInfo] = useState<ConnectionInfo | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -129,8 +113,7 @@ function ConnectionInfoCell({ serverId, type }: { serverId: string; type: "norma
|
||||
if (res.data) {
|
||||
setConnectionInfo(res.data as ConnectionInfo);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch connection info:", error);
|
||||
} catch (_error) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -213,8 +196,7 @@ export default function ServersPage() {
|
||||
if (proxyRes.data) {
|
||||
setReverseProxies(proxyRes.data as unknown as ReverseProxyServer[]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch servers:", error);
|
||||
} catch (_error) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -236,9 +218,7 @@ export default function ServersPage() {
|
||||
}
|
||||
await fetchServers();
|
||||
setDeleteTarget(null);
|
||||
} catch (error) {
|
||||
console.error("Failed to delete server:", error);
|
||||
}
|
||||
} catch (_error) {}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
@@ -270,7 +250,6 @@ export default function ServersPage() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Normal Servers */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -360,7 +339,6 @@ export default function ServersPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Reverse Proxy Servers */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -448,7 +426,6 @@ export default function ServersPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<Dialog open={!!deleteTarget} onOpenChange={() => setDeleteTarget(null)}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
|
||||
89
apps/web/app/dashboard/topology/page.tsx
Normal file
89
apps/web/app/dashboard/topology/page.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
"use client";
|
||||
|
||||
import { Network, RefreshCw } from "lucide-react";
|
||||
import { TopologyCanvas } from "@/components/topology/topology-canvas";
|
||||
import { useTopologyData } from "@/hooks/use-topology-data";
|
||||
|
||||
export default function TopologyPage() {
|
||||
const { graph, loading, error } = useTopologyData();
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Network Topology</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
Real-time server infrastructure, proxy connections, and Kubernetes nodes
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-center h-[calc(100vh-250px)]">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<RefreshCw className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
<p className="text-muted-foreground">Loading topology...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Network Topology</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
Real-time server infrastructure, proxy connections, and Kubernetes nodes
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-center h-[calc(100vh-250px)] border-2 border-dashed rounded-lg">
|
||||
<div className="flex flex-col items-center gap-2 p-6 text-center">
|
||||
<p className="text-destructive font-semibold">Error loading topology</p>
|
||||
<p className="text-muted-foreground text-sm">{error}</p>
|
||||
<p className="text-muted-foreground text-xs mt-2">Auto-retrying...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!graph || graph.nodes.length === 0) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Network Topology</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
Real-time server infrastructure, proxy connections, and Kubernetes nodes
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-center h-[calc(100vh-250px)] border-2 border-dashed rounded-lg">
|
||||
<div className="flex flex-col items-center gap-2 p-6 text-center">
|
||||
<p className="text-muted-foreground">No servers or infrastructure found</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Create a server to see it appear in the topology
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-primary/10 rounded-lg">
|
||||
<Network className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Network Topology</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
Real-time server infrastructure, proxy connections, and Kubernetes nodes
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TopologyCanvas graph={graph} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { Ban, CheckCircle, Edit, Trash2 } from "lucide-react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { getUserApi } from "@/lib/api-helpers";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
@@ -31,7 +30,8 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { api } from "@/lib/api";
|
||||
import { api } from "@/lib/api-client";
|
||||
import { getUserApi } from "@/lib/api-helpers";
|
||||
import { useSession } from "@/lib/auth-client";
|
||||
|
||||
type User = {
|
||||
@@ -59,8 +59,7 @@ export default function UsersPage() {
|
||||
if (data && typeof data === "object" && "users" in data) {
|
||||
setUsers(data.users as User[]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch users:", error);
|
||||
} catch (_error) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -88,9 +87,7 @@ export default function UsersPage() {
|
||||
await fetchUsers();
|
||||
setEditingUser(null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to update user:", error);
|
||||
}
|
||||
} catch (_error) {}
|
||||
};
|
||||
|
||||
const handleSuspend = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
@@ -110,9 +107,7 @@ export default function UsersPage() {
|
||||
await fetchUsers();
|
||||
setSuspendingUser(null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to suspend user:", error);
|
||||
}
|
||||
} catch (_error) {}
|
||||
};
|
||||
|
||||
const handleUnsuspend = async (userId: string) => {
|
||||
@@ -125,9 +120,7 @@ export default function UsersPage() {
|
||||
if (!error) {
|
||||
await fetchUsers();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to unsuspend user:", error);
|
||||
}
|
||||
} catch (_error) {}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
@@ -140,9 +133,7 @@ export default function UsersPage() {
|
||||
await fetchUsers();
|
||||
setDeleteUser(null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to delete user:", error);
|
||||
}
|
||||
} catch (_error) {}
|
||||
};
|
||||
|
||||
const isUserSuspended = (user: User): boolean => {
|
||||
@@ -252,7 +243,6 @@ export default function UsersPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Edit Dialog */}
|
||||
<Dialog open={!!editingUser} onOpenChange={() => setEditingUser(null)}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
@@ -288,7 +278,6 @@ export default function UsersPage() {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Suspend Dialog */}
|
||||
<Dialog open={!!suspendingUser} onOpenChange={() => setSuspendingUser(null)}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
@@ -324,7 +313,6 @@ export default function UsersPage() {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Delete Dialog */}
|
||||
<Dialog open={!!deleteUser} onOpenChange={() => setDeleteUser(null)}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
|
||||
Reference in New Issue
Block a user