mirror of
https://github.com/YuzuZensai/Minikura.git
synced 2026-09-13 18:59:25 +00:00
✨ feat: refine server management workflows
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
"use client";
|
||||
|
||||
import type { ConnectionInfo } from "@minikura/api";
|
||||
import { Check, Copy } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { api } from "@/lib/api-client";
|
||||
import { getReverseProxyApi } from "@/lib/api-helpers";
|
||||
|
||||
type ConnectionInfoCellProps = {
|
||||
serverId: string;
|
||||
type: "normal" | "proxy";
|
||||
};
|
||||
|
||||
export function ConnectionInfoCell({ serverId, type }: ConnectionInfoCellProps) {
|
||||
const [connectionInfo, setConnectionInfo] = useState<ConnectionInfo | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchConnectionInfo = async () => {
|
||||
try {
|
||||
const reverseProxyApi = getReverseProxyApi();
|
||||
const endpoint =
|
||||
type === "normal"
|
||||
? api.api.servers({ id: serverId })["connection-info"]
|
||||
: reverseProxyApi({ id: serverId })["connection-info"];
|
||||
const res = await endpoint.get();
|
||||
if (res.data) {
|
||||
setConnectionInfo(res.data as ConnectionInfo);
|
||||
}
|
||||
} catch (_error) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchConnectionInfo();
|
||||
}, [serverId, type]);
|
||||
|
||||
const handleCopy = async () => {
|
||||
if (connectionInfo?.connectionString) {
|
||||
await navigator.clipboard.writeText(connectionInfo.connectionString);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <span className="text-muted-foreground text-xs">Loading...</span>;
|
||||
}
|
||||
|
||||
if (!connectionInfo) {
|
||||
return <span className="text-muted-foreground text-xs">N/A</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Badge variant="secondary" className="w-fit text-xs">
|
||||
{connectionInfo.type}
|
||||
</Badge>
|
||||
{connectionInfo.connectionString && (
|
||||
<div className="flex items-center gap-1">
|
||||
<code className="text-xs bg-muted px-1.5 py-0.5 rounded font-mono">
|
||||
{connectionInfo.connectionString}
|
||||
</code>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-5 w-5"
|
||||
onClick={handleCopy}
|
||||
aria-label="Copy connection string"
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="h-3 w-3 text-green-500" />
|
||||
) : (
|
||||
<Copy className="h-3 w-3" />
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{copied ? "Copied!" : "Copy to clipboard"}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
{connectionInfo.note && (
|
||||
<p className="text-xs text-muted-foreground">{connectionInfo.note}</p>
|
||||
)}
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
"use client";
|
||||
|
||||
import type { PodInfo } from "@minikura/api";
|
||||
import { useEffect, useState } from "react";
|
||||
import { StatusBadge } from "@/components/status-badge";
|
||||
import { api } from "@/lib/api-client";
|
||||
|
||||
type ServerStatusCellProps = {
|
||||
serverId: string;
|
||||
type: "normal" | "proxy";
|
||||
};
|
||||
|
||||
export function ServerStatusCell({ serverId, type }: ServerStatusCellProps) {
|
||||
const [pods, setPods] = useState<PodInfo[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPods = async () => {
|
||||
try {
|
||||
const endpoint =
|
||||
type === "normal"
|
||||
? 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) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchPods();
|
||||
}, [serverId, type]);
|
||||
|
||||
if (loading) {
|
||||
return <StatusBadge pulse>Checking</StatusBadge>;
|
||||
}
|
||||
|
||||
if (pods.length === 0) {
|
||||
return <StatusBadge>No pods</StatusBadge>;
|
||||
}
|
||||
|
||||
const allRunning = pods.every((pod) => pod.status === "Running");
|
||||
const readyCount = pods.filter((pod) => pod.ready === "1/1").length;
|
||||
|
||||
return (
|
||||
<StatusBadge tone={allRunning ? "success" : "warning"}>
|
||||
{readyCount}/{pods.length} ready
|
||||
</StatusBadge>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
import type { NormalServer, ReverseProxyServer } from "@minikura/api";
|
||||
import { Pencil, Trash2 } from "lucide-react";
|
||||
import { DataTable, type DataTableColumn } from "@/components/data-table";
|
||||
import { TableActions } from "@/components/section-card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ConnectionInfoCell } from "./connection-info-cell";
|
||||
import { ServerStatusCell } from "./server-status-cell";
|
||||
|
||||
type ServerTableProps =
|
||||
| {
|
||||
type: "normal";
|
||||
servers: NormalServer[];
|
||||
onEdit: (id: string) => void;
|
||||
onDelete: (id: string) => void;
|
||||
}
|
||||
| {
|
||||
type: "proxy";
|
||||
servers: ReverseProxyServer[];
|
||||
onEdit: (id: string) => void;
|
||||
onDelete: (id: string) => void;
|
||||
};
|
||||
|
||||
function RowActions({
|
||||
id,
|
||||
kind,
|
||||
onEdit,
|
||||
onDelete,
|
||||
}: {
|
||||
id: string;
|
||||
kind: string;
|
||||
onEdit: (id: string) => void;
|
||||
onDelete: (id: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<TableActions>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => onEdit(id)}
|
||||
aria-label={`Edit ${kind} ${id}`}
|
||||
>
|
||||
<Pencil />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => onDelete(id)}
|
||||
aria-label={`Delete ${kind} ${id}`}
|
||||
>
|
||||
<Trash2 />
|
||||
</Button>
|
||||
</TableActions>
|
||||
);
|
||||
}
|
||||
|
||||
export function ServerTable(props: ServerTableProps) {
|
||||
if (props.type === "normal") {
|
||||
const columns: readonly DataTableColumn<NormalServer>[] = [
|
||||
{ id: "id", header: "ID", cell: (server) => server.id, className: "font-bold" },
|
||||
{
|
||||
id: "status",
|
||||
header: "Status",
|
||||
cell: (server) => <ServerStatusCell serverId={server.id} type="normal" />,
|
||||
},
|
||||
{
|
||||
id: "storage",
|
||||
header: "Storage",
|
||||
cell: (server) => <Badge variant="outline">{server.type}</Badge>,
|
||||
},
|
||||
{
|
||||
id: "software",
|
||||
header: "Software",
|
||||
cell: (server) => <Badge variant="secondary">{server.jar_type || "VANILLA"}</Badge>,
|
||||
},
|
||||
{
|
||||
id: "version",
|
||||
header: "Version",
|
||||
cell: (server) => server.minecraft_version || "LATEST",
|
||||
className: "text-muted-foreground",
|
||||
},
|
||||
{ id: "memory", header: "Memory", cell: (server) => `${server.memory || 1024} MB` },
|
||||
{
|
||||
id: "network",
|
||||
header: "Network",
|
||||
cell: (server) => <ConnectionInfoCell serverId={server.id} type="normal" />,
|
||||
},
|
||||
{
|
||||
id: "description",
|
||||
header: "Description",
|
||||
cell: (server) => server.description || "-",
|
||||
className: "max-w-64 truncate text-muted-foreground",
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: "Actions",
|
||||
headerClassName: "text-right",
|
||||
className: "text-right",
|
||||
cell: (server) => (
|
||||
<RowActions
|
||||
id={server.id}
|
||||
kind="server"
|
||||
onEdit={props.onEdit}
|
||||
onDelete={props.onDelete}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return <DataTable data={props.servers} columns={columns} getRowKey={(server) => server.id} />;
|
||||
}
|
||||
|
||||
const columns: readonly DataTableColumn<ReverseProxyServer>[] = [
|
||||
{ id: "id", header: "ID", cell: (server) => server.id, className: "font-bold" },
|
||||
{
|
||||
id: "status",
|
||||
header: "Status",
|
||||
cell: (server) => <ServerStatusCell serverId={server.id} type="proxy" />,
|
||||
},
|
||||
{
|
||||
id: "type",
|
||||
header: "Type",
|
||||
cell: (server) => <Badge variant="outline">{server.type}</Badge>,
|
||||
},
|
||||
{
|
||||
id: "external",
|
||||
header: "External",
|
||||
cell: (server) => `${server.external_address}:${server.external_port}`,
|
||||
},
|
||||
{ id: "port", header: "Listen port", cell: (server) => server.listen_port },
|
||||
{ id: "memory", header: "Memory", cell: (server) => `${server.memory} MB` },
|
||||
{
|
||||
id: "network",
|
||||
header: "Network",
|
||||
cell: (server) => <ConnectionInfoCell serverId={server.id} type="proxy" />,
|
||||
},
|
||||
{
|
||||
id: "description",
|
||||
header: "Description",
|
||||
cell: (server) => server.description || "-",
|
||||
className: "max-w-64 truncate text-muted-foreground",
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: "Actions",
|
||||
headerClassName: "text-right",
|
||||
className: "text-right",
|
||||
cell: (server) => (
|
||||
<RowActions id={server.id} kind="proxy" onEdit={props.onEdit} onDelete={props.onDelete} />
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return <DataTable data={props.servers} columns={columns} getRowKey={(server) => server.id} />;
|
||||
}
|
||||
Reference in New Issue
Block a user