mirror of
https://github.com/YuzuZensai/Minikura.git
synced 2026-09-13 10:49:21 +00:00
✨ feat: refine server management workflows
This commit is contained in:
@@ -4,139 +4,20 @@ import type { CreateServerRequest } from "@minikura/api";
|
|||||||
import { ArrowLeft } from "lucide-react";
|
import { ArrowLeft } from "lucide-react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { ServerForm, type ServerFormData } from "@/components/server-form";
|
import { ServerForm, type ServerFormData } from "@/components/server-form";
|
||||||
|
import { PageHeader, PageShell } from "@/components/page-layout";
|
||||||
|
import { SectionCard } from "@/components/section-card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import { api } from "@/lib/api-client";
|
import { api } from "@/lib/api-client";
|
||||||
|
import { toCommonServerRequestFields } from "@/lib/server-form/mappings";
|
||||||
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() {
|
export default function CreateServerPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const handleSubmit = async (data: ServerFormData) => {
|
const handleSubmit = async (data: ServerFormData) => {
|
||||||
const filteredEnvVars = data.envVars.filter((ev) => ev.key && ev.value);
|
|
||||||
|
|
||||||
const envVariables: Record<string, string> = {};
|
|
||||||
|
|
||||||
if (data.allowFlight) envVariables.ALLOW_FLIGHT = String(data.allowFlight);
|
|
||||||
if (data.enableCommandBlock)
|
|
||||||
envVariables.ENABLE_COMMAND_BLOCK = String(data.enableCommandBlock);
|
|
||||||
if (data.spawnProtection) envVariables.SPAWN_PROTECTION = data.spawnProtection;
|
|
||||||
if (data.viewDistance) envVariables.VIEW_DISTANCE = data.viewDistance;
|
|
||||||
if (data.simulationDistance) envVariables.SIMULATION_DISTANCE = data.simulationDistance;
|
|
||||||
|
|
||||||
if (data.levelName) envVariables.LEVEL = data.levelName;
|
|
||||||
if (data.levelSeed) envVariables.SEED = data.levelSeed;
|
|
||||||
if (data.levelType) envVariables.LEVEL_TYPE = data.levelType;
|
|
||||||
if (data.generatorSettings) envVariables.GENERATOR_SETTINGS = data.generatorSettings;
|
|
||||||
if (data.hardcore) envVariables.HARDCORE = String(data.hardcore);
|
|
||||||
if (data.spawnAnimals !== undefined) envVariables.SPAWN_ANIMALS = String(data.spawnAnimals);
|
|
||||||
if (data.spawnMonsters !== undefined) envVariables.SPAWN_MONSTERS = String(data.spawnMonsters);
|
|
||||||
if (data.spawnNpcs !== undefined) envVariables.SPAWN_NPCS = String(data.spawnNpcs);
|
|
||||||
|
|
||||||
if (data.enableWhitelist) envVariables.ENABLE_WHITELIST = String(data.enableWhitelist);
|
|
||||||
if (data.whitelist) envVariables.WHITELIST = data.whitelist;
|
|
||||||
if (data.whitelistFile) envVariables.WHITELIST_FILE = data.whitelistFile;
|
|
||||||
if (data.ops) envVariables.OPS = data.ops;
|
|
||||||
if (data.opsFile) envVariables.OPS_FILE = data.opsFile;
|
|
||||||
|
|
||||||
if (data.jvmXxOpts) envVariables.JVM_XX_OPTS = data.jvmXxOpts;
|
|
||||||
if (data.jvmDdOpts) envVariables.JVM_DD_OPTS = data.jvmDdOpts;
|
|
||||||
if (data.enableJmx) envVariables.ENABLE_JMX = String(data.enableJmx);
|
|
||||||
|
|
||||||
if (data.resourcePack) envVariables.RESOURCE_PACK = data.resourcePack;
|
|
||||||
if (data.resourcePackSha1) envVariables.RESOURCE_PACK_SHA1 = data.resourcePackSha1;
|
|
||||||
if (data.resourcePackEnforce)
|
|
||||||
envVariables.RESOURCE_PACK_ENFORCE = String(data.resourcePackEnforce);
|
|
||||||
|
|
||||||
if (data.enableRcon !== undefined) envVariables.ENABLE_RCON = String(data.enableRcon);
|
|
||||||
if (data.rconPassword) envVariables.RCON_PASSWORD = data.rconPassword;
|
|
||||||
if (data.rconPort) envVariables.RCON_PORT = data.rconPort;
|
|
||||||
if (data.rconCmdsStartup) envVariables.RCON_CMDS_STARTUP = data.rconCmdsStartup;
|
|
||||||
if (data.rconCmdsOnConnect) envVariables.RCON_CMDS_ON_CONNECT = data.rconCmdsOnConnect;
|
|
||||||
if (data.rconCmdsFirstConnect) envVariables.RCON_CMDS_FIRST_CONNECT = data.rconCmdsFirstConnect;
|
|
||||||
if (data.rconCmdsOnDisconnect) envVariables.RCON_CMDS_ON_DISCONNECT = data.rconCmdsOnDisconnect;
|
|
||||||
if (data.rconCmdsLastDisconnect)
|
|
||||||
envVariables.RCON_CMDS_LAST_DISCONNECT = data.rconCmdsLastDisconnect;
|
|
||||||
|
|
||||||
if (data.enableQuery !== undefined) envVariables.ENABLE_QUERY = String(data.enableQuery);
|
|
||||||
if (data.queryPort) envVariables.QUERY_PORT = data.queryPort;
|
|
||||||
|
|
||||||
if (data.enableAutopause) envVariables.ENABLE_AUTOPAUSE = String(data.enableAutopause);
|
|
||||||
if (data.autopauseTimeoutEst) envVariables.AUTOPAUSE_TIMEOUT_EST = data.autopauseTimeoutEst;
|
|
||||||
if (data.autopauseTimeoutInit) envVariables.AUTOPAUSE_TIMEOUT_INIT = data.autopauseTimeoutInit;
|
|
||||||
if (data.autopauseTimeoutKn) envVariables.AUTOPAUSE_TIMEOUT_KN = data.autopauseTimeoutKn;
|
|
||||||
if (data.autopausePeriod) envVariables.AUTOPAUSE_PERIOD = data.autopausePeriod;
|
|
||||||
if (data.autopauseKnockInterface)
|
|
||||||
envVariables.AUTOPAUSE_KNOCK_INTERFACE = data.autopauseKnockInterface;
|
|
||||||
|
|
||||||
if (data.enableAutostop) envVariables.ENABLE_AUTOSTOP = String(data.enableAutostop);
|
|
||||||
if (data.autostopTimeoutEst) envVariables.AUTOSTOP_TIMEOUT_EST = data.autostopTimeoutEst;
|
|
||||||
if (data.autostopTimeoutInit) envVariables.AUTOSTOP_TIMEOUT_INIT = data.autostopTimeoutInit;
|
|
||||||
if (data.autostopPeriod) envVariables.AUTOSTOP_PERIOD = data.autostopPeriod;
|
|
||||||
|
|
||||||
if (data.plugins) envVariables.PLUGINS = data.plugins;
|
|
||||||
if (data.removeOldPlugins) envVariables.REMOVE_OLD_PLUGINS = String(data.removeOldPlugins);
|
|
||||||
if (data.spigetResources) envVariables.SPIGET_RESOURCES = data.spigetResources;
|
|
||||||
|
|
||||||
if (data.paperBuild) envVariables.PAPER_BUILD = data.paperBuild;
|
|
||||||
|
|
||||||
if (data.type === "CUSTOM" && data.customJarUrl) {
|
|
||||||
envVariables.CUSTOM_SERVER = data.customJarUrl;
|
|
||||||
envVariables.VERSION = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.timezone) envVariables.TZ = data.timezone;
|
|
||||||
if (data.uid) envVariables.UID = data.uid;
|
|
||||||
if (data.gid) envVariables.GID = data.gid;
|
|
||||||
if (data.stopDuration) envVariables.STOP_DURATION = data.stopDuration;
|
|
||||||
if (data.serverIcon) envVariables.ICON = data.serverIcon;
|
|
||||||
|
|
||||||
envVariables.EULA = String(data.eula);
|
|
||||||
|
|
||||||
envVariables.TYPE = data.type;
|
|
||||||
if (data.type !== "CUSTOM" && data.version) {
|
|
||||||
envVariables.VERSION = data.version;
|
|
||||||
}
|
|
||||||
if (data.type === "CUSTOM" && !data.customJarUrl) {
|
|
||||||
throw new Error("Custom jar URL is required for custom servers");
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const envVar of filteredEnvVars) {
|
|
||||||
envVariables[envVar.key] = envVar.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload: CreateServerRequest = {
|
const payload: CreateServerRequest = {
|
||||||
id: data.id.trim(),
|
id: data.id.trim(),
|
||||||
description: data.description.trim() || null,
|
|
||||||
listen_port: Number(data.listenPort),
|
|
||||||
type: "STATEFUL",
|
type: "STATEFUL",
|
||||||
service_type: data.serviceType,
|
...toCommonServerRequestFields(data),
|
||||||
node_port: data.serviceType === "NODE_PORT" && data.nodePort ? Number(data.nodePort) : null,
|
|
||||||
env_variables: Object.entries(envVariables).map(([key, value]) => ({ key, value })),
|
|
||||||
memory: data.memoryLimit ? Number(data.memoryLimit) : undefined,
|
|
||||||
memory_request: data.memoryRequest ? Number(data.memoryRequest) : undefined,
|
|
||||||
cpu_request: data.cpuRequest || undefined,
|
|
||||||
cpu_limit: data.cpuLimit || undefined,
|
|
||||||
jar_type: data.type === "CUSTOM" ? "VANILLA" : data.type,
|
|
||||||
minecraft_version: data.type === "CUSTOM" ? undefined : data.version || "LATEST",
|
|
||||||
jvm_opts: data.jvmOpts || undefined,
|
|
||||||
use_aikar_flags: data.useAikarFlags || undefined,
|
|
||||||
use_meowice_flags: data.useMeowiceFlags || undefined,
|
|
||||||
difficulty: toDifficultyUppercase(data.difficulty),
|
|
||||||
game_mode: toModeUppercase(data.mode),
|
|
||||||
max_players: data.maxPlayers ? Number(data.maxPlayers) : undefined,
|
|
||||||
pvp: data.pvp,
|
|
||||||
online_mode: data.onlineMode,
|
|
||||||
motd: data.motd,
|
|
||||||
level_seed: data.levelSeed,
|
|
||||||
level_type: data.levelType,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await api.api.servers.post(payload);
|
const response = await api.api.servers.post(payload);
|
||||||
@@ -158,35 +39,29 @@ export default function CreateServerPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<PageShell>
|
||||||
<div className="flex items-center gap-4">
|
<PageHeader
|
||||||
|
eyebrow="Compute / Provision"
|
||||||
|
title="Create Server"
|
||||||
|
description="Define the runtime, world, resources, and network contract."
|
||||||
|
leading={
|
||||||
<Button variant="ghost" size="icon" onClick={() => router.push("/dashboard/servers")}>
|
<Button variant="ghost" size="icon" onClick={() => router.push("/dashboard/servers")}>
|
||||||
<ArrowLeft className="h-5 w-5" />
|
<ArrowLeft className="size-5" />
|
||||||
</Button>
|
</Button>
|
||||||
<div>
|
}
|
||||||
<h1 className="text-3xl font-bold">Create Minecraft Server</h1>
|
className="flex-row items-center justify-start"
|
||||||
<p className="text-muted-foreground mt-1">
|
/>
|
||||||
Configure your new Minecraft server with comprehensive settings
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Card>
|
<SectionCard
|
||||||
<CardHeader>
|
title="Server Configuration"
|
||||||
<CardTitle>Server Configuration</CardTitle>
|
description="Complete configuration for the itzg/minecraft-server workload."
|
||||||
<CardDescription>
|
>
|
||||||
Complete configuration for itzg/minecraft-server Docker image with all environment
|
|
||||||
variables
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<ServerForm
|
<ServerForm
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
onCancel={() => router.push("/dashboard/servers")}
|
onCancel={() => router.push("/dashboard/servers")}
|
||||||
submitLabel="Create Server"
|
submitLabel="Create Server"
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</SectionCard>
|
||||||
</Card>
|
</PageShell>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ import type { NormalServer, UpdateServerRequest } from "@minikura/api";
|
|||||||
import { ArrowLeft } from "lucide-react";
|
import { ArrowLeft } from "lucide-react";
|
||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { ServerForm, type ServerFormData, type ServerType } from "@/components/server-form";
|
import { ServerForm, type ServerFormData } from "@/components/server-form";
|
||||||
|
import { PageHeader, PageShell, StatePanel } from "@/components/page-layout";
|
||||||
|
import { SectionCard } from "@/components/section-card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import { api } from "@/lib/api-client";
|
import { api } from "@/lib/api-client";
|
||||||
import { getReverseProxyApi } from "@/lib/api-helpers";
|
import { getReverseProxyApi } from "@/lib/api-helpers";
|
||||||
|
import { toCommonServerRequestFields, toInitialServerFormData } from "@/lib/server-form/mappings";
|
||||||
|
|
||||||
export default function EditServerPage() {
|
export default function EditServerPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -59,161 +61,10 @@ export default function EditServerPage() {
|
|||||||
}
|
}
|
||||||
}, [serverId]);
|
}, [serverId]);
|
||||||
|
|
||||||
const toServiceType = (value?: string | null): ServerFormData["serviceType"] => {
|
|
||||||
if (value === "NODE_PORT" || value === "LOAD_BALANCER") {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
return "CLUSTER_IP";
|
|
||||||
};
|
|
||||||
|
|
||||||
const toDifficulty = (value?: string | null): ServerFormData["difficulty"] => {
|
|
||||||
if (value === "peaceful" || value === "easy" || value === "normal" || value === "hard") {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
return "easy";
|
|
||||||
};
|
|
||||||
|
|
||||||
const toMode = (value?: string | null): ServerFormData["mode"] => {
|
|
||||||
if (value === "creative" || value === "adventure" || value === "spectator") {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
return "survival";
|
|
||||||
};
|
|
||||||
|
|
||||||
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 }>) => {
|
|
||||||
if (!envVars) return {};
|
|
||||||
|
|
||||||
const parsed: Record<string, string> = {};
|
|
||||||
for (const { key, value } of envVars) {
|
|
||||||
parsed[key] = value;
|
|
||||||
}
|
|
||||||
return parsed;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async (data: ServerFormData) => {
|
const handleSubmit = async (data: ServerFormData) => {
|
||||||
if (!serverData) return;
|
if (!serverData) return;
|
||||||
|
|
||||||
const filteredEnvVars = data.envVars.filter((ev) => ev.key && ev.value);
|
const payload: UpdateServerRequest = toCommonServerRequestFields(data);
|
||||||
|
|
||||||
const envVariables: Record<string, string> = {};
|
|
||||||
if (data.allowFlight) envVariables.ALLOW_FLIGHT = String(data.allowFlight);
|
|
||||||
if (data.enableCommandBlock)
|
|
||||||
envVariables.ENABLE_COMMAND_BLOCK = String(data.enableCommandBlock);
|
|
||||||
if (data.spawnProtection) envVariables.SPAWN_PROTECTION = data.spawnProtection;
|
|
||||||
if (data.viewDistance) envVariables.VIEW_DISTANCE = data.viewDistance;
|
|
||||||
if (data.simulationDistance) envVariables.SIMULATION_DISTANCE = data.simulationDistance;
|
|
||||||
|
|
||||||
if (data.levelSeed) envVariables.SEED = data.levelSeed;
|
|
||||||
if (data.levelType) envVariables.LEVEL_TYPE = data.levelType;
|
|
||||||
if (data.generatorSettings) envVariables.GENERATOR_SETTINGS = data.generatorSettings;
|
|
||||||
if (data.hardcore) envVariables.HARDCORE = String(data.hardcore);
|
|
||||||
if (data.spawnAnimals !== undefined) envVariables.SPAWN_ANIMALS = String(data.spawnAnimals);
|
|
||||||
if (data.spawnMonsters !== undefined) envVariables.SPAWN_MONSTERS = String(data.spawnMonsters);
|
|
||||||
if (data.spawnNpcs !== undefined) envVariables.SPAWN_NPCS = String(data.spawnNpcs);
|
|
||||||
|
|
||||||
if (data.enableWhitelist) envVariables.ENABLE_WHITELIST = String(data.enableWhitelist);
|
|
||||||
if (data.whitelist) envVariables.WHITELIST = data.whitelist;
|
|
||||||
if (data.whitelistFile) envVariables.WHITELIST_FILE = data.whitelistFile;
|
|
||||||
if (data.ops) envVariables.OPS = data.ops;
|
|
||||||
if (data.opsFile) envVariables.OPS_FILE = data.opsFile;
|
|
||||||
|
|
||||||
if (data.jvmXxOpts) envVariables.JVM_XX_OPTS = data.jvmXxOpts;
|
|
||||||
if (data.jvmDdOpts) envVariables.JVM_DD_OPTS = data.jvmDdOpts;
|
|
||||||
if (data.enableJmx) envVariables.ENABLE_JMX = String(data.enableJmx);
|
|
||||||
|
|
||||||
if (data.resourcePack) envVariables.RESOURCE_PACK = data.resourcePack;
|
|
||||||
if (data.resourcePackSha1) envVariables.RESOURCE_PACK_SHA1 = data.resourcePackSha1;
|
|
||||||
if (data.resourcePackEnforce)
|
|
||||||
envVariables.RESOURCE_PACK_ENFORCE = String(data.resourcePackEnforce);
|
|
||||||
|
|
||||||
if (data.enableRcon !== undefined) envVariables.ENABLE_RCON = String(data.enableRcon);
|
|
||||||
if (data.rconPassword) envVariables.RCON_PASSWORD = data.rconPassword;
|
|
||||||
if (data.rconPort) envVariables.RCON_PORT = data.rconPort;
|
|
||||||
if (data.rconCmdsStartup) envVariables.RCON_CMDS_STARTUP = data.rconCmdsStartup;
|
|
||||||
if (data.rconCmdsOnConnect) envVariables.RCON_CMDS_ON_CONNECT = data.rconCmdsOnConnect;
|
|
||||||
if (data.rconCmdsFirstConnect) envVariables.RCON_CMDS_FIRST_CONNECT = data.rconCmdsFirstConnect;
|
|
||||||
if (data.rconCmdsOnDisconnect) envVariables.RCON_CMDS_ON_DISCONNECT = data.rconCmdsOnDisconnect;
|
|
||||||
if (data.rconCmdsLastDisconnect)
|
|
||||||
envVariables.RCON_CMDS_LAST_DISCONNECT = data.rconCmdsLastDisconnect;
|
|
||||||
|
|
||||||
if (data.enableQuery !== undefined) envVariables.ENABLE_QUERY = String(data.enableQuery);
|
|
||||||
if (data.queryPort) envVariables.QUERY_PORT = data.queryPort;
|
|
||||||
|
|
||||||
if (data.enableAutopause) envVariables.ENABLE_AUTOPAUSE = String(data.enableAutopause);
|
|
||||||
if (data.autopauseTimeoutEst) envVariables.AUTOPAUSE_TIMEOUT_EST = data.autopauseTimeoutEst;
|
|
||||||
if (data.autopauseTimeoutInit) envVariables.AUTOPAUSE_TIMEOUT_INIT = data.autopauseTimeoutInit;
|
|
||||||
if (data.autopauseTimeoutKn) envVariables.AUTOPAUSE_TIMEOUT_KN = data.autopauseTimeoutKn;
|
|
||||||
if (data.autopausePeriod) envVariables.AUTOPAUSE_PERIOD = data.autopausePeriod;
|
|
||||||
if (data.autopauseKnockInterface)
|
|
||||||
envVariables.AUTOPAUSE_KNOCK_INTERFACE = data.autopauseKnockInterface;
|
|
||||||
|
|
||||||
if (data.enableAutostop) envVariables.ENABLE_AUTOSTOP = String(data.enableAutostop);
|
|
||||||
if (data.autostopTimeoutEst) envVariables.AUTOSTOP_TIMEOUT_EST = data.autostopTimeoutEst;
|
|
||||||
if (data.autostopTimeoutInit) envVariables.AUTOSTOP_TIMEOUT_INIT = data.autostopTimeoutInit;
|
|
||||||
if (data.autostopPeriod) envVariables.AUTOSTOP_PERIOD = data.autostopPeriod;
|
|
||||||
|
|
||||||
if (data.plugins) envVariables.PLUGINS = data.plugins;
|
|
||||||
if (data.removeOldPlugins) envVariables.REMOVE_OLD_PLUGINS = String(data.removeOldPlugins);
|
|
||||||
if (data.spigetResources) envVariables.SPIGET_RESOURCES = data.spigetResources;
|
|
||||||
|
|
||||||
if (data.paperBuild) envVariables.PAPER_BUILD = data.paperBuild;
|
|
||||||
|
|
||||||
if (data.type === "CUSTOM" && data.customJarUrl) {
|
|
||||||
envVariables.CUSTOM_SERVER = data.customJarUrl;
|
|
||||||
envVariables.VERSION = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.timezone) envVariables.TZ = data.timezone;
|
|
||||||
if (data.uid) envVariables.UID = data.uid;
|
|
||||||
if (data.gid) envVariables.GID = data.gid;
|
|
||||||
if (data.stopDuration) envVariables.STOP_DURATION = data.stopDuration;
|
|
||||||
if (data.serverIcon) envVariables.ICON = data.serverIcon;
|
|
||||||
|
|
||||||
envVariables.EULA = String(data.eula);
|
|
||||||
envVariables.TYPE = data.type;
|
|
||||||
if (data.type !== "CUSTOM" && data.version) {
|
|
||||||
envVariables.VERSION = data.version;
|
|
||||||
}
|
|
||||||
if (data.type === "CUSTOM" && !data.customJarUrl) {
|
|
||||||
throw new Error("Custom jar URL is required for custom servers");
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const envVar of filteredEnvVars) {
|
|
||||||
envVariables[envVar.key] = envVar.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload: UpdateServerRequest = {
|
|
||||||
description: data.description.trim() || null,
|
|
||||||
listen_port: Number(data.listenPort),
|
|
||||||
service_type: data.serviceType,
|
|
||||||
node_port: data.serviceType === "NODE_PORT" && data.nodePort ? Number(data.nodePort) : null,
|
|
||||||
env_variables: Object.entries(envVariables).map(([key, value]) => ({ key, value })),
|
|
||||||
memory: data.memoryLimit ? Number(data.memoryLimit) : undefined,
|
|
||||||
memory_request: data.memoryRequest ? Number(data.memoryRequest) : undefined,
|
|
||||||
cpu_request: data.cpuRequest || undefined,
|
|
||||||
cpu_limit: data.cpuLimit || undefined,
|
|
||||||
jar_type: data.type === "CUSTOM" ? "VANILLA" : data.type,
|
|
||||||
minecraft_version: data.type === "CUSTOM" ? undefined : data.version || "LATEST",
|
|
||||||
jvm_opts: data.jvmOpts || undefined,
|
|
||||||
use_aikar_flags: data.useAikarFlags || undefined,
|
|
||||||
use_meowice_flags: data.useMeowiceFlags || undefined,
|
|
||||||
difficulty: toDifficultyUppercase(data.difficulty),
|
|
||||||
game_mode: toModeUppercase(data.mode),
|
|
||||||
max_players: data.maxPlayers ? Number(data.maxPlayers) : undefined,
|
|
||||||
pvp: data.pvp,
|
|
||||||
online_mode: data.onlineMode,
|
|
||||||
motd: data.motd,
|
|
||||||
level_seed: data.levelSeed,
|
|
||||||
level_type: data.levelType,
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await api.api.servers({ id: serverId }).patch(payload);
|
const response = await api.api.servers({ id: serverId }).patch(payload);
|
||||||
|
|
||||||
@@ -234,227 +85,55 @@ export default function EditServerPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return <StatePanel loading title="Loading server data..." className="min-h-[50vh]" />;
|
||||||
<div className="flex items-center justify-center min-h-screen">
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto mb-4"></div>
|
|
||||||
<p className="text-muted-foreground">Loading server data...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error || !serverData) {
|
if (error || !serverData) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center min-h-screen">
|
<StatePanel
|
||||||
<div className="text-center">
|
title={error || "Server not found"}
|
||||||
<div className="bg-destructive/10 text-destructive px-6 py-4 rounded-lg">
|
tone="error"
|
||||||
{error || "Server not found"}
|
className="min-h-[50vh]"
|
||||||
</div>
|
action={
|
||||||
<Button
|
<Button
|
||||||
className="mt-4"
|
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => router.push("/dashboard/servers")}
|
onClick={() => router.push("/dashboard/servers")}
|
||||||
>
|
>
|
||||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
<ArrowLeft className="size-4" />
|
||||||
Back to Servers
|
Back to Servers
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
}
|
||||||
</div>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const envVars = parseEnvVariables(serverData.env_variables);
|
const initialData = toInitialServerFormData(serverData);
|
||||||
|
|
||||||
const dockerManagedKeys = new Set([
|
|
||||||
"EULA",
|
|
||||||
"TYPE",
|
|
||||||
"VERSION",
|
|
||||||
"CUSTOM_SERVER",
|
|
||||||
"MOTD",
|
|
||||||
"DIFFICULTY",
|
|
||||||
"MODE",
|
|
||||||
"MAX_PLAYERS",
|
|
||||||
"PVP",
|
|
||||||
"ONLINE_MODE",
|
|
||||||
"ALLOW_FLIGHT",
|
|
||||||
"ENABLE_COMMAND_BLOCK",
|
|
||||||
"SPAWN_PROTECTION",
|
|
||||||
"VIEW_DISTANCE",
|
|
||||||
"SIMULATION_DISTANCE",
|
|
||||||
"LEVEL",
|
|
||||||
"SEED",
|
|
||||||
"LEVEL_TYPE",
|
|
||||||
"GENERATOR_SETTINGS",
|
|
||||||
"HARDCORE",
|
|
||||||
"SPAWN_ANIMALS",
|
|
||||||
"SPAWN_MONSTERS",
|
|
||||||
"SPAWN_NPCS",
|
|
||||||
"ENABLE_WHITELIST",
|
|
||||||
"WHITELIST",
|
|
||||||
"WHITELIST_FILE",
|
|
||||||
"OPS",
|
|
||||||
"OPS_FILE",
|
|
||||||
"USE_AIKAR_FLAGS",
|
|
||||||
"USE_MEOWICE_FLAGS",
|
|
||||||
"JVM_OPTS",
|
|
||||||
"JVM_XX_OPTS",
|
|
||||||
"JVM_DD_OPTS",
|
|
||||||
"ENABLE_JMX",
|
|
||||||
"RESOURCE_PACK",
|
|
||||||
"RESOURCE_PACK_SHA1",
|
|
||||||
"RESOURCE_PACK_ENFORCE",
|
|
||||||
"ENABLE_RCON",
|
|
||||||
"RCON_PASSWORD",
|
|
||||||
"RCON_PORT",
|
|
||||||
"RCON_CMDS_STARTUP",
|
|
||||||
"RCON_CMDS_ON_CONNECT",
|
|
||||||
"RCON_CMDS_FIRST_CONNECT",
|
|
||||||
"RCON_CMDS_ON_DISCONNECT",
|
|
||||||
"RCON_CMDS_LAST_DISCONNECT",
|
|
||||||
"ENABLE_QUERY",
|
|
||||||
"QUERY_PORT",
|
|
||||||
"ENABLE_AUTOPAUSE",
|
|
||||||
"AUTOPAUSE_TIMEOUT_EST",
|
|
||||||
"AUTOPAUSE_TIMEOUT_INIT",
|
|
||||||
"AUTOPAUSE_TIMEOUT_KN",
|
|
||||||
"AUTOPAUSE_PERIOD",
|
|
||||||
"AUTOPAUSE_KNOCK_INTERFACE",
|
|
||||||
"ENABLE_AUTOSTOP",
|
|
||||||
"AUTOSTOP_TIMEOUT_EST",
|
|
||||||
"AUTOSTOP_TIMEOUT_INIT",
|
|
||||||
"AUTOSTOP_PERIOD",
|
|
||||||
"PLUGINS",
|
|
||||||
"REMOVE_OLD_PLUGINS",
|
|
||||||
"SPIGET_RESOURCES",
|
|
||||||
"PAPER_BUILD",
|
|
||||||
"TZ",
|
|
||||||
"UID",
|
|
||||||
"GID",
|
|
||||||
"STOP_DURATION",
|
|
||||||
"ICON",
|
|
||||||
]);
|
|
||||||
|
|
||||||
const customEnvVars = Object.entries(envVars)
|
|
||||||
.filter(([key]) => !dockerManagedKeys.has(key))
|
|
||||||
.map(([key, value]) => ({ id: crypto.randomUUID(), key, value }));
|
|
||||||
|
|
||||||
const initialData: Partial<ServerFormData> = {
|
|
||||||
id: serverData.id,
|
|
||||||
description: serverData.description || "",
|
|
||||||
memoryLimit: String(serverData.memory || 2048),
|
|
||||||
memoryRequest: String(serverData.memory_request ?? 1024),
|
|
||||||
cpuRequest: serverData.cpu_request || "500m",
|
|
||||||
cpuLimit: serverData.cpu_limit || "2",
|
|
||||||
type: (envVars.TYPE || serverData.jar_type || "PAPER") as ServerType,
|
|
||||||
version: envVars.VERSION || serverData.minecraft_version || "",
|
|
||||||
customJarUrl: envVars.CUSTOM_SERVER || undefined,
|
|
||||||
eula: envVars.EULA === "true",
|
|
||||||
listenPort: String(serverData.listen_port || 25565),
|
|
||||||
serviceType: toServiceType(serverData.service_type),
|
|
||||||
nodePort: serverData.node_port ? String(serverData.node_port) : undefined,
|
|
||||||
|
|
||||||
motd: envVars.MOTD || serverData.motd || undefined,
|
|
||||||
difficulty: toDifficulty(envVars.DIFFICULTY || serverData.difficulty),
|
|
||||||
mode: toMode(envVars.MODE || serverData.game_mode),
|
|
||||||
maxPlayers: envVars.MAX_PLAYERS || String(serverData.max_players || 20),
|
|
||||||
pvp: envVars.PVP ? envVars.PVP === "true" : (serverData.pvp ?? true),
|
|
||||||
onlineMode: envVars.ONLINE_MODE
|
|
||||||
? envVars.ONLINE_MODE === "true"
|
|
||||||
: (serverData.online_mode ?? true),
|
|
||||||
allowFlight: envVars.ALLOW_FLIGHT === "true",
|
|
||||||
enableCommandBlock: envVars.ENABLE_COMMAND_BLOCK === "true",
|
|
||||||
spawnProtection: envVars.SPAWN_PROTECTION || "16",
|
|
||||||
viewDistance: envVars.VIEW_DISTANCE || "10",
|
|
||||||
simulationDistance: envVars.SIMULATION_DISTANCE || "10",
|
|
||||||
|
|
||||||
levelName: envVars.LEVEL || "world",
|
|
||||||
levelSeed: envVars.SEED || serverData.level_seed || undefined,
|
|
||||||
levelType: envVars.LEVEL_TYPE || serverData.level_type || undefined,
|
|
||||||
generatorSettings: envVars.GENERATOR_SETTINGS || undefined,
|
|
||||||
hardcore: envVars.HARDCORE === "true",
|
|
||||||
spawnAnimals: envVars.SPAWN_ANIMALS !== "false",
|
|
||||||
spawnMonsters: envVars.SPAWN_MONSTERS !== "false",
|
|
||||||
spawnNpcs: envVars.SPAWN_NPCS !== "false",
|
|
||||||
|
|
||||||
enableWhitelist: envVars.ENABLE_WHITELIST === "true",
|
|
||||||
whitelist: envVars.WHITELIST || undefined,
|
|
||||||
whitelistFile: envVars.WHITELIST_FILE || undefined,
|
|
||||||
ops: envVars.OPS || undefined,
|
|
||||||
opsFile: envVars.OPS_FILE || undefined,
|
|
||||||
|
|
||||||
useAikarFlags: envVars.USE_AIKAR_FLAGS === "true" || serverData.use_aikar_flags || false,
|
|
||||||
useMeowiceFlags: envVars.USE_MEOWICE_FLAGS === "true" || serverData.use_meowice_flags || false,
|
|
||||||
jvmOpts: envVars.JVM_OPTS || serverData.jvm_opts || undefined,
|
|
||||||
jvmXxOpts: envVars.JVM_XX_OPTS || undefined,
|
|
||||||
jvmDdOpts: envVars.JVM_DD_OPTS || undefined,
|
|
||||||
enableJmx: envVars.ENABLE_JMX === "true",
|
|
||||||
|
|
||||||
resourcePack: envVars.RESOURCE_PACK || undefined,
|
|
||||||
resourcePackSha1: envVars.RESOURCE_PACK_SHA1 || undefined,
|
|
||||||
resourcePackEnforce: envVars.RESOURCE_PACK_ENFORCE === "true",
|
|
||||||
|
|
||||||
enableRcon: envVars.ENABLE_RCON !== "false",
|
|
||||||
rconPassword: envVars.RCON_PASSWORD || undefined,
|
|
||||||
rconPort: envVars.RCON_PORT || "25575",
|
|
||||||
rconCmdsStartup: envVars.RCON_CMDS_STARTUP || undefined,
|
|
||||||
rconCmdsOnConnect: envVars.RCON_CMDS_ON_CONNECT || undefined,
|
|
||||||
rconCmdsFirstConnect: envVars.RCON_CMDS_FIRST_CONNECT || undefined,
|
|
||||||
rconCmdsOnDisconnect: envVars.RCON_CMDS_ON_DISCONNECT || undefined,
|
|
||||||
rconCmdsLastDisconnect: envVars.RCON_CMDS_LAST_DISCONNECT || undefined,
|
|
||||||
|
|
||||||
enableQuery: envVars.ENABLE_QUERY === "true",
|
|
||||||
queryPort: envVars.QUERY_PORT || "25565",
|
|
||||||
|
|
||||||
enableAutopause: envVars.ENABLE_AUTOPAUSE === "true",
|
|
||||||
autopauseTimeoutEst: envVars.AUTOPAUSE_TIMEOUT_EST || "3600",
|
|
||||||
autopauseTimeoutInit: envVars.AUTOPAUSE_TIMEOUT_INIT || "600",
|
|
||||||
autopauseTimeoutKn: envVars.AUTOPAUSE_TIMEOUT_KN || "120",
|
|
||||||
autopausePeriod: envVars.AUTOPAUSE_PERIOD || "10",
|
|
||||||
autopauseKnockInterface: envVars.AUTOPAUSE_KNOCK_INTERFACE || "eth0",
|
|
||||||
|
|
||||||
enableAutostop: envVars.ENABLE_AUTOSTOP === "true",
|
|
||||||
autostopTimeoutEst: envVars.AUTOSTOP_TIMEOUT_EST || "3600",
|
|
||||||
autostopTimeoutInit: envVars.AUTOSTOP_TIMEOUT_INIT || "1800",
|
|
||||||
autostopPeriod: envVars.AUTOSTOP_PERIOD || "10",
|
|
||||||
|
|
||||||
plugins: envVars.PLUGINS || undefined,
|
|
||||||
removeOldPlugins: envVars.REMOVE_OLD_PLUGINS === "true",
|
|
||||||
spigetResources: envVars.SPIGET_RESOURCES || undefined,
|
|
||||||
|
|
||||||
paperBuild: envVars.PAPER_BUILD || undefined,
|
|
||||||
|
|
||||||
serverIcon: envVars.ICON || undefined,
|
|
||||||
|
|
||||||
envVars: customEnvVars,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<PageShell>
|
||||||
<div className="flex items-center gap-4">
|
<PageHeader
|
||||||
|
eyebrow="Compute / Configure"
|
||||||
|
title={<>Edit {serverData.id}</>}
|
||||||
|
description="Update the workload specification and deployment behavior."
|
||||||
|
leading={
|
||||||
<Button variant="ghost" size="icon" onClick={() => router.push("/dashboard/servers")}>
|
<Button variant="ghost" size="icon" onClick={() => router.push("/dashboard/servers")}>
|
||||||
<ArrowLeft className="h-5 w-5" />
|
<ArrowLeft className="size-5" />
|
||||||
</Button>
|
</Button>
|
||||||
<div>
|
}
|
||||||
<h1 className="text-3xl font-bold">Edit Server: {serverData.id}</h1>
|
className="flex-row items-center justify-start"
|
||||||
<p className="text-muted-foreground mt-1">Update your Minecraft server configuration</p>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Card>
|
<SectionCard
|
||||||
<CardHeader>
|
title="Server Configuration"
|
||||||
<CardTitle>Server Configuration</CardTitle>
|
description="Modify settings for your Minecraft server"
|
||||||
<CardDescription>Modify settings for your Minecraft server</CardDescription>
|
>
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<ServerForm
|
<ServerForm
|
||||||
initialData={initialData}
|
initialData={initialData}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
onCancel={() => router.push("/dashboard/servers")}
|
onCancel={() => router.push("/dashboard/servers")}
|
||||||
submitLabel="Save Changes"
|
submitLabel="Save Changes"
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</SectionCard>
|
||||||
</Card>
|
</PageShell>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,451 +1,102 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { ConnectionInfo, NormalServer, PodInfo, ReverseProxyServer } from "@minikura/api";
|
import { Globe, Plus, Server, ServerCog } from "lucide-react";
|
||||||
import {
|
|
||||||
AlertCircle,
|
|
||||||
Check,
|
|
||||||
CheckCircle2,
|
|
||||||
Copy,
|
|
||||||
FileText,
|
|
||||||
Globe,
|
|
||||||
Pencil,
|
|
||||||
Plus,
|
|
||||||
Server,
|
|
||||||
Trash2,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useState } from "react";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { ConfirmDialog } from "@/components/confirm-dialog";
|
||||||
|
import { PageHeader, PageShell, StatePanel } from "@/components/page-layout";
|
||||||
|
import { ResourceSection } from "@/components/section-card";
|
||||||
|
import { ServerTable } from "@/components/servers/server-table";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { useServerList } from "@/hooks/use-server-list";
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from "@/components/ui/table";
|
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
|
||||||
import { api } from "@/lib/api-client";
|
|
||||||
import { getReverseProxyApi } from "@/lib/api-helpers";
|
|
||||||
|
|
||||||
function ServerStatusCell({ serverId, type }: { serverId: string; type: "normal" | "proxy" }) {
|
|
||||||
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 (
|
|
||||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
||||||
<div className="h-3 w-3 animate-pulse bg-muted rounded-full" />
|
|
||||||
Loading...
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pods.length === 0) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
||||||
<AlertCircle className="h-3 w-3" />
|
|
||||||
No pods
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const allRunning = pods.every((pod) => pod.status === "Running");
|
|
||||||
const readyCount = pods.filter((pod) => pod.ready === "1/1").length;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{allRunning ? (
|
|
||||||
<CheckCircle2 className="h-3 w-3 text-green-500" />
|
|
||||||
) : (
|
|
||||||
<AlertCircle className="h-3 w-3 text-yellow-500" />
|
|
||||||
)}
|
|
||||||
<span className="text-xs">
|
|
||||||
{readyCount}/{pods.length} Ready
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ConnectionInfoCell({ serverId, type }: { serverId: string; type: "normal" | "proxy" }) {
|
|
||||||
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}>
|
|
||||||
{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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ServersPage() {
|
export default function ServersPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [normalServers, setNormalServers] = useState<NormalServer[]>([]);
|
const { normalServers, reverseProxies, loading, error, deleteServer } = useServerList();
|
||||||
const [reverseProxies, setReverseProxies] = useState<ReverseProxyServer[]>([]);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [deleteTarget, setDeleteTarget] = useState<{
|
const [deleteTarget, setDeleteTarget] = useState<{
|
||||||
id: string;
|
id: string;
|
||||||
type: "normal" | "proxy";
|
type: "normal" | "proxy";
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
const fetchServers = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const reverseProxyApi = getReverseProxyApi();
|
|
||||||
const [normalRes, proxyRes] = await Promise.all([
|
|
||||||
api.api.servers.get(),
|
|
||||||
reverseProxyApi.get(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (normalRes.data) {
|
|
||||||
setNormalServers(normalRes.data as unknown as NormalServer[]);
|
|
||||||
}
|
|
||||||
if (proxyRes.data) {
|
|
||||||
setReverseProxies(proxyRes.data as unknown as ReverseProxyServer[]);
|
|
||||||
}
|
|
||||||
} catch (_error) {
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchServers();
|
|
||||||
}, [fetchServers]);
|
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
if (!deleteTarget) return;
|
if (!deleteTarget) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const reverseProxyApi = getReverseProxyApi();
|
await deleteServer(deleteTarget.id, deleteTarget.type);
|
||||||
if (deleteTarget.type === "normal") {
|
|
||||||
await api.api.servers({ id: deleteTarget.id }).delete();
|
|
||||||
} else {
|
|
||||||
await reverseProxyApi({ id: deleteTarget.id }).delete();
|
|
||||||
}
|
|
||||||
await fetchServers();
|
|
||||||
setDeleteTarget(null);
|
setDeleteTarget(null);
|
||||||
} catch (_error) {}
|
} catch (_error) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<PageShell>
|
||||||
<div>
|
<PageHeader
|
||||||
<h1 className="text-3xl font-bold">Server Management</h1>
|
eyebrow="Workloads"
|
||||||
<p className="text-muted-foreground mt-1">Manage your Minecraft servers</p>
|
title="Servers"
|
||||||
</div>
|
description="Provision Minecraft runtimes and route traffic through edge proxies."
|
||||||
<div className="flex items-center justify-center h-64">
|
actions={
|
||||||
<p className="text-muted-foreground">Loading...</p>
|
<Button size="lg" onClick={() => router.push("/dashboard/servers/create")}>
|
||||||
</div>
|
<Plus className="size-4" />
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold">Server Management</h1>
|
|
||||||
<p className="text-muted-foreground mt-1">
|
|
||||||
Manage your Minecraft servers and reverse proxies
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Button onClick={() => router.push("/dashboard/servers/create")}>
|
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
|
||||||
Create Server
|
Create Server
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<Card>
|
{loading ? (
|
||||||
<CardHeader>
|
<StatePanel loading title="Discovering workloads..." className="h-64" />
|
||||||
<div className="flex items-center gap-2">
|
) : error ? (
|
||||||
<Server className="h-5 w-5" />
|
<StatePanel title="Unable to load workloads" description={error} tone="error" />
|
||||||
<CardTitle>Minecraft Servers</CardTitle>
|
|
||||||
</div>
|
|
||||||
<CardDescription>Manage your normal Minecraft server instances</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
{normalServers.length === 0 ? (
|
|
||||||
<div className="flex items-center justify-center h-32 border-2 border-dashed rounded-lg">
|
|
||||||
<p className="text-muted-foreground">No servers created yet</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="overflow-x-auto">
|
<>
|
||||||
<Table>
|
<ResourceSection
|
||||||
<TableHeader>
|
title="Minecraft Workloads"
|
||||||
<TableRow>
|
description="Manage your normal Minecraft server instances"
|
||||||
<TableHead>ID</TableHead>
|
icon={<ServerCog className="size-5 text-primary" />}
|
||||||
<TableHead>Status</TableHead>
|
count={normalServers.length}
|
||||||
<TableHead>Storage</TableHead>
|
emptyIcon={<Server className="size-6" />}
|
||||||
<TableHead>Software</TableHead>
|
emptyTitle="No workloads"
|
||||||
<TableHead>Version</TableHead>
|
emptyDescription="Create a server to begin."
|
||||||
<TableHead>Memory (MB)</TableHead>
|
|
||||||
<TableHead>Network</TableHead>
|
|
||||||
<TableHead>Description</TableHead>
|
|
||||||
<TableHead className="text-right">Actions</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{normalServers.map((server) => (
|
|
||||||
<TableRow key={server.id}>
|
|
||||||
<TableCell className="font-medium">{server.id}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<ServerStatusCell serverId={server.id} type="normal" />
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Badge variant="outline">{server.type}</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Badge variant="secondary">{server.jar_type || "VANILLA"}</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-muted-foreground">
|
|
||||||
{server.minecraft_version || "LATEST"}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>{server.memory || 1024}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<ConnectionInfoCell serverId={server.id} type="normal" />
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-muted-foreground">
|
|
||||||
{server.description || "-"}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right">
|
|
||||||
<div className="flex items-center justify-end gap-2">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => router.push(`/dashboard/servers/${server.id}/logs`)}
|
|
||||||
title="View Logs"
|
|
||||||
>
|
>
|
||||||
<FileText className="h-4 w-4" />
|
<ServerTable
|
||||||
</Button>
|
type="normal"
|
||||||
<Button
|
servers={normalServers}
|
||||||
variant="ghost"
|
onEdit={(id) => router.push(`/dashboard/servers/edit/${id}`)}
|
||||||
size="icon"
|
onDelete={(id) => setDeleteTarget({ id, type: "normal" })}
|
||||||
onClick={() => router.push(`/dashboard/servers/edit/${server.id}`)}
|
/>
|
||||||
title="Edit Server"
|
</ResourceSection>
|
||||||
>
|
|
||||||
<Pencil className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => setDeleteTarget({ id: server.id, type: "normal" })}
|
|
||||||
title="Delete"
|
|
||||||
>
|
|
||||||
<Trash2 className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
<ResourceSection
|
||||||
<CardHeader>
|
title="Edge Proxies"
|
||||||
<div className="flex items-center gap-2">
|
description="Manage your Velocity and BungeeCord proxy servers"
|
||||||
<Globe className="h-5 w-5" />
|
icon={<Globe className="size-5 text-primary" />}
|
||||||
<CardTitle>Reverse Proxy Servers</CardTitle>
|
count={reverseProxies.length}
|
||||||
</div>
|
emptyIcon={<Globe className="size-6" />}
|
||||||
<CardDescription>Manage your Velocity and BungeeCord proxy servers</CardDescription>
|
emptyTitle="No edge proxies"
|
||||||
</CardHeader>
|
emptyDescription="Proxy workloads will appear here."
|
||||||
<CardContent>
|
|
||||||
{reverseProxies.length === 0 ? (
|
|
||||||
<div className="flex items-center justify-center h-32 border-2 border-dashed rounded-lg">
|
|
||||||
<p className="text-muted-foreground">No reverse proxies created yet</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="overflow-x-auto">
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>ID</TableHead>
|
|
||||||
<TableHead>Status</TableHead>
|
|
||||||
<TableHead>Type</TableHead>
|
|
||||||
<TableHead>External</TableHead>
|
|
||||||
<TableHead>Listen Port</TableHead>
|
|
||||||
<TableHead>Memory (MB)</TableHead>
|
|
||||||
<TableHead>Network</TableHead>
|
|
||||||
<TableHead>Description</TableHead>
|
|
||||||
<TableHead className="text-right">Actions</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{reverseProxies.map((proxy) => (
|
|
||||||
<TableRow key={proxy.id}>
|
|
||||||
<TableCell className="font-medium">{proxy.id}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<ServerStatusCell serverId={proxy.id} type="proxy" />
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Badge variant="outline">{proxy.type}</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{proxy.external_address}:{proxy.external_port}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>{proxy.listen_port}</TableCell>
|
|
||||||
<TableCell>{proxy.memory}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<ConnectionInfoCell serverId={proxy.id} type="proxy" />
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-muted-foreground">
|
|
||||||
{proxy.description || "-"}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right">
|
|
||||||
<div className="flex items-center justify-end gap-2">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => router.push(`/dashboard/servers/${proxy.id}/logs`)}
|
|
||||||
title="View Logs"
|
|
||||||
>
|
>
|
||||||
<FileText className="h-4 w-4" />
|
<ServerTable
|
||||||
</Button>
|
type="proxy"
|
||||||
<Button
|
servers={reverseProxies}
|
||||||
variant="ghost"
|
onEdit={(id) => router.push(`/dashboard/servers/edit/${id}`)}
|
||||||
size="icon"
|
onDelete={(id) => setDeleteTarget({ id, type: "proxy" })}
|
||||||
onClick={() => router.push(`/dashboard/servers/edit/${proxy.id}`)}
|
/>
|
||||||
title="Edit Server"
|
</ResourceSection>
|
||||||
>
|
</>
|
||||||
<Pencil className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => setDeleteTarget({ id: proxy.id, type: "proxy" })}
|
|
||||||
title="Delete"
|
|
||||||
>
|
|
||||||
<Trash2 className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Dialog open={!!deleteTarget} onOpenChange={() => setDeleteTarget(null)}>
|
<ConfirmDialog
|
||||||
<DialogContent>
|
open={!!deleteTarget}
|
||||||
<DialogHeader>
|
title="Delete Server"
|
||||||
<DialogTitle>Delete Server</DialogTitle>
|
description={
|
||||||
<DialogDescription>
|
<>
|
||||||
Are you sure you want to delete this{" "}
|
Are you sure you want to delete this{" "}
|
||||||
{deleteTarget?.type === "normal" ? "server" : "reverse proxy"}? This action cannot be
|
{deleteTarget?.type === "normal" ? "server" : "reverse proxy"}? This action cannot be
|
||||||
undone.
|
undone.
|
||||||
</DialogDescription>
|
</>
|
||||||
</DialogHeader>
|
}
|
||||||
<DialogFooter>
|
confirmLabel="Delete"
|
||||||
<Button variant="outline" onClick={() => setDeleteTarget(null)}>
|
onConfirm={handleDelete}
|
||||||
Cancel
|
onOpenChange={(open) => !open && setDeleteTarget(null)}
|
||||||
</Button>
|
/>
|
||||||
<Button variant="destructive" onClick={handleDelete}>
|
</PageShell>
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Info } from "lucide-react";
|
||||||
|
import type * as React from "react";
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
|
import { cn } from "@/lib/cn";
|
||||||
|
|
||||||
|
export function HelpTooltip({ content }: { content: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<button type="button" className="text-muted-foreground hover:text-foreground">
|
||||||
|
<Info className="size-4" />
|
||||||
|
<span className="sr-only">More information</span>
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="max-w-64">{content}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FormNotice({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn("rounded-sm border border-dashed p-4 text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
+59
-1280
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,106 @@
|
|||||||
|
import { Plus, Trash2 } from "lucide-react";
|
||||||
|
import { HelpTooltip } from "@/components/form-controls";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { TabsContent } from "@/components/ui/tabs";
|
||||||
|
import { Field } from "./fields";
|
||||||
|
import type { ServerFormPanelProps } from "./types";
|
||||||
|
|
||||||
|
interface AdvancedPanelProps extends ServerFormPanelProps {
|
||||||
|
addEnvVar: () => void;
|
||||||
|
removeEnvVar: (index: number) => void;
|
||||||
|
updateEnvVar: (index: number, field: "key" | "value", value: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AdvancedPanel({
|
||||||
|
formData,
|
||||||
|
updateField,
|
||||||
|
addEnvVar,
|
||||||
|
removeEnvVar,
|
||||||
|
updateEnvVar,
|
||||||
|
}: AdvancedPanelProps) {
|
||||||
|
return (
|
||||||
|
<TabsContent value="advanced" className="space-y-4 mt-4">
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
<Field id="timezone" label="Timezone" tooltip="Timezone for server logs and scheduling (e.g., America/New_York)">
|
||||||
|
<Input
|
||||||
|
id="timezone"
|
||||||
|
value={formData.timezone}
|
||||||
|
onChange={(event) => updateField("timezone", event.target.value)}
|
||||||
|
placeholder="UTC"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field id="uid" label="User ID (UID)" tooltip="Linux user ID for the Minecraft process">
|
||||||
|
<Input
|
||||||
|
id="uid"
|
||||||
|
value={formData.uid}
|
||||||
|
onChange={(event) => updateField("uid", event.target.value)}
|
||||||
|
placeholder="1000"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field id="gid" label="Group ID (GID)" tooltip="Linux group ID for the Minecraft process">
|
||||||
|
<Input
|
||||||
|
id="gid"
|
||||||
|
value={formData.gid}
|
||||||
|
onChange={(event) => updateField("gid", event.target.value)}
|
||||||
|
placeholder="1000"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
<Field
|
||||||
|
id="stopDuration"
|
||||||
|
label="Graceful Shutdown Timeout (seconds)"
|
||||||
|
tooltip="How long to wait for graceful shutdown before forcing stop"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
id="stopDuration"
|
||||||
|
type="number"
|
||||||
|
value={formData.stopDuration}
|
||||||
|
onChange={(event) => updateField("stopDuration", event.target.value)}
|
||||||
|
min="1"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label>
|
||||||
|
Custom Environment Variables
|
||||||
|
<HelpTooltip content="Additional environment variables to pass to the container" />
|
||||||
|
</Label>
|
||||||
|
<Button type="button" variant="outline" size="sm" onClick={addEnvVar}>
|
||||||
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
|
Add Variable
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{formData.envVars.length > 0 && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{formData.envVars.map((envVar, index) => (
|
||||||
|
<div key={envVar.id} className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
placeholder="KEY"
|
||||||
|
value={envVar.key}
|
||||||
|
onChange={(event) => updateEnvVar(index, "key", event.target.value)}
|
||||||
|
className="flex-1 font-mono"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="value"
|
||||||
|
value={envVar.value}
|
||||||
|
onChange={(event) => updateEnvVar(index, "value", event.target.value)}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => removeEnvVar(index)}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
import { FormNotice } from "@/components/form-controls";
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionContent,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionTrigger,
|
||||||
|
} from "@/components/ui/accordion";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { TabsContent } from "@/components/ui/tabs";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { CheckboxField, Field } from "./fields";
|
||||||
|
import type { ServerFormPanelProps } from "./types";
|
||||||
|
|
||||||
|
export function AutomationPanel({ formData, updateField }: ServerFormPanelProps) {
|
||||||
|
return (
|
||||||
|
<TabsContent value="automation" className="space-y-4 mt-4">
|
||||||
|
{formData.type === "CUSTOM" && (
|
||||||
|
<FormNotice>
|
||||||
|
Automation hooks depend on container scripts; custom jars may ignore them.
|
||||||
|
</FormNotice>
|
||||||
|
)}
|
||||||
|
<Accordion type="multiple" className="w-full">
|
||||||
|
<AccordionItem value="rcon">
|
||||||
|
<AccordionTrigger>RCON Configuration</AccordionTrigger>
|
||||||
|
<AccordionContent className="space-y-4 pt-4">
|
||||||
|
<CheckboxField
|
||||||
|
id="enableRcon"
|
||||||
|
checked={formData.enableRcon}
|
||||||
|
onCheckedChange={(value) => updateField("enableRcon", value)}
|
||||||
|
tooltip="Remote console for server management. Enabled by default for graceful shutdown."
|
||||||
|
>
|
||||||
|
Enable RCON
|
||||||
|
</CheckboxField>
|
||||||
|
{formData.enableRcon && (
|
||||||
|
<>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<Field id="rconPassword" label="RCON Password">
|
||||||
|
<Input
|
||||||
|
id="rconPassword"
|
||||||
|
type="password"
|
||||||
|
value={formData.rconPassword || ""}
|
||||||
|
onChange={(event) => updateField("rconPassword", event.target.value)}
|
||||||
|
placeholder="Auto-generated if empty"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field id="rconPort" label="RCON Port">
|
||||||
|
<Input
|
||||||
|
id="rconPort"
|
||||||
|
type="number"
|
||||||
|
value={formData.rconPort}
|
||||||
|
onChange={(event) => updateField("rconPort", event.target.value)}
|
||||||
|
min="1"
|
||||||
|
max="65535"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
<Field
|
||||||
|
id="rconCmdsStartup"
|
||||||
|
label="RCON Commands on Startup"
|
||||||
|
tooltip="Semicolon-separated commands to run when server starts"
|
||||||
|
>
|
||||||
|
<Textarea
|
||||||
|
id="rconCmdsStartup"
|
||||||
|
value={formData.rconCmdsStartup || ""}
|
||||||
|
onChange={(event) => updateField("rconCmdsStartup", event.target.value)}
|
||||||
|
placeholder="say Server starting...;gamerule doDaylightCycle false"
|
||||||
|
rows={2}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field id="rconCmdsOnConnect" label="Commands on Player Connect">
|
||||||
|
<Textarea
|
||||||
|
id="rconCmdsOnConnect"
|
||||||
|
value={formData.rconCmdsOnConnect || ""}
|
||||||
|
onChange={(event) => updateField("rconCmdsOnConnect", event.target.value)}
|
||||||
|
placeholder="tell {{player}} Welcome to the server!"
|
||||||
|
rows={2}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
|
||||||
|
<AccordionItem value="query">
|
||||||
|
<AccordionTrigger>Query Protocol</AccordionTrigger>
|
||||||
|
<AccordionContent className="space-y-4 pt-4">
|
||||||
|
<CheckboxField
|
||||||
|
id="enableQuery"
|
||||||
|
checked={formData.enableQuery}
|
||||||
|
onCheckedChange={(value) => updateField("enableQuery", value)}
|
||||||
|
tooltip="Allows external tools to query server status and player list"
|
||||||
|
>
|
||||||
|
Enable Query Protocol
|
||||||
|
</CheckboxField>
|
||||||
|
{formData.enableQuery && (
|
||||||
|
<Field id="queryPort" label="Query Port">
|
||||||
|
<Input
|
||||||
|
id="queryPort"
|
||||||
|
type="number"
|
||||||
|
value={formData.queryPort}
|
||||||
|
onChange={(event) => updateField("queryPort", event.target.value)}
|
||||||
|
min="1"
|
||||||
|
max="65535"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
|
||||||
|
<AccordionItem value="autopause">
|
||||||
|
<AccordionTrigger>Auto-Pause</AccordionTrigger>
|
||||||
|
<AccordionContent className="space-y-4 pt-4">
|
||||||
|
<CheckboxField
|
||||||
|
id="enableAutopause"
|
||||||
|
checked={formData.enableAutopause}
|
||||||
|
onCheckedChange={(value) => updateField("enableAutopause", value)}
|
||||||
|
tooltip="Automatically pauses the server when no players are online to save resources"
|
||||||
|
>
|
||||||
|
Enable Auto-Pause
|
||||||
|
</CheckboxField>
|
||||||
|
{formData.enableAutopause && (
|
||||||
|
<>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<Field id="autopauseTimeoutEst" label="Timeout After Disconnect (seconds)">
|
||||||
|
<Input
|
||||||
|
id="autopauseTimeoutEst"
|
||||||
|
type="number"
|
||||||
|
value={formData.autopauseTimeoutEst}
|
||||||
|
onChange={(event) => updateField("autopauseTimeoutEst", event.target.value)}
|
||||||
|
min="0"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field id="autopauseTimeoutInit" label="Timeout After Startup (seconds)">
|
||||||
|
<Input
|
||||||
|
id="autopauseTimeoutInit"
|
||||||
|
type="number"
|
||||||
|
value={formData.autopauseTimeoutInit}
|
||||||
|
onChange={(event) => updateField("autopauseTimeoutInit", event.target.value)}
|
||||||
|
min="0"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
<Field id="autopauseKnockInterface" label="Network Interface">
|
||||||
|
<Input
|
||||||
|
id="autopauseKnockInterface"
|
||||||
|
value={formData.autopauseKnockInterface}
|
||||||
|
onChange={(event) => updateField("autopauseKnockInterface", event.target.value)}
|
||||||
|
placeholder="eth0"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
|
||||||
|
<AccordionItem value="autostop">
|
||||||
|
<AccordionTrigger>Auto-Stop</AccordionTrigger>
|
||||||
|
<AccordionContent className="space-y-4 pt-4">
|
||||||
|
<CheckboxField
|
||||||
|
id="enableAutostop"
|
||||||
|
checked={formData.enableAutostop}
|
||||||
|
onCheckedChange={(value) => updateField("enableAutostop", value)}
|
||||||
|
tooltip="Automatically stops the server when no players are online"
|
||||||
|
>
|
||||||
|
Enable Auto-Stop
|
||||||
|
</CheckboxField>
|
||||||
|
{formData.enableAutostop && (
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<Field id="autostopTimeoutEst" label="Timeout After Player Departure (seconds)">
|
||||||
|
<Input
|
||||||
|
id="autostopTimeoutEst"
|
||||||
|
type="number"
|
||||||
|
value={formData.autostopTimeoutEst}
|
||||||
|
onChange={(event) => updateField("autostopTimeoutEst", event.target.value)}
|
||||||
|
min="0"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field id="autostopTimeoutInit" label="Timeout After Launch (seconds)">
|
||||||
|
<Input
|
||||||
|
id="autostopTimeoutInit"
|
||||||
|
type="number"
|
||||||
|
value={formData.autostopTimeoutInit}
|
||||||
|
onChange={(event) => updateField("autostopTimeoutInit", event.target.value)}
|
||||||
|
min="0"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
</TabsContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionContent,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionTrigger,
|
||||||
|
} from "@/components/ui/accordion";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { TabsContent } from "@/components/ui/tabs";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { CheckboxField, Field } from "./fields";
|
||||||
|
import type { ServerFormPanelProps } from "./types";
|
||||||
|
|
||||||
|
export function BasicPanel({ formData, updateField }: ServerFormPanelProps) {
|
||||||
|
return (
|
||||||
|
<TabsContent value="basic" className="space-y-4 mt-4">
|
||||||
|
<Field id="id" label="Server ID *">
|
||||||
|
<Input
|
||||||
|
id="id"
|
||||||
|
value={formData.id}
|
||||||
|
onChange={(event) => updateField("id", event.target.value)}
|
||||||
|
placeholder="my-server"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field id="description" label="Description">
|
||||||
|
<Textarea
|
||||||
|
id="description"
|
||||||
|
value={formData.description}
|
||||||
|
onChange={(event) => updateField("description", event.target.value)}
|
||||||
|
placeholder="A brief description of this server"
|
||||||
|
rows={2}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-4 gap-4">
|
||||||
|
<Field
|
||||||
|
id="memoryRequest"
|
||||||
|
label="Memory Request (MB) *"
|
||||||
|
tooltip="Kubernetes guaranteed memory. Java heap sizes from the limit."
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
id="memoryRequest"
|
||||||
|
type="number"
|
||||||
|
value={formData.memoryRequest}
|
||||||
|
onChange={(event) => updateField("memoryRequest", event.target.value)}
|
||||||
|
min="256"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field
|
||||||
|
id="memoryLimit"
|
||||||
|
label="Memory Limit (MB) *"
|
||||||
|
tooltip="Kubernetes max memory. Java heap scales from this."
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
id="memoryLimit"
|
||||||
|
type="number"
|
||||||
|
value={formData.memoryLimit}
|
||||||
|
onChange={(event) => updateField("memoryLimit", event.target.value)}
|
||||||
|
min="256"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field
|
||||||
|
id="cpuRequest"
|
||||||
|
label="CPU Request *"
|
||||||
|
tooltip="Guaranteed CPU allocation (e.g., 500m = 0.5 cores)"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
id="cpuRequest"
|
||||||
|
value={formData.cpuRequest}
|
||||||
|
onChange={(event) => updateField("cpuRequest", event.target.value)}
|
||||||
|
placeholder="500m"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field id="cpuLimit" label="CPU Limit *" tooltip="Maximum CPU the server can use">
|
||||||
|
<Input
|
||||||
|
id="cpuLimit"
|
||||||
|
value={formData.cpuLimit}
|
||||||
|
onChange={(event) => updateField("cpuLimit", event.target.value)}
|
||||||
|
placeholder="2"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Accordion type="single" collapsible className="w-full">
|
||||||
|
<AccordionItem value="advanced-memory">
|
||||||
|
<AccordionTrigger>Java Heap Overrides</AccordionTrigger>
|
||||||
|
<AccordionContent className="space-y-2 text-sm text-muted-foreground">
|
||||||
|
Heap sizes are derived from the memory limit. Override only if needed.
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<CheckboxField
|
||||||
|
id="eula"
|
||||||
|
checked={formData.eula}
|
||||||
|
onCheckedChange={(checked) => updateField("eula", checked)}
|
||||||
|
labelClassName="cursor-pointer"
|
||||||
|
>
|
||||||
|
I accept the{" "}
|
||||||
|
<a
|
||||||
|
href="https://www.minecraft.net/eula"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-primary underline"
|
||||||
|
>
|
||||||
|
Minecraft EULA
|
||||||
|
</a>{" "}
|
||||||
|
*
|
||||||
|
</CheckboxField>
|
||||||
|
</TabsContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { HelpTooltip } from "@/components/form-controls";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
|
||||||
|
interface FieldProps {
|
||||||
|
id: string;
|
||||||
|
label: ReactNode;
|
||||||
|
tooltip?: string;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Field({ id, label, tooltip, children }: FieldProps) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={id}>
|
||||||
|
{label}
|
||||||
|
{tooltip && <HelpTooltip content={tooltip} />}
|
||||||
|
</Label>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CheckboxFieldProps {
|
||||||
|
id: string;
|
||||||
|
checked: boolean;
|
||||||
|
onCheckedChange: (checked: boolean) => void;
|
||||||
|
children: ReactNode;
|
||||||
|
tooltip?: string;
|
||||||
|
labelClassName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CheckboxField({
|
||||||
|
id,
|
||||||
|
checked,
|
||||||
|
onCheckedChange,
|
||||||
|
children,
|
||||||
|
tooltip,
|
||||||
|
labelClassName,
|
||||||
|
}: CheckboxFieldProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id={id}
|
||||||
|
checked={checked}
|
||||||
|
onCheckedChange={(value) => onCheckedChange(value as boolean)}
|
||||||
|
/>
|
||||||
|
<Label htmlFor={id} className={labelClassName}>
|
||||||
|
{children}
|
||||||
|
{tooltip && <HelpTooltip content={tooltip} />}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { FormNotice } from "@/components/form-controls";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { TabsContent } from "@/components/ui/tabs";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { CheckboxField, Field } from "./fields";
|
||||||
|
import type { ServerFormPanelProps } from "./types";
|
||||||
|
|
||||||
|
export function ModsPanel({ formData, updateField }: ServerFormPanelProps) {
|
||||||
|
return (
|
||||||
|
<TabsContent value="mods" className="space-y-4 mt-4">
|
||||||
|
{formData.type === "CUSTOM" && (
|
||||||
|
<FormNotice>Mods/plugins automation is intended for Vanilla/Paper workflows.</FormNotice>
|
||||||
|
)}
|
||||||
|
<Field id="plugins" label="Plugins" tooltip="Comma-separated list of plugin URLs or filenames">
|
||||||
|
<Textarea
|
||||||
|
id="plugins"
|
||||||
|
value={formData.plugins || ""}
|
||||||
|
onChange={(event) => updateField("plugins", event.target.value)}
|
||||||
|
placeholder="https://example.com/plugin1.jar,plugin2.jar"
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
For Paper/Spigot/Bukkit servers. URLs or local filenames.
|
||||||
|
</p>
|
||||||
|
</Field>
|
||||||
|
<Field
|
||||||
|
id="spigetResources"
|
||||||
|
label="Spiget Resource IDs"
|
||||||
|
tooltip="Comma-separated Spiget resource IDs to auto-download from SpigotMC"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
id="spigetResources"
|
||||||
|
value={formData.spigetResources || ""}
|
||||||
|
onChange={(event) => updateField("spigetResources", event.target.value)}
|
||||||
|
placeholder="1234,5678,9012"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<CheckboxField
|
||||||
|
id="removeOldPlugins"
|
||||||
|
checked={formData.removeOldPlugins}
|
||||||
|
onCheckedChange={(value) => updateField("removeOldPlugins", value)}
|
||||||
|
tooltip="Automatically remove old versions of plugins when updating"
|
||||||
|
>
|
||||||
|
Remove Old Plugins
|
||||||
|
</CheckboxField>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { FormNotice } from "@/components/form-controls";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { TabsContent } from "@/components/ui/tabs";
|
||||||
|
import { Field } from "./fields";
|
||||||
|
import type { ServerFormPanelProps, ServiceType } from "./types";
|
||||||
|
|
||||||
|
export function NetworkPanel({ formData, updateField }: ServerFormPanelProps) {
|
||||||
|
return (
|
||||||
|
<TabsContent value="network" className="space-y-4 mt-4">
|
||||||
|
{formData.type === "CUSTOM" && (
|
||||||
|
<FormNotice>
|
||||||
|
Network settings apply at the container level and still work with custom jars.
|
||||||
|
</FormNotice>
|
||||||
|
)}
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<Field id="listenPort" label="Listen Port">
|
||||||
|
<Input
|
||||||
|
id="listenPort"
|
||||||
|
type="number"
|
||||||
|
value={formData.listenPort}
|
||||||
|
onChange={(event) => updateField("listenPort", event.target.value)}
|
||||||
|
min="1"
|
||||||
|
max="65535"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field id="serviceType" label="Service Type" tooltip="How the server is exposed in Kubernetes">
|
||||||
|
<Select
|
||||||
|
value={formData.serviceType}
|
||||||
|
onValueChange={(value) => updateField("serviceType", value as ServiceType)}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="serviceType"><SelectValue /></SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="CLUSTER_IP">ClusterIP (Internal Only)</SelectItem>
|
||||||
|
<SelectItem value="NODE_PORT">NodePort (External Access)</SelectItem>
|
||||||
|
<SelectItem value="LOAD_BALANCER">LoadBalancer (Cloud/MetalLB)</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
{formData.serviceType === "NODE_PORT" && (
|
||||||
|
<Field
|
||||||
|
id="nodePort"
|
||||||
|
label="Node Port"
|
||||||
|
tooltip="Specific port on the node (30000-32767). Leave empty for auto-assignment."
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
id="nodePort"
|
||||||
|
type="number"
|
||||||
|
value={formData.nodePort || ""}
|
||||||
|
onChange={(event) => updateField("nodePort", event.target.value)}
|
||||||
|
placeholder="30000-32767"
|
||||||
|
min="30000"
|
||||||
|
max="32767"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { FormNotice } from "@/components/form-controls";
|
||||||
|
import { TabsContent } from "@/components/ui/tabs";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { CheckboxField, Field } from "./fields";
|
||||||
|
import type { ServerFormPanelProps } from "./types";
|
||||||
|
|
||||||
|
export function PerformancePanel({ formData, updateField }: ServerFormPanelProps) {
|
||||||
|
return (
|
||||||
|
<TabsContent value="performance" className="space-y-4 mt-4">
|
||||||
|
{formData.type === "CUSTOM" && (
|
||||||
|
<FormNotice>
|
||||||
|
Custom jars may not honor built-in tuning flags. Use custom env vars if required.
|
||||||
|
</FormNotice>
|
||||||
|
)}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<CheckboxField
|
||||||
|
id="useAikarFlags"
|
||||||
|
checked={formData.useAikarFlags}
|
||||||
|
onCheckedChange={(value) => updateField("useAikarFlags", value)}
|
||||||
|
tooltip="Optimized G1GC garbage collection flags. Recommended for servers with 1GB+ RAM."
|
||||||
|
>
|
||||||
|
Use Aikar's Flags
|
||||||
|
</CheckboxField>
|
||||||
|
<CheckboxField
|
||||||
|
id="useMeowiceFlags"
|
||||||
|
checked={formData.useMeowiceFlags}
|
||||||
|
onCheckedChange={(value) => updateField("useMeowiceFlags", value)}
|
||||||
|
tooltip="Modern optimization flags for Java 17+. Based on Aikar's flags with improvements."
|
||||||
|
>
|
||||||
|
Use Meowice Flags
|
||||||
|
</CheckboxField>
|
||||||
|
<CheckboxField
|
||||||
|
id="enableJmx"
|
||||||
|
checked={formData.enableJmx}
|
||||||
|
onCheckedChange={(value) => updateField("enableJmx", value)}
|
||||||
|
tooltip="Enables remote JMX monitoring for advanced profiling"
|
||||||
|
>
|
||||||
|
Enable JMX
|
||||||
|
</CheckboxField>
|
||||||
|
</div>
|
||||||
|
<Field
|
||||||
|
id="jvmOpts"
|
||||||
|
label="Custom JVM Options"
|
||||||
|
tooltip="Space-separated JVM arguments (e.g., -Xms2G -Xmx4G)"
|
||||||
|
>
|
||||||
|
<Textarea
|
||||||
|
id="jvmOpts"
|
||||||
|
value={formData.jvmOpts || ""}
|
||||||
|
onChange={(event) => updateField("jvmOpts", event.target.value)}
|
||||||
|
placeholder="-XX:+UseG1GC -XX:MaxGCPauseMillis=200"
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field id="jvmXxOpts" label="JVM -XX Options" tooltip="Space-separated -XX JVM flags for advanced tuning">
|
||||||
|
<Textarea
|
||||||
|
id="jvmXxOpts"
|
||||||
|
value={formData.jvmXxOpts || ""}
|
||||||
|
onChange={(event) => updateField("jvmXxOpts", event.target.value)}
|
||||||
|
placeholder="+UnlockExperimentalVMOptions +UseZGC"
|
||||||
|
rows={2}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field id="jvmDdOpts" label="JVM -D System Properties" tooltip="Comma-separated key=value pairs for system properties">
|
||||||
|
<Textarea
|
||||||
|
id="jvmDdOpts"
|
||||||
|
value={formData.jvmDdOpts || ""}
|
||||||
|
onChange={(event) => updateField("jvmDdOpts", event.target.value)}
|
||||||
|
placeholder="property1=value1,property2=value2"
|
||||||
|
rows={2}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</TabsContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { FormNotice } from "@/components/form-controls";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { TabsContent } from "@/components/ui/tabs";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { CheckboxField, Field } from "./fields";
|
||||||
|
import type { ServerFormPanelProps } from "./types";
|
||||||
|
|
||||||
|
export function PlayersPanel({ formData, updateField }: ServerFormPanelProps) {
|
||||||
|
return (
|
||||||
|
<TabsContent value="players" className="space-y-4 mt-4">
|
||||||
|
{formData.type === "CUSTOM" && (
|
||||||
|
<FormNotice>Some custom jars do not support managed whitelist/ops settings.</FormNotice>
|
||||||
|
)}
|
||||||
|
<CheckboxField
|
||||||
|
id="enableWhitelist"
|
||||||
|
checked={formData.enableWhitelist}
|
||||||
|
onCheckedChange={(value) => updateField("enableWhitelist", value)}
|
||||||
|
tooltip="Only whitelisted players can join the server"
|
||||||
|
>
|
||||||
|
Enable Whitelist
|
||||||
|
</CheckboxField>
|
||||||
|
{formData.enableWhitelist && (
|
||||||
|
<>
|
||||||
|
<Field
|
||||||
|
id="whitelist"
|
||||||
|
label="Whitelisted Players"
|
||||||
|
tooltip="Comma-separated list of usernames or UUIDs"
|
||||||
|
>
|
||||||
|
<Textarea
|
||||||
|
id="whitelist"
|
||||||
|
value={formData.whitelist || ""}
|
||||||
|
onChange={(event) => updateField("whitelist", event.target.value)}
|
||||||
|
placeholder="Player1,Player2,UUID-here"
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field
|
||||||
|
id="whitelistFile"
|
||||||
|
label="Whitelist File URL"
|
||||||
|
tooltip="URL or path to a whitelist.json file. Overrides the whitelist field."
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
id="whitelistFile"
|
||||||
|
value={formData.whitelistFile || ""}
|
||||||
|
onChange={(event) => updateField("whitelistFile", event.target.value)}
|
||||||
|
placeholder="https://example.com/whitelist.json"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Field
|
||||||
|
id="ops"
|
||||||
|
label="Operators (Ops)"
|
||||||
|
tooltip="Comma-separated list of usernames or UUIDs with full server permissions"
|
||||||
|
>
|
||||||
|
<Textarea
|
||||||
|
id="ops"
|
||||||
|
value={formData.ops || ""}
|
||||||
|
onChange={(event) => updateField("ops", event.target.value)}
|
||||||
|
placeholder="Admin1,Admin2"
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field id="opsFile" label="Ops File URL">
|
||||||
|
<Input
|
||||||
|
id="opsFile"
|
||||||
|
value={formData.opsFile || ""}
|
||||||
|
onChange={(event) => updateField("opsFile", event.target.value)}
|
||||||
|
placeholder="https://example.com/ops.json"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</TabsContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { FormNotice } from "@/components/form-controls";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { TabsContent } from "@/components/ui/tabs";
|
||||||
|
import { CheckboxField, Field } from "./fields";
|
||||||
|
import type { ServerFormPanelProps } from "./types";
|
||||||
|
|
||||||
|
export function ResourcesPanel({ formData, updateField }: ServerFormPanelProps) {
|
||||||
|
return (
|
||||||
|
<TabsContent value="resources" className="space-y-4 mt-4">
|
||||||
|
{formData.type === "CUSTOM" && (
|
||||||
|
<FormNotice>Resource pack settings may not apply to custom jars.</FormNotice>
|
||||||
|
)}
|
||||||
|
<Field id="resourcePack" label="Resource Pack URL" tooltip="URL or path to a resource pack ZIP file">
|
||||||
|
<Input
|
||||||
|
id="resourcePack"
|
||||||
|
value={formData.resourcePack || ""}
|
||||||
|
onChange={(event) => updateField("resourcePack", event.target.value)}
|
||||||
|
placeholder="https://example.com/resourcepack.zip"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field id="resourcePackSha1" label="Resource Pack SHA1" tooltip="SHA1 checksum of the resource pack for verification">
|
||||||
|
<Input
|
||||||
|
id="resourcePackSha1"
|
||||||
|
value={formData.resourcePackSha1 || ""}
|
||||||
|
onChange={(event) => updateField("resourcePackSha1", event.target.value)}
|
||||||
|
placeholder="a1b2c3d4e5f6..."
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<CheckboxField
|
||||||
|
id="resourcePackEnforce"
|
||||||
|
checked={formData.resourcePackEnforce}
|
||||||
|
onCheckedChange={(value) => updateField("resourcePackEnforce", value)}
|
||||||
|
tooltip="Require players to accept the resource pack to join"
|
||||||
|
>
|
||||||
|
Enforce Resource Pack
|
||||||
|
</CheckboxField>
|
||||||
|
<Field id="serverIcon" label="Server Icon URL" tooltip="URL or path to a server icon image (PNG, 64x64 recommended)">
|
||||||
|
<Input
|
||||||
|
id="serverIcon"
|
||||||
|
value={formData.serverIcon || ""}
|
||||||
|
onChange={(event) => updateField("serverIcon", event.target.value)}
|
||||||
|
placeholder="https://example.com/icon.png"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</TabsContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
import { FormNotice } from "@/components/form-controls";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { TabsContent } from "@/components/ui/tabs";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { CheckboxField, Field } from "./fields";
|
||||||
|
import type { ServerFormData, ServerFormPanelProps, ServerType } from "./types";
|
||||||
|
|
||||||
|
const toMode = (value: string): ServerFormData["mode"] => {
|
||||||
|
if (value === "creative" || value === "adventure" || value === "spectator") return value;
|
||||||
|
return "survival";
|
||||||
|
};
|
||||||
|
|
||||||
|
const toDifficulty = (value: string): ServerFormData["difficulty"] => {
|
||||||
|
if (value === "peaceful" || value === "normal" || value === "hard") return value;
|
||||||
|
return "easy";
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ServerPanel({ formData, updateField }: ServerFormPanelProps) {
|
||||||
|
return (
|
||||||
|
<TabsContent value="server" className="space-y-4 mt-4">
|
||||||
|
{formData.type === "CUSTOM" && (
|
||||||
|
<FormNotice>
|
||||||
|
Custom jars may ignore built-in server settings. Configure extra values via custom
|
||||||
|
environment variables if needed.
|
||||||
|
</FormNotice>
|
||||||
|
)}
|
||||||
|
<Field
|
||||||
|
id="type"
|
||||||
|
label="Server Type *"
|
||||||
|
tooltip="Choose the server software (Vanilla, Paper, or Custom Jar)."
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
value={formData.type}
|
||||||
|
onValueChange={(value) => updateField("type", value as ServerType)}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="type">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="VANILLA">Vanilla (Official Mojang)</SelectItem>
|
||||||
|
<SelectItem value="PAPER">Paper (Recommended - High Performance)</SelectItem>
|
||||||
|
<SelectItem value="CUSTOM">Custom Jar</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
{formData.type !== "CUSTOM" && (
|
||||||
|
<Field
|
||||||
|
id="version"
|
||||||
|
label="Minecraft Version"
|
||||||
|
tooltip="Use LATEST for newest stable, SNAPSHOT for snapshots, or specific version like 1.20.4"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
id="version"
|
||||||
|
value={formData.version || ""}
|
||||||
|
onChange={(event) => updateField("version", event.target.value)}
|
||||||
|
placeholder="LATEST"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{formData.type === "PAPER" && (
|
||||||
|
<Field id="paperBuild" label="Paper Build Number">
|
||||||
|
<Input
|
||||||
|
id="paperBuild"
|
||||||
|
value={formData.paperBuild || ""}
|
||||||
|
onChange={(event) => updateField("paperBuild", event.target.value)}
|
||||||
|
placeholder="Leave empty for latest"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{formData.type === "CUSTOM" && (
|
||||||
|
<Field id="customJarUrl" label="Custom Server Jar URL/Path">
|
||||||
|
<Input
|
||||||
|
id="customJarUrl"
|
||||||
|
value={formData.customJarUrl || ""}
|
||||||
|
onChange={(event) => updateField("customJarUrl", event.target.value)}
|
||||||
|
placeholder="https://example.com/server.jar"
|
||||||
|
/>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Sets CUSTOM_SERVER for the itzg/minecraft-server image.
|
||||||
|
</p>
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Field id="motd" label="Message of the Day (MOTD)">
|
||||||
|
<Textarea
|
||||||
|
id="motd"
|
||||||
|
value={formData.motd || ""}
|
||||||
|
onChange={(event) => updateField("motd", event.target.value)}
|
||||||
|
placeholder="A Minecraft Server"
|
||||||
|
rows={2}
|
||||||
|
/>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Supports formatting codes like §l for bold, §c for red
|
||||||
|
</p>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<Field id="mode" label="Game Mode">
|
||||||
|
<Select value={formData.mode} onValueChange={(value) => updateField("mode", toMode(value))}>
|
||||||
|
<SelectTrigger id="mode"><SelectValue /></SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="survival">Survival</SelectItem>
|
||||||
|
<SelectItem value="creative">Creative</SelectItem>
|
||||||
|
<SelectItem value="adventure">Adventure</SelectItem>
|
||||||
|
<SelectItem value="spectator">Spectator</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</Field>
|
||||||
|
<Field id="difficulty" label="Difficulty">
|
||||||
|
<Select
|
||||||
|
value={formData.difficulty}
|
||||||
|
onValueChange={(value) => updateField("difficulty", toDifficulty(value))}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="difficulty"><SelectValue /></SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="peaceful">Peaceful</SelectItem>
|
||||||
|
<SelectItem value="easy">Easy</SelectItem>
|
||||||
|
<SelectItem value="normal">Normal</SelectItem>
|
||||||
|
<SelectItem value="hard">Hard</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<Field id="maxPlayers" label="Max Players">
|
||||||
|
<Input
|
||||||
|
id="maxPlayers"
|
||||||
|
type="number"
|
||||||
|
value={formData.maxPlayers}
|
||||||
|
onChange={(event) => updateField("maxPlayers", event.target.value)}
|
||||||
|
min="1"
|
||||||
|
max="1000"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field id="viewDistance" label="View Distance (chunks)">
|
||||||
|
<Input
|
||||||
|
id="viewDistance"
|
||||||
|
type="number"
|
||||||
|
value={formData.viewDistance}
|
||||||
|
onChange={(event) => updateField("viewDistance", event.target.value)}
|
||||||
|
min="3"
|
||||||
|
max="32"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<CheckboxField id="pvp" checked={formData.pvp} onCheckedChange={(value) => updateField("pvp", value)}>
|
||||||
|
Enable PvP (Player vs Player)
|
||||||
|
</CheckboxField>
|
||||||
|
<CheckboxField id="onlineMode" checked={formData.onlineMode} onCheckedChange={(value) => updateField("onlineMode", value)}>
|
||||||
|
Online Mode (Requires authenticated Minecraft accounts)
|
||||||
|
</CheckboxField>
|
||||||
|
<CheckboxField id="allowFlight" checked={formData.allowFlight} onCheckedChange={(value) => updateField("allowFlight", value)}>
|
||||||
|
Allow Flight
|
||||||
|
</CheckboxField>
|
||||||
|
<CheckboxField id="enableCommandBlock" checked={formData.enableCommandBlock} onCheckedChange={(value) => updateField("enableCommandBlock", value)}>
|
||||||
|
Enable Command Blocks
|
||||||
|
</CheckboxField>
|
||||||
|
<CheckboxField id="hardcore" checked={formData.hardcore} onCheckedChange={(value) => updateField("hardcore", value)}>
|
||||||
|
Hardcore Mode (Permanent Death)
|
||||||
|
</CheckboxField>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import type { GameMode, ServiceType as PrismaServiceType, ServerDifficulty } from "@minikura/db";
|
||||||
|
|
||||||
|
export type ServerType = "VANILLA" | "PAPER" | "SPIGOT" | "PURPUR" | "FABRIC" | "CUSTOM";
|
||||||
|
export type ServiceType = PrismaServiceType;
|
||||||
|
export type Difficulty = ServerDifficulty;
|
||||||
|
export type Mode = GameMode;
|
||||||
|
|
||||||
|
export interface EnvVar {
|
||||||
|
id?: string;
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServerFormData {
|
||||||
|
id: string;
|
||||||
|
description: string;
|
||||||
|
memoryLimit: string;
|
||||||
|
memoryRequest: string;
|
||||||
|
cpuRequest: string;
|
||||||
|
cpuLimit: string;
|
||||||
|
type: ServerType;
|
||||||
|
version?: string;
|
||||||
|
customJarUrl?: string;
|
||||||
|
eula: boolean;
|
||||||
|
listenPort: string;
|
||||||
|
serviceType: ServiceType;
|
||||||
|
nodePort?: string;
|
||||||
|
motd?: string;
|
||||||
|
difficulty: "peaceful" | "easy" | "normal" | "hard";
|
||||||
|
mode: "survival" | "creative" | "adventure" | "spectator";
|
||||||
|
maxPlayers: string;
|
||||||
|
pvp: boolean;
|
||||||
|
onlineMode: boolean;
|
||||||
|
allowFlight: boolean;
|
||||||
|
enableCommandBlock: boolean;
|
||||||
|
spawnProtection: string;
|
||||||
|
viewDistance: string;
|
||||||
|
simulationDistance: string;
|
||||||
|
levelName: string;
|
||||||
|
levelSeed?: string;
|
||||||
|
levelType?: string;
|
||||||
|
generatorSettings?: string;
|
||||||
|
hardcore: boolean;
|
||||||
|
spawnAnimals: boolean;
|
||||||
|
spawnMonsters: boolean;
|
||||||
|
spawnNpcs: boolean;
|
||||||
|
enableWhitelist: boolean;
|
||||||
|
whitelist?: string;
|
||||||
|
whitelistFile?: string;
|
||||||
|
ops?: string;
|
||||||
|
opsFile?: string;
|
||||||
|
useAikarFlags: boolean;
|
||||||
|
useMeowiceFlags: boolean;
|
||||||
|
jvmOpts?: string;
|
||||||
|
jvmXxOpts?: string;
|
||||||
|
jvmDdOpts?: string;
|
||||||
|
resourcePack?: string;
|
||||||
|
resourcePackSha1?: string;
|
||||||
|
resourcePackEnforce: boolean;
|
||||||
|
enableRcon: boolean;
|
||||||
|
rconPassword?: string;
|
||||||
|
rconPort: string;
|
||||||
|
rconCmdsStartup?: string;
|
||||||
|
rconCmdsOnConnect?: string;
|
||||||
|
rconCmdsFirstConnect?: string;
|
||||||
|
rconCmdsOnDisconnect?: string;
|
||||||
|
rconCmdsLastDisconnect?: string;
|
||||||
|
enableQuery: boolean;
|
||||||
|
queryPort: string;
|
||||||
|
enableAutopause: boolean;
|
||||||
|
autopauseTimeoutEst: string;
|
||||||
|
autopauseTimeoutInit: string;
|
||||||
|
autopauseTimeoutKn: string;
|
||||||
|
autopausePeriod: string;
|
||||||
|
autopauseKnockInterface: string;
|
||||||
|
enableAutostop: boolean;
|
||||||
|
autostopTimeoutEst: string;
|
||||||
|
autostopTimeoutInit: string;
|
||||||
|
autostopPeriod: string;
|
||||||
|
plugins?: string;
|
||||||
|
removeOldPlugins: boolean;
|
||||||
|
spigetResources?: string;
|
||||||
|
paperBuild?: string;
|
||||||
|
timezone: string;
|
||||||
|
uid: string;
|
||||||
|
gid: string;
|
||||||
|
enableJmx: boolean;
|
||||||
|
stopDuration: string;
|
||||||
|
serverIcon?: string;
|
||||||
|
envVars: EnvVar[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UpdateServerField = <K extends keyof ServerFormData>(
|
||||||
|
key: K,
|
||||||
|
value: ServerFormData[K],
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
export interface ServerFormPanelProps {
|
||||||
|
formData: ServerFormData;
|
||||||
|
updateField: UpdateServerField;
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import { FormNotice } from "@/components/form-controls";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { TabsContent } from "@/components/ui/tabs";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { CheckboxField, Field } from "./fields";
|
||||||
|
import type { ServerFormPanelProps } from "./types";
|
||||||
|
|
||||||
|
export function WorldPanel({ formData, updateField }: ServerFormPanelProps) {
|
||||||
|
return (
|
||||||
|
<TabsContent value="world" className="space-y-4 mt-4">
|
||||||
|
{formData.type === "CUSTOM" && (
|
||||||
|
<FormNotice>
|
||||||
|
World options apply to standard server properties and may be ignored by custom jars.
|
||||||
|
</FormNotice>
|
||||||
|
)}
|
||||||
|
<Field id="levelName" label="World Name">
|
||||||
|
<Input
|
||||||
|
id="levelName"
|
||||||
|
value={formData.levelName}
|
||||||
|
onChange={(event) => updateField("levelName", event.target.value)}
|
||||||
|
placeholder="world"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field
|
||||||
|
id="levelSeed"
|
||||||
|
label="World Seed"
|
||||||
|
tooltip="Leave empty for random generation. Can be numeric or text."
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
id="levelSeed"
|
||||||
|
value={formData.levelSeed || ""}
|
||||||
|
onChange={(event) => updateField("levelSeed", event.target.value)}
|
||||||
|
placeholder="Leave empty for random"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field id="levelType" label="Level Type">
|
||||||
|
<Select
|
||||||
|
value={formData.levelType || "default"}
|
||||||
|
onValueChange={(value) => updateField("levelType", value === "default" ? "" : value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="levelType"><SelectValue placeholder="Default" /></SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="default">Default</SelectItem>
|
||||||
|
<SelectItem value="flat">Flat/Superflat</SelectItem>
|
||||||
|
<SelectItem value="largeBiomes">Large Biomes</SelectItem>
|
||||||
|
<SelectItem value="amplified">Amplified</SelectItem>
|
||||||
|
<SelectItem value="buffet">Buffet (Single Biome)</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</Field>
|
||||||
|
<Field
|
||||||
|
id="generatorSettings"
|
||||||
|
label="Generator Settings"
|
||||||
|
tooltip="JSON configuration for custom world generation. Advanced users only."
|
||||||
|
>
|
||||||
|
<Textarea
|
||||||
|
id="generatorSettings"
|
||||||
|
value={formData.generatorSettings || ""}
|
||||||
|
onChange={(event) => updateField("generatorSettings", event.target.value)}
|
||||||
|
placeholder={'{"structures": {...}, "layers": [...]}'}
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<Field
|
||||||
|
id="spawnProtection"
|
||||||
|
label="Spawn Protection (blocks)"
|
||||||
|
tooltip="Radius around spawn where only ops can build. Set to 0 to disable."
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
id="spawnProtection"
|
||||||
|
type="number"
|
||||||
|
value={formData.spawnProtection}
|
||||||
|
onChange={(event) => updateField("spawnProtection", event.target.value)}
|
||||||
|
min="0"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field
|
||||||
|
id="simulationDistance"
|
||||||
|
label="Simulation Distance"
|
||||||
|
tooltip="Distance in chunks where game logic runs (redstone, crops, etc.)"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
id="simulationDistance"
|
||||||
|
type="number"
|
||||||
|
value={formData.simulationDistance}
|
||||||
|
onChange={(event) => updateField("simulationDistance", event.target.value)}
|
||||||
|
min="3"
|
||||||
|
max="32"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<CheckboxField id="spawnAnimals" checked={formData.spawnAnimals} onCheckedChange={(value) => updateField("spawnAnimals", value)}>
|
||||||
|
Spawn Animals
|
||||||
|
</CheckboxField>
|
||||||
|
<CheckboxField id="spawnMonsters" checked={formData.spawnMonsters} onCheckedChange={(value) => updateField("spawnMonsters", value)}>
|
||||||
|
Spawn Monsters
|
||||||
|
</CheckboxField>
|
||||||
|
<CheckboxField id="spawnNpcs" checked={formData.spawnNpcs} onCheckedChange={(value) => updateField("spawnNpcs", value)}>
|
||||||
|
Spawn NPCs (Villagers)
|
||||||
|
</CheckboxField>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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} />;
|
||||||
|
}
|
||||||
@@ -9,8 +9,10 @@ export function useServerList() {
|
|||||||
const [normalServers, setNormalServers] = useState<NormalServer[]>([]);
|
const [normalServers, setNormalServers] = useState<NormalServer[]>([]);
|
||||||
const [reverseProxies, setReverseProxies] = useState<ReverseProxyServer[]>([]);
|
const [reverseProxies, setReverseProxies] = useState<ReverseProxyServer[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const fetchServers = useCallback(async () => {
|
const fetchServers = useCallback(async () => {
|
||||||
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const [normalRes, proxyRes] = await Promise.all([
|
const [normalRes, proxyRes] = await Promise.all([
|
||||||
api.api.servers.get(),
|
api.api.servers.get(),
|
||||||
@@ -23,7 +25,8 @@ export function useServerList() {
|
|||||||
if (proxyRes.data) {
|
if (proxyRes.data) {
|
||||||
setReverseProxies(proxyRes.data as unknown as ReverseProxyServer[]);
|
setReverseProxies(proxyRes.data as unknown as ReverseProxyServer[]);
|
||||||
}
|
}
|
||||||
} catch (_error) {
|
} catch (error) {
|
||||||
|
setError(error instanceof Error ? error.message : "Failed to load servers");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -49,6 +52,7 @@ export function useServerList() {
|
|||||||
normalServers,
|
normalServers,
|
||||||
reverseProxies,
|
reverseProxies,
|
||||||
loading,
|
loading,
|
||||||
|
error,
|
||||||
refresh: fetchServers,
|
refresh: fetchServers,
|
||||||
deleteServer,
|
deleteServer,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,368 @@
|
|||||||
|
import type { CreateServerRequest, NormalServer } from "@minikura/api";
|
||||||
|
import type { EnvVar, ServerFormData, ServerType, ServiceType } from "@/components/server-form";
|
||||||
|
|
||||||
|
type EnvironmentVariable = { key: string; value: string };
|
||||||
|
|
||||||
|
export type CommonServerRequestFields = Omit<CreateServerRequest, "id" | "type">;
|
||||||
|
|
||||||
|
const MANAGED_ENVIRONMENT_KEYS = new Set([
|
||||||
|
"EULA",
|
||||||
|
"TYPE",
|
||||||
|
"VERSION",
|
||||||
|
"CUSTOM_SERVER",
|
||||||
|
"MOTD",
|
||||||
|
"DIFFICULTY",
|
||||||
|
"MODE",
|
||||||
|
"MAX_PLAYERS",
|
||||||
|
"PVP",
|
||||||
|
"ONLINE_MODE",
|
||||||
|
"ALLOW_FLIGHT",
|
||||||
|
"ENABLE_COMMAND_BLOCK",
|
||||||
|
"SPAWN_PROTECTION",
|
||||||
|
"VIEW_DISTANCE",
|
||||||
|
"SIMULATION_DISTANCE",
|
||||||
|
"LEVEL",
|
||||||
|
"SEED",
|
||||||
|
"LEVEL_TYPE",
|
||||||
|
"GENERATOR_SETTINGS",
|
||||||
|
"HARDCORE",
|
||||||
|
"SPAWN_ANIMALS",
|
||||||
|
"SPAWN_MONSTERS",
|
||||||
|
"SPAWN_NPCS",
|
||||||
|
"ENABLE_WHITELIST",
|
||||||
|
"WHITELIST",
|
||||||
|
"WHITELIST_FILE",
|
||||||
|
"OPS",
|
||||||
|
"OPS_FILE",
|
||||||
|
"USE_AIKAR_FLAGS",
|
||||||
|
"USE_MEOWICE_FLAGS",
|
||||||
|
"JVM_OPTS",
|
||||||
|
"JVM_XX_OPTS",
|
||||||
|
"JVM_DD_OPTS",
|
||||||
|
"ENABLE_JMX",
|
||||||
|
"RESOURCE_PACK",
|
||||||
|
"RESOURCE_PACK_SHA1",
|
||||||
|
"RESOURCE_PACK_ENFORCE",
|
||||||
|
"ENABLE_RCON",
|
||||||
|
"RCON_PASSWORD",
|
||||||
|
"RCON_PORT",
|
||||||
|
"RCON_CMDS_STARTUP",
|
||||||
|
"RCON_CMDS_ON_CONNECT",
|
||||||
|
"RCON_CMDS_FIRST_CONNECT",
|
||||||
|
"RCON_CMDS_ON_DISCONNECT",
|
||||||
|
"RCON_CMDS_LAST_DISCONNECT",
|
||||||
|
"ENABLE_QUERY",
|
||||||
|
"QUERY_PORT",
|
||||||
|
"ENABLE_AUTOPAUSE",
|
||||||
|
"AUTOPAUSE_TIMEOUT_EST",
|
||||||
|
"AUTOPAUSE_TIMEOUT_INIT",
|
||||||
|
"AUTOPAUSE_TIMEOUT_KN",
|
||||||
|
"AUTOPAUSE_PERIOD",
|
||||||
|
"AUTOPAUSE_KNOCK_INTERFACE",
|
||||||
|
"ENABLE_AUTOSTOP",
|
||||||
|
"AUTOSTOP_TIMEOUT_EST",
|
||||||
|
"AUTOSTOP_TIMEOUT_INIT",
|
||||||
|
"AUTOSTOP_PERIOD",
|
||||||
|
"PLUGINS",
|
||||||
|
"REMOVE_OLD_PLUGINS",
|
||||||
|
"SPIGET_RESOURCES",
|
||||||
|
"PAPER_BUILD",
|
||||||
|
"TZ",
|
||||||
|
"UID",
|
||||||
|
"GID",
|
||||||
|
"STOP_DURATION",
|
||||||
|
"ICON",
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const normalizeDifficulty = (value?: string | null): ServerFormData["difficulty"] => {
|
||||||
|
const normalized = value?.toLowerCase();
|
||||||
|
if (
|
||||||
|
normalized === "peaceful" ||
|
||||||
|
normalized === "easy" ||
|
||||||
|
normalized === "normal" ||
|
||||||
|
normalized === "hard"
|
||||||
|
) {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
return "easy";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const normalizeMode = (value?: string | null): ServerFormData["mode"] => {
|
||||||
|
const normalized = value?.toLowerCase();
|
||||||
|
if (
|
||||||
|
normalized === "survival" ||
|
||||||
|
normalized === "creative" ||
|
||||||
|
normalized === "adventure" ||
|
||||||
|
normalized === "spectator"
|
||||||
|
) {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
return "survival";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const normalizeServiceType = (value?: string | null): ServiceType => {
|
||||||
|
if (value === "NODE_PORT" || value === "LOAD_BALANCER") {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return "CLUSTER_IP";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const normalizeServerType = (value?: string | null): ServerType => {
|
||||||
|
if (
|
||||||
|
value === "VANILLA" ||
|
||||||
|
value === "PAPER" ||
|
||||||
|
value === "SPIGOT" ||
|
||||||
|
value === "PURPUR" ||
|
||||||
|
value === "FABRIC" ||
|
||||||
|
value === "CUSTOM"
|
||||||
|
) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return "PAPER";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toApiDifficulty = (
|
||||||
|
value: ServerFormData["difficulty"]
|
||||||
|
): Uppercase<ServerFormData["difficulty"]> => {
|
||||||
|
return value.toUpperCase() as Uppercase<ServerFormData["difficulty"]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toApiMode = (value: ServerFormData["mode"]): Uppercase<ServerFormData["mode"]> => {
|
||||||
|
return value.toUpperCase() as Uppercase<ServerFormData["mode"]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toManagedEnvironmentVariables = (data: ServerFormData): Record<string, string> => {
|
||||||
|
const environment: Record<string, string> = {};
|
||||||
|
|
||||||
|
if (data.allowFlight) environment.ALLOW_FLIGHT = String(data.allowFlight);
|
||||||
|
if (data.enableCommandBlock) {
|
||||||
|
environment.ENABLE_COMMAND_BLOCK = String(data.enableCommandBlock);
|
||||||
|
}
|
||||||
|
if (data.spawnProtection) environment.SPAWN_PROTECTION = data.spawnProtection;
|
||||||
|
if (data.viewDistance) environment.VIEW_DISTANCE = data.viewDistance;
|
||||||
|
if (data.simulationDistance) environment.SIMULATION_DISTANCE = data.simulationDistance;
|
||||||
|
|
||||||
|
if (data.levelName) environment.LEVEL = data.levelName;
|
||||||
|
if (data.levelSeed) environment.SEED = data.levelSeed;
|
||||||
|
if (data.levelType) environment.LEVEL_TYPE = data.levelType;
|
||||||
|
if (data.generatorSettings) environment.GENERATOR_SETTINGS = data.generatorSettings;
|
||||||
|
if (data.hardcore) environment.HARDCORE = String(data.hardcore);
|
||||||
|
if (data.spawnAnimals !== undefined) environment.SPAWN_ANIMALS = String(data.spawnAnimals);
|
||||||
|
if (data.spawnMonsters !== undefined) environment.SPAWN_MONSTERS = String(data.spawnMonsters);
|
||||||
|
if (data.spawnNpcs !== undefined) environment.SPAWN_NPCS = String(data.spawnNpcs);
|
||||||
|
|
||||||
|
if (data.enableWhitelist) environment.ENABLE_WHITELIST = String(data.enableWhitelist);
|
||||||
|
if (data.whitelist) environment.WHITELIST = data.whitelist;
|
||||||
|
if (data.whitelistFile) environment.WHITELIST_FILE = data.whitelistFile;
|
||||||
|
if (data.ops) environment.OPS = data.ops;
|
||||||
|
if (data.opsFile) environment.OPS_FILE = data.opsFile;
|
||||||
|
|
||||||
|
if (data.jvmXxOpts) environment.JVM_XX_OPTS = data.jvmXxOpts;
|
||||||
|
if (data.jvmDdOpts) environment.JVM_DD_OPTS = data.jvmDdOpts;
|
||||||
|
if (data.enableJmx) environment.ENABLE_JMX = String(data.enableJmx);
|
||||||
|
|
||||||
|
if (data.resourcePack) environment.RESOURCE_PACK = data.resourcePack;
|
||||||
|
if (data.resourcePackSha1) environment.RESOURCE_PACK_SHA1 = data.resourcePackSha1;
|
||||||
|
if (data.resourcePackEnforce) {
|
||||||
|
environment.RESOURCE_PACK_ENFORCE = String(data.resourcePackEnforce);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.enableRcon !== undefined) environment.ENABLE_RCON = String(data.enableRcon);
|
||||||
|
if (data.rconPassword) environment.RCON_PASSWORD = data.rconPassword;
|
||||||
|
if (data.rconPort) environment.RCON_PORT = data.rconPort;
|
||||||
|
if (data.rconCmdsStartup) environment.RCON_CMDS_STARTUP = data.rconCmdsStartup;
|
||||||
|
if (data.rconCmdsOnConnect) environment.RCON_CMDS_ON_CONNECT = data.rconCmdsOnConnect;
|
||||||
|
if (data.rconCmdsFirstConnect) {
|
||||||
|
environment.RCON_CMDS_FIRST_CONNECT = data.rconCmdsFirstConnect;
|
||||||
|
}
|
||||||
|
if (data.rconCmdsOnDisconnect) {
|
||||||
|
environment.RCON_CMDS_ON_DISCONNECT = data.rconCmdsOnDisconnect;
|
||||||
|
}
|
||||||
|
if (data.rconCmdsLastDisconnect) {
|
||||||
|
environment.RCON_CMDS_LAST_DISCONNECT = data.rconCmdsLastDisconnect;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.enableQuery !== undefined) environment.ENABLE_QUERY = String(data.enableQuery);
|
||||||
|
if (data.queryPort) environment.QUERY_PORT = data.queryPort;
|
||||||
|
|
||||||
|
if (data.enableAutopause) environment.ENABLE_AUTOPAUSE = String(data.enableAutopause);
|
||||||
|
if (data.autopauseTimeoutEst) environment.AUTOPAUSE_TIMEOUT_EST = data.autopauseTimeoutEst;
|
||||||
|
if (data.autopauseTimeoutInit) environment.AUTOPAUSE_TIMEOUT_INIT = data.autopauseTimeoutInit;
|
||||||
|
if (data.autopauseTimeoutKn) environment.AUTOPAUSE_TIMEOUT_KN = data.autopauseTimeoutKn;
|
||||||
|
if (data.autopausePeriod) environment.AUTOPAUSE_PERIOD = data.autopausePeriod;
|
||||||
|
if (data.autopauseKnockInterface) {
|
||||||
|
environment.AUTOPAUSE_KNOCK_INTERFACE = data.autopauseKnockInterface;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.enableAutostop) environment.ENABLE_AUTOSTOP = String(data.enableAutostop);
|
||||||
|
if (data.autostopTimeoutEst) environment.AUTOSTOP_TIMEOUT_EST = data.autostopTimeoutEst;
|
||||||
|
if (data.autostopTimeoutInit) environment.AUTOSTOP_TIMEOUT_INIT = data.autostopTimeoutInit;
|
||||||
|
if (data.autostopPeriod) environment.AUTOSTOP_PERIOD = data.autostopPeriod;
|
||||||
|
|
||||||
|
if (data.plugins) environment.PLUGINS = data.plugins;
|
||||||
|
if (data.removeOldPlugins) environment.REMOVE_OLD_PLUGINS = String(data.removeOldPlugins);
|
||||||
|
if (data.spigetResources) environment.SPIGET_RESOURCES = data.spigetResources;
|
||||||
|
if (data.paperBuild) environment.PAPER_BUILD = data.paperBuild;
|
||||||
|
|
||||||
|
if (data.type === "CUSTOM" && data.customJarUrl) {
|
||||||
|
environment.CUSTOM_SERVER = data.customJarUrl;
|
||||||
|
environment.VERSION = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.timezone) environment.TZ = data.timezone;
|
||||||
|
if (data.uid) environment.UID = data.uid;
|
||||||
|
if (data.gid) environment.GID = data.gid;
|
||||||
|
if (data.stopDuration) environment.STOP_DURATION = data.stopDuration;
|
||||||
|
if (data.serverIcon) environment.ICON = data.serverIcon;
|
||||||
|
|
||||||
|
environment.EULA = String(data.eula);
|
||||||
|
environment.TYPE = data.type;
|
||||||
|
if (data.type !== "CUSTOM" && data.version) {
|
||||||
|
environment.VERSION = data.version;
|
||||||
|
}
|
||||||
|
if (data.type === "CUSTOM" && !data.customJarUrl) {
|
||||||
|
throw new Error("Custom jar URL is required for custom servers");
|
||||||
|
}
|
||||||
|
|
||||||
|
return environment;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const filterCustomEnvironmentVariables = (
|
||||||
|
environmentVariables?: EnvironmentVariable[]
|
||||||
|
): EnvVar[] => {
|
||||||
|
return (environmentVariables ?? [])
|
||||||
|
.filter(({ key }) => !MANAGED_ENVIRONMENT_KEYS.has(key))
|
||||||
|
.map(({ key, value }) => ({ key, value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toCommonServerRequestFields = (data: ServerFormData): CommonServerRequestFields => {
|
||||||
|
const environment = toManagedEnvironmentVariables(data);
|
||||||
|
for (const { key, value } of data.envVars) {
|
||||||
|
if (key && value) {
|
||||||
|
environment[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
description: data.description.trim() || null,
|
||||||
|
listen_port: Number(data.listenPort),
|
||||||
|
service_type: data.serviceType,
|
||||||
|
node_port: data.serviceType === "NODE_PORT" && data.nodePort ? Number(data.nodePort) : null,
|
||||||
|
env_variables: Object.entries(environment).map(([key, value]) => ({ key, value })),
|
||||||
|
memory: data.memoryLimit ? Number(data.memoryLimit) : undefined,
|
||||||
|
memory_request: data.memoryRequest ? Number(data.memoryRequest) : undefined,
|
||||||
|
cpu_request: data.cpuRequest || undefined,
|
||||||
|
cpu_limit: data.cpuLimit || undefined,
|
||||||
|
jar_type: data.type === "CUSTOM" ? "VANILLA" : data.type,
|
||||||
|
minecraft_version: data.type === "CUSTOM" ? undefined : data.version || "LATEST",
|
||||||
|
jvm_opts: data.jvmOpts || undefined,
|
||||||
|
use_aikar_flags: data.useAikarFlags || undefined,
|
||||||
|
use_meowice_flags: data.useMeowiceFlags || undefined,
|
||||||
|
difficulty: toApiDifficulty(data.difficulty),
|
||||||
|
game_mode: toApiMode(data.mode),
|
||||||
|
max_players: data.maxPlayers ? Number(data.maxPlayers) : undefined,
|
||||||
|
pvp: data.pvp,
|
||||||
|
online_mode: data.onlineMode,
|
||||||
|
motd: data.motd,
|
||||||
|
level_seed: data.levelSeed,
|
||||||
|
level_type: data.levelType,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toInitialServerFormData = (server: NormalServer): Partial<ServerFormData> => {
|
||||||
|
const environment = Object.fromEntries(
|
||||||
|
server.env_variables.map(({ key, value }) => [key, value])
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: server.id,
|
||||||
|
description: server.description || "",
|
||||||
|
memoryLimit: String(server.memory || 2048),
|
||||||
|
memoryRequest: String(server.memory_request ?? 1024),
|
||||||
|
cpuRequest: server.cpu_request || "500m",
|
||||||
|
cpuLimit: server.cpu_limit || "2",
|
||||||
|
type: normalizeServerType(environment.TYPE || server.jar_type),
|
||||||
|
version: environment.VERSION || server.minecraft_version || "",
|
||||||
|
customJarUrl: environment.CUSTOM_SERVER || undefined,
|
||||||
|
eula: environment.EULA === "true",
|
||||||
|
listenPort: String(server.listen_port || 25565),
|
||||||
|
serviceType: normalizeServiceType(server.service_type),
|
||||||
|
nodePort: server.node_port ? String(server.node_port) : undefined,
|
||||||
|
|
||||||
|
motd: environment.MOTD || server.motd || undefined,
|
||||||
|
difficulty: normalizeDifficulty(environment.DIFFICULTY || server.difficulty),
|
||||||
|
mode: normalizeMode(environment.MODE || server.game_mode),
|
||||||
|
maxPlayers: environment.MAX_PLAYERS || String(server.max_players || 20),
|
||||||
|
pvp: environment.PVP ? environment.PVP === "true" : (server.pvp ?? true),
|
||||||
|
onlineMode: environment.ONLINE_MODE
|
||||||
|
? environment.ONLINE_MODE === "true"
|
||||||
|
: (server.online_mode ?? true),
|
||||||
|
allowFlight: environment.ALLOW_FLIGHT === "true",
|
||||||
|
enableCommandBlock: environment.ENABLE_COMMAND_BLOCK === "true",
|
||||||
|
spawnProtection: environment.SPAWN_PROTECTION || "16",
|
||||||
|
viewDistance: environment.VIEW_DISTANCE || "10",
|
||||||
|
simulationDistance: environment.SIMULATION_DISTANCE || "10",
|
||||||
|
|
||||||
|
levelName: environment.LEVEL || "world",
|
||||||
|
levelSeed: environment.SEED || server.level_seed || undefined,
|
||||||
|
levelType: environment.LEVEL_TYPE || server.level_type || undefined,
|
||||||
|
generatorSettings: environment.GENERATOR_SETTINGS || undefined,
|
||||||
|
hardcore: environment.HARDCORE === "true",
|
||||||
|
spawnAnimals: environment.SPAWN_ANIMALS !== "false",
|
||||||
|
spawnMonsters: environment.SPAWN_MONSTERS !== "false",
|
||||||
|
spawnNpcs: environment.SPAWN_NPCS !== "false",
|
||||||
|
|
||||||
|
enableWhitelist: environment.ENABLE_WHITELIST === "true",
|
||||||
|
whitelist: environment.WHITELIST || undefined,
|
||||||
|
whitelistFile: environment.WHITELIST_FILE || undefined,
|
||||||
|
ops: environment.OPS || undefined,
|
||||||
|
opsFile: environment.OPS_FILE || undefined,
|
||||||
|
|
||||||
|
useAikarFlags: environment.USE_AIKAR_FLAGS === "true" || server.use_aikar_flags || false,
|
||||||
|
useMeowiceFlags: environment.USE_MEOWICE_FLAGS === "true" || server.use_meowice_flags || false,
|
||||||
|
jvmOpts: environment.JVM_OPTS || server.jvm_opts || undefined,
|
||||||
|
jvmXxOpts: environment.JVM_XX_OPTS || undefined,
|
||||||
|
jvmDdOpts: environment.JVM_DD_OPTS || undefined,
|
||||||
|
enableJmx: environment.ENABLE_JMX === "true",
|
||||||
|
|
||||||
|
resourcePack: environment.RESOURCE_PACK || undefined,
|
||||||
|
resourcePackSha1: environment.RESOURCE_PACK_SHA1 || undefined,
|
||||||
|
resourcePackEnforce: environment.RESOURCE_PACK_ENFORCE === "true",
|
||||||
|
|
||||||
|
enableRcon: environment.ENABLE_RCON !== "false",
|
||||||
|
rconPassword: environment.RCON_PASSWORD || undefined,
|
||||||
|
rconPort: environment.RCON_PORT || "25575",
|
||||||
|
rconCmdsStartup: environment.RCON_CMDS_STARTUP || undefined,
|
||||||
|
rconCmdsOnConnect: environment.RCON_CMDS_ON_CONNECT || undefined,
|
||||||
|
rconCmdsFirstConnect: environment.RCON_CMDS_FIRST_CONNECT || undefined,
|
||||||
|
rconCmdsOnDisconnect: environment.RCON_CMDS_ON_DISCONNECT || undefined,
|
||||||
|
rconCmdsLastDisconnect: environment.RCON_CMDS_LAST_DISCONNECT || undefined,
|
||||||
|
|
||||||
|
enableQuery: environment.ENABLE_QUERY === "true",
|
||||||
|
queryPort: environment.QUERY_PORT || "25565",
|
||||||
|
|
||||||
|
enableAutopause: environment.ENABLE_AUTOPAUSE === "true",
|
||||||
|
autopauseTimeoutEst: environment.AUTOPAUSE_TIMEOUT_EST || "3600",
|
||||||
|
autopauseTimeoutInit: environment.AUTOPAUSE_TIMEOUT_INIT || "600",
|
||||||
|
autopauseTimeoutKn: environment.AUTOPAUSE_TIMEOUT_KN || "120",
|
||||||
|
autopausePeriod: environment.AUTOPAUSE_PERIOD || "10",
|
||||||
|
autopauseKnockInterface: environment.AUTOPAUSE_KNOCK_INTERFACE || "eth0",
|
||||||
|
|
||||||
|
enableAutostop: environment.ENABLE_AUTOSTOP === "true",
|
||||||
|
autostopTimeoutEst: environment.AUTOSTOP_TIMEOUT_EST || "3600",
|
||||||
|
autostopTimeoutInit: environment.AUTOSTOP_TIMEOUT_INIT || "1800",
|
||||||
|
autostopPeriod: environment.AUTOSTOP_PERIOD || "10",
|
||||||
|
|
||||||
|
plugins: environment.PLUGINS || undefined,
|
||||||
|
removeOldPlugins: environment.REMOVE_OLD_PLUGINS === "true",
|
||||||
|
spigetResources: environment.SPIGET_RESOURCES || undefined,
|
||||||
|
paperBuild: environment.PAPER_BUILD || undefined,
|
||||||
|
|
||||||
|
timezone: environment.TZ || "UTC",
|
||||||
|
uid: environment.UID || "1000",
|
||||||
|
gid: environment.GID || "1000",
|
||||||
|
stopDuration: environment.STOP_DURATION || "60",
|
||||||
|
serverIcon: environment.ICON || undefined,
|
||||||
|
envVars: filterCustomEnvironmentVariables(server.env_variables),
|
||||||
|
};
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user