diff --git a/apps/web/app/dashboard/servers/create/page.tsx b/apps/web/app/dashboard/servers/create/page.tsx index f6c48e6..1c0874e 100644 --- a/apps/web/app/dashboard/servers/create/page.tsx +++ b/apps/web/app/dashboard/servers/create/page.tsx @@ -4,139 +4,20 @@ import type { CreateServerRequest } from "@minikura/api"; import { ArrowLeft } from "lucide-react"; import { useRouter } from "next/navigation"; 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 { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { api } from "@/lib/api-client"; - -const toDifficultyUppercase = (value: string): "PEACEFUL" | "EASY" | "NORMAL" | "HARD" => { - return value.toUpperCase() as "PEACEFUL" | "EASY" | "NORMAL" | "HARD"; -}; - -const toModeUppercase = (value: string): "SURVIVAL" | "CREATIVE" | "ADVENTURE" | "SPECTATOR" => { - return value.toUpperCase() as "SURVIVAL" | "CREATIVE" | "ADVENTURE" | "SPECTATOR"; -}; +import { toCommonServerRequestFields } from "@/lib/server-form/mappings"; export default function CreateServerPage() { const router = useRouter(); const handleSubmit = async (data: ServerFormData) => { - const filteredEnvVars = data.envVars.filter((ev) => ev.key && ev.value); - - const envVariables: Record = {}; - - 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 = { id: data.id.trim(), - description: data.description.trim() || null, - listen_port: Number(data.listenPort), type: "STATEFUL", - 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, + ...toCommonServerRequestFields(data), }; const response = await api.api.servers.post(payload); @@ -158,35 +39,29 @@ export default function CreateServerPage() { }; return ( -
-
- -
-

Create Minecraft Server

-

- Configure your new Minecraft server with comprehensive settings -

-
-
+ + router.push("/dashboard/servers")}> + + + } + className="flex-row items-center justify-start" + /> - - - Server Configuration - - Complete configuration for itzg/minecraft-server Docker image with all environment - variables - - - + router.push("/dashboard/servers")} submitLabel="Create Server" /> - - -
+ + ); } diff --git a/apps/web/app/dashboard/servers/edit/[id]/page.tsx b/apps/web/app/dashboard/servers/edit/[id]/page.tsx index 8ce21a3..87e0f95 100644 --- a/apps/web/app/dashboard/servers/edit/[id]/page.tsx +++ b/apps/web/app/dashboard/servers/edit/[id]/page.tsx @@ -4,11 +4,13 @@ import type { NormalServer, UpdateServerRequest } from "@minikura/api"; import { ArrowLeft } from "lucide-react"; import { useParams, useRouter } from "next/navigation"; 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 { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { api } from "@/lib/api-client"; import { getReverseProxyApi } from "@/lib/api-helpers"; +import { toCommonServerRequestFields, toInitialServerFormData } from "@/lib/server-form/mappings"; export default function EditServerPage() { const router = useRouter(); @@ -59,161 +61,10 @@ export default function EditServerPage() { } }, [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 = {}; - for (const { key, value } of envVars) { - parsed[key] = value; - } - return parsed; - }; - const handleSubmit = async (data: ServerFormData) => { if (!serverData) return; - const filteredEnvVars = data.envVars.filter((ev) => ev.key && ev.value); - - const envVariables: Record = {}; - 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 payload: UpdateServerRequest = toCommonServerRequestFields(data); const response = await api.api.servers({ id: serverId }).patch(payload); @@ -234,227 +85,55 @@ export default function EditServerPage() { }; if (loading) { - return ( -
-
-
-

Loading server data...

-
-
- ); + return ; } if (error || !serverData) { return ( -
-
-
- {error || "Server not found"} -
+ router.push("/dashboard/servers")} > - + Back to Servers -
-
+ } + /> ); } - const envVars = parseEnvVariables(serverData.env_variables); - - 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 = { - 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, - }; + const initialData = toInitialServerFormData(serverData); return ( -
-
- -
-

Edit Server: {serverData.id}

-

Update your Minecraft server configuration

-
-
+ + Edit {serverData.id}} + description="Update the workload specification and deployment behavior." + leading={ + + } + className="flex-row items-center justify-start" + /> - - - Server Configuration - Modify settings for your Minecraft server - - + router.push("/dashboard/servers")} submitLabel="Save Changes" /> - - -
+ + ); } diff --git a/apps/web/app/dashboard/servers/page.tsx b/apps/web/app/dashboard/servers/page.tsx index 679ea34..6548c93 100644 --- a/apps/web/app/dashboard/servers/page.tsx +++ b/apps/web/app/dashboard/servers/page.tsx @@ -1,451 +1,102 @@ "use client"; -import type { ConnectionInfo, NormalServer, PodInfo, ReverseProxyServer } from "@minikura/api"; -import { - AlertCircle, - Check, - CheckCircle2, - Copy, - FileText, - Globe, - Pencil, - Plus, - Server, - Trash2, -} from "lucide-react"; +import { Globe, Plus, Server, ServerCog } from "lucide-react"; import { useRouter } from "next/navigation"; -import { useCallback, useEffect, useState } from "react"; -import { Badge } from "@/components/ui/badge"; +import { useState } from "react"; +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 { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -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([]); - 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 ( -
-
- Loading... -
- ); - } - - if (pods.length === 0) { - return ( -
- - No pods -
- ); - } - - const allRunning = pods.every((pod) => pod.status === "Running"); - const readyCount = pods.filter((pod) => pod.ready === "1/1").length; - - return ( -
- {allRunning ? ( - - ) : ( - - )} - - {readyCount}/{pods.length} Ready - -
- ); -} - -function ConnectionInfoCell({ serverId, type }: { serverId: string; type: "normal" | "proxy" }) { - const [connectionInfo, setConnectionInfo] = useState(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 Loading...; - } - - if (!connectionInfo) { - return N/A; - } - - return ( - -
- - {connectionInfo.type} - - {connectionInfo.connectionString && ( -
- - {connectionInfo.connectionString} - - - - - - -

{copied ? "Copied!" : "Copy to clipboard"}

-
-
-
- )} - {connectionInfo.note && ( -

{connectionInfo.note}

- )} -
-
- ); -} +import { useServerList } from "@/hooks/use-server-list"; export default function ServersPage() { const router = useRouter(); - const [normalServers, setNormalServers] = useState([]); - const [reverseProxies, setReverseProxies] = useState([]); - const [loading, setLoading] = useState(true); + const { normalServers, reverseProxies, loading, error, deleteServer } = useServerList(); const [deleteTarget, setDeleteTarget] = useState<{ id: string; type: "normal" | "proxy"; } | 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 () => { if (!deleteTarget) return; try { - const reverseProxyApi = getReverseProxyApi(); - if (deleteTarget.type === "normal") { - await api.api.servers({ id: deleteTarget.id }).delete(); - } else { - await reverseProxyApi({ id: deleteTarget.id }).delete(); - } - await fetchServers(); + await deleteServer(deleteTarget.id, deleteTarget.type); setDeleteTarget(null); } catch (_error) {} }; - if (loading) { - return ( -
-
-

Server Management

-

Manage your Minecraft servers

-
-
-

Loading...

-
-
- ); - } - return ( -
-
-
-

Server Management

-

- Manage your Minecraft servers and reverse proxies -

-
- -
+ + router.push("/dashboard/servers/create")}> + + Create Server + + } + /> - - -
- - Minecraft Servers -
- Manage your normal Minecraft server instances -
- - {normalServers.length === 0 ? ( -
-

No servers created yet

-
- ) : ( -
- - - - ID - Status - Storage - Software - Version - Memory (MB) - Network - Description - Actions - - - - {normalServers.map((server) => ( - - {server.id} - - - - - {server.type} - - - {server.jar_type || "VANILLA"} - - - {server.minecraft_version || "LATEST"} - - {server.memory || 1024} - - - - - {server.description || "-"} - - -
- - - -
-
-
- ))} -
-
-
- )} -
-
+ {loading ? ( + + ) : error ? ( + + ) : ( + <> + } + count={normalServers.length} + emptyIcon={} + emptyTitle="No workloads" + emptyDescription="Create a server to begin." + > + router.push(`/dashboard/servers/edit/${id}`)} + onDelete={(id) => setDeleteTarget({ id, type: "normal" })} + /> + - - -
- - Reverse Proxy Servers -
- Manage your Velocity and BungeeCord proxy servers -
- - {reverseProxies.length === 0 ? ( -
-

No reverse proxies created yet

-
- ) : ( -
- - - - ID - Status - Type - External - Listen Port - Memory (MB) - Network - Description - Actions - - - - {reverseProxies.map((proxy) => ( - - {proxy.id} - - - - - {proxy.type} - - - {proxy.external_address}:{proxy.external_port} - - {proxy.listen_port} - {proxy.memory} - - - - - {proxy.description || "-"} - - -
- - - -
-
-
- ))} -
-
-
- )} -
-
+ } + count={reverseProxies.length} + emptyIcon={} + emptyTitle="No edge proxies" + emptyDescription="Proxy workloads will appear here." + > + router.push(`/dashboard/servers/edit/${id}`)} + onDelete={(id) => setDeleteTarget({ id, type: "proxy" })} + /> + + + )} - setDeleteTarget(null)}> - - - Delete Server - - Are you sure you want to delete this{" "} - {deleteTarget?.type === "normal" ? "server" : "reverse proxy"}? This action cannot be - undone. - - - - - - - - -
+ + Are you sure you want to delete this{" "} + {deleteTarget?.type === "normal" ? "server" : "reverse proxy"}? This action cannot be + undone. + + } + confirmLabel="Delete" + onConfirm={handleDelete} + onOpenChange={(open) => !open && setDeleteTarget(null)} + /> + ); } diff --git a/apps/web/components/form-controls.tsx b/apps/web/components/form-controls.tsx new file mode 100644 index 0000000..96e79cf --- /dev/null +++ b/apps/web/components/form-controls.tsx @@ -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 ( + + + + + {content} + + ); +} + +export function FormNotice({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} diff --git a/apps/web/components/server-form.tsx b/apps/web/components/server-form.tsx index 4cecf65..8c753e3 100644 --- a/apps/web/components/server-form.tsx +++ b/apps/web/components/server-form.tsx @@ -1,163 +1,28 @@ "use client"; -import type { GameMode, ServiceType as PrismaServiceType, ServerDifficulty } from "@minikura/db"; -import { Info, Plus, Trash2 } from "lucide-react"; import { useState } from "react"; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "@/components/ui/accordion"; +import { AdvancedPanel } from "@/components/server-form/advanced-panel"; +import { AutomationPanel } from "@/components/server-form/automation-panel"; +import { BasicPanel } from "@/components/server-form/basic-panel"; +import { ModsPanel } from "@/components/server-form/mods-panel"; +import { NetworkPanel } from "@/components/server-form/network-panel"; +import { PerformancePanel } from "@/components/server-form/performance-panel"; +import { PlayersPanel } from "@/components/server-form/players-panel"; +import { ResourcesPanel } from "@/components/server-form/resources-panel"; +import { ServerPanel } from "@/components/server-form/server-panel"; +import type { ServerFormData, UpdateServerField } from "@/components/server-form/types"; +import { WorldPanel } from "@/components/server-form/world-panel"; import { Button } from "@/components/ui/button"; -import { Checkbox } from "@/components/ui/checkbox"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { Textarea } from "@/components/ui/textarea"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; -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 { - // Basic Configuration - id: string; - description: string; - memoryLimit: string; - memoryRequest: string; - cpuRequest: string; - cpuLimit: string; - - // Server Type & Version - type: ServerType; - version?: string; - customJarUrl?: string; - eula: boolean; - - // Network Configuration - listenPort: string; - serviceType: ServiceType; - nodePort?: string; - - // Server Properties - 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; - - // World Configuration - levelName: string; - levelSeed?: string; - levelType?: string; - generatorSettings?: string; - hardcore: boolean; - spawnAnimals: boolean; - spawnMonsters: boolean; - spawnNpcs: boolean; - - // Player Management - enableWhitelist: boolean; - whitelist?: string; - whitelistFile?: string; - ops?: string; - opsFile?: string; - - // JVM & Performance - useAikarFlags: boolean; - useMeowiceFlags: boolean; - jvmOpts?: string; - jvmXxOpts?: string; - jvmDdOpts?: string; - - // Resource Pack - - resourcePack?: string; - resourcePackSha1?: string; - resourcePackEnforce: boolean; - - // RCON Configuration - enableRcon: boolean; - rconPassword?: string; - rconPort: string; - rconCmdsStartup?: string; - rconCmdsOnConnect?: string; - rconCmdsFirstConnect?: string; - rconCmdsOnDisconnect?: string; - rconCmdsLastDisconnect?: string; - - // Query Protocol - enableQuery: boolean; - queryPort: string; - - // Auto-Pause - enableAutopause: boolean; - autopauseTimeoutEst: string; - autopauseTimeoutInit: string; - autopauseTimeoutKn: string; - autopausePeriod: string; - autopauseKnockInterface: string; - - // Auto-Stop - enableAutostop: boolean; - autostopTimeoutEst: string; - autostopTimeoutInit: string; - autostopPeriod: string; - - // Mods & Plugins - plugins?: string; - removeOldPlugins: boolean; - spigetResources?: string; - - // Type-Specific Options - paperBuild?: string; - - // Advanced - timezone: string; - uid: string; - gid: string; - enableJmx: boolean; - stopDuration: string; - serverIcon?: string; - - // Environment Variables - envVars: EnvVar[]; -} - -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 type { + Difficulty, + EnvVar, + Mode, + ServerFormData, + ServerType, + ServiceType, +} from "@/components/server-form/types"; interface ServerFormProps { initialData?: Partial; @@ -167,15 +32,6 @@ interface ServerFormProps { loading?: boolean; } -const InfoTooltip = ({ text }: { text: string }) => ( -
- - - {text} - -
-); - export function ServerForm({ initialData, onSubmit, @@ -185,7 +41,6 @@ export function ServerForm({ }: ServerFormProps) { const defaultType = initialData?.type || "PAPER"; const defaultVersion = defaultType === "CUSTOM" ? "" : initialData?.version || "LATEST"; - const [formData, setFormData] = useState({ id: initialData?.id || "", description: initialData?.description || "", @@ -244,55 +99,50 @@ export function ServerForm({ value: envVar.value, })), }); - const [error, setError] = useState(null); - const updateField = (key: K, value: ServerFormData[K]) => { - setFormData((prev) => ({ ...prev, [key]: value })); + const updateField: UpdateServerField = (key, value) => { + setFormData((previous) => ({ ...previous, [key]: value })); }; const addEnvVar = () => { - setFormData((prev) => ({ - ...prev, - envVars: [...prev.envVars, { id: crypto.randomUUID(), key: "", value: "" }], + setFormData((previous) => ({ + ...previous, + envVars: [...previous.envVars, { id: crypto.randomUUID(), key: "", value: "" }], })); }; const removeEnvVar = (index: number) => { - setFormData((prev) => ({ - ...prev, - envVars: prev.envVars.filter((_, i) => i !== index), + setFormData((previous) => ({ + ...previous, + envVars: previous.envVars.filter((_, envVarIndex) => envVarIndex !== index), })); }; const updateEnvVar = (index: number, field: "key" | "value", value: string) => { - setFormData((prev) => { - const updated = [...prev.envVars]; - updated[index] = { ...updated[index], [field]: value }; - return { ...prev, envVars: updated }; + setFormData((previous) => { + const envVars = [...previous.envVars]; + envVars[index] = { ...envVars[index], [field]: value }; + return { ...previous, envVars }; }); }; - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); setError(null); - // Basic validation if (!formData.id.trim()) { setError("Server ID is required"); return; } - if (!/^[a-zA-Z0-9-_]+$/.test(formData.id)) { setError("ID must be alphanumeric with - or _"); return; } - if (!formData.eula) { setError("You must accept the Minecraft EULA to create a server"); return; } - if (formData.type === "CUSTOM" && !formData.customJarUrl?.trim()) { setError("Custom jar URL is required for custom servers"); return; @@ -300,15 +150,17 @@ export function ServerForm({ try { await onSubmit(formData); - } catch (err) { - setError(err instanceof Error ? err.message : "An error occurred"); + } catch (submitError) { + setError(submitError instanceof Error ? submitError.message : "An error occurred"); } }; + const panelProps = { formData, updateField }; + return ( -
+ - + Basic Server World @@ -320,1103 +172,30 @@ export function ServerForm({ Network Advanced - - {/* Basic Configuration */} - -
- - updateField("id", e.target.value)} - placeholder="my-server" - required - /> -
- -
- -