feat(panel): support full server config edit dialog with status prefilling
This commit is contained in:
8 files changed
+356
-3
No files matched your search
@@ -23,6 +23,11 @@ type ServerInfo struct {
|
|||||||
EndpointAddress string `json:"endpointAddress,omitempty"`
|
EndpointAddress string `json:"endpointAddress,omitempty"`
|
||||||
PlayersOnline int32 `json:"playersOnline"`
|
PlayersOnline int32 `json:"playersOnline"`
|
||||||
PlayersMax int32 `json:"playersMax"`
|
PlayersMax int32 `json:"playersMax"`
|
||||||
|
DisplayName string `json:"displayName,omitempty"`
|
||||||
|
Image string `json:"image,omitempty"`
|
||||||
|
JavaMemory string `json:"javaMemory,omitempty"`
|
||||||
|
StorageSize string `json:"storageSize,omitempty"`
|
||||||
|
CPU string `json:"cpu,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateServerInput is the validated, structured create-server form (spec §15).
|
// CreateServerInput is the validated, structured create-server form (spec §15).
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
|
|
||||||
"felis.lolicon.best/internal/apis/felis/v1alpha1"
|
"felis.lolicon.best/internal/apis/felis/v1alpha1"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
@@ -142,6 +143,13 @@ func (k *K8sCluster) PatchServerSpec(ctx context.Context, name string, p ServerS
|
|||||||
|
|
||||||
// serverInfo projects a MinecraftServer onto the API's lifecycle view.
|
// serverInfo projects a MinecraftServer onto the API's lifecycle view.
|
||||||
func serverInfo(ms *v1alpha1.MinecraftServer) *ServerInfo {
|
func serverInfo(ms *v1alpha1.MinecraftServer) *ServerInfo {
|
||||||
|
var cpuStr string
|
||||||
|
if ms.Spec.Resources.Limits != nil {
|
||||||
|
if limit, ok := ms.Spec.Resources.Limits[corev1.ResourceCPU]; ok {
|
||||||
|
cpuStr = limit.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return &ServerInfo{
|
return &ServerInfo{
|
||||||
Name: ms.Name,
|
Name: ms.Name,
|
||||||
Subdomain: ms.Spec.Subdomain,
|
Subdomain: ms.Spec.Subdomain,
|
||||||
@@ -153,5 +161,10 @@ func serverInfo(ms *v1alpha1.MinecraftServer) *ServerInfo {
|
|||||||
EndpointAddress: ms.Status.Endpoint.Address,
|
EndpointAddress: ms.Status.Endpoint.Address,
|
||||||
PlayersOnline: ms.Status.Players.Online,
|
PlayersOnline: ms.Status.Players.Online,
|
||||||
PlayersMax: ms.Status.Players.Max,
|
PlayersMax: ms.Status.Players.Max,
|
||||||
|
DisplayName: ms.Spec.DisplayName,
|
||||||
|
Image: ms.Spec.Image,
|
||||||
|
JavaMemory: ms.Spec.JavaMemory,
|
||||||
|
StorageSize: ms.Spec.Storage.Size,
|
||||||
|
CPU: cpuStr,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,283 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { Settings, ChevronRight } from "lucide-react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { api, humanizeError } from "@/lib/api";
|
||||||
|
import { useAsync } from "@/lib/hooks";
|
||||||
|
import type { AutostartPolicy } from "@/lib/types";
|
||||||
|
|
||||||
|
const MEMORY_OPTIONS = ["2Gi", "4Gi", "6Gi", "8Gi"];
|
||||||
|
|
||||||
|
function policyOptions(t: (key: string) => string): { value: AutostartPolicy; label: string }[] {
|
||||||
|
return [
|
||||||
|
{ value: "ownerOnly", label: t("create_server_policy_owner") },
|
||||||
|
{ value: "public", label: t("create_server_policy_public") },
|
||||||
|
{ value: "allowlist", label: t("create_server_policy_allowlist") },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EditServerForm {
|
||||||
|
displayName: string;
|
||||||
|
autostartPolicy: AutostartPolicy;
|
||||||
|
image: string;
|
||||||
|
memory: string;
|
||||||
|
cpu: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
serverName: string;
|
||||||
|
currentDisplayName?: string;
|
||||||
|
currentPolicy?: AutostartPolicy;
|
||||||
|
currentImage?: string;
|
||||||
|
currentMemory?: string;
|
||||||
|
currentStorage?: string;
|
||||||
|
currentCpu?: string;
|
||||||
|
onUpdated: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EditServerDialog({
|
||||||
|
serverName,
|
||||||
|
currentDisplayName = "",
|
||||||
|
currentPolicy = "ownerOnly",
|
||||||
|
currentImage = "",
|
||||||
|
currentMemory = "",
|
||||||
|
currentStorage = "",
|
||||||
|
currentCpu = "",
|
||||||
|
onUpdated,
|
||||||
|
}: Props) {
|
||||||
|
const { t } = useTranslation("servers");
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const images = useAsync(() => api.listImages(), []);
|
||||||
|
|
||||||
|
const [form, setForm] = useState<EditServerForm>({
|
||||||
|
displayName: currentDisplayName,
|
||||||
|
autostartPolicy: currentPolicy,
|
||||||
|
image: currentImage,
|
||||||
|
memory: currentMemory,
|
||||||
|
cpu: currentCpu,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// Sync form state when dialog opens or current values change from server status
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
setForm({
|
||||||
|
displayName: currentDisplayName,
|
||||||
|
autostartPolicy: currentPolicy,
|
||||||
|
image: currentImage,
|
||||||
|
memory: currentMemory,
|
||||||
|
cpu: currentCpu,
|
||||||
|
});
|
||||||
|
setError(null);
|
||||||
|
}
|
||||||
|
}, [open, currentDisplayName, currentPolicy, currentImage, currentMemory, currentCpu]);
|
||||||
|
|
||||||
|
function set<K extends keyof EditServerForm>(k: K, v: EditServerForm[K]) {
|
||||||
|
setForm((f) => ({ ...f, [k]: v }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const enabledImages = (images.data ?? []).filter((i) => i.enabled);
|
||||||
|
|
||||||
|
// Checks if form state has mutated from initial values
|
||||||
|
const hasChanges =
|
||||||
|
form.displayName !== currentDisplayName ||
|
||||||
|
form.autostartPolicy !== currentPolicy ||
|
||||||
|
form.image !== currentImage ||
|
||||||
|
form.memory !== currentMemory ||
|
||||||
|
form.cpu !== currentCpu;
|
||||||
|
|
||||||
|
const canSubmit = hasChanges && !submitting;
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
setSubmitting(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const payload: Parameters<typeof api.patchServer>[1] = {};
|
||||||
|
|
||||||
|
if (form.displayName !== currentDisplayName) {
|
||||||
|
payload.displayName = form.displayName.trim() || undefined;
|
||||||
|
}
|
||||||
|
if (form.autostartPolicy !== currentPolicy) {
|
||||||
|
payload.autostartPolicy = form.autostartPolicy;
|
||||||
|
}
|
||||||
|
if (form.image !== currentImage) {
|
||||||
|
payload.image = form.image;
|
||||||
|
}
|
||||||
|
if (form.memory !== currentMemory) {
|
||||||
|
payload.memory = form.memory;
|
||||||
|
}
|
||||||
|
if (form.cpu !== currentCpu) {
|
||||||
|
payload.resources = {
|
||||||
|
cpu: form.cpu.trim(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await api.patchServer(serverName, payload);
|
||||||
|
setOpen(false);
|
||||||
|
onUpdated();
|
||||||
|
} catch (e) {
|
||||||
|
setError(humanizeError(e));
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const policies = policyOptions(t);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
{/* Unified Sidebar Link/Card style trigger */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="group flex w-full text-left items-center gap-3 rounded-lg border border-border bg-card p-4 transition-colors hover:border-primary/40 hover:bg-accent focus:outline-none"
|
||||||
|
>
|
||||||
|
<Settings className="h-5 w-5 shrink-0 text-primary" />
|
||||||
|
<div className="min-w-0 flex-1 text-left">
|
||||||
|
<p className="text-sm font-medium">{t("edit_server_title")}</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
|
{t("edit_server_desc")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ChevronRight className="h-4 w-4 shrink-0 text-muted-foreground transition-transform group-hover:translate-x-0.5" />
|
||||||
|
</button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{t("edit_server_title")}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{t("edit_server_desc_long")}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="es-display">{t("create_server_display_name")}</Label>
|
||||||
|
<Input
|
||||||
|
id="es-display"
|
||||||
|
placeholder={t("create_server_display_name_placeholder")}
|
||||||
|
value={form.displayName}
|
||||||
|
onChange={(e) => set("displayName", e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label>{t("create_server_image")}</Label>
|
||||||
|
<Select value={form.image} onValueChange={(v) => set("image", v)}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue
|
||||||
|
placeholder={
|
||||||
|
images.loading
|
||||||
|
? t("create_server_image_loading")
|
||||||
|
: enabledImages.length
|
||||||
|
? t("create_server_image_choose")
|
||||||
|
: t("create_server_image_none")
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{/* Fallback to display the current image even if not in the whitelist options list */}
|
||||||
|
{form.image && !enabledImages.some((img) => img.image_ref === form.image) && (
|
||||||
|
<SelectItem value={form.image}>{form.image}</SelectItem>
|
||||||
|
)}
|
||||||
|
{enabledImages.map((img) => (
|
||||||
|
<SelectItem key={img.image_ref} value={img.image_ref}>
|
||||||
|
{img.image_ref}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label>{t("create_server_memory")}</Label>
|
||||||
|
<Select value={form.memory} onValueChange={(v) => set("memory", v)}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{/* Prepend current memory if not present in options */}
|
||||||
|
{form.memory && !MEMORY_OPTIONS.includes(form.memory) && (
|
||||||
|
<SelectItem value={form.memory}>{form.memory}</SelectItem>
|
||||||
|
)}
|
||||||
|
{MEMORY_OPTIONS.map((m) => (
|
||||||
|
<SelectItem key={m} value={m}>
|
||||||
|
{m}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
{/* Storage is immutable but displayed to maintain visual consistency */}
|
||||||
|
<Label>{t("create_server_storage")}</Label>
|
||||||
|
<Input value={currentStorage} disabled className="opacity-60 cursor-not-allowed select-none bg-muted" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="es-cpu">CPU 限制</Label>
|
||||||
|
<Input
|
||||||
|
id="es-cpu"
|
||||||
|
placeholder='例如 1, 2, 500m'
|
||||||
|
value={form.cpu}
|
||||||
|
onChange={(e) => set("cpu", e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label>{t("create_server_policy")}</Label>
|
||||||
|
<Select
|
||||||
|
value={form.autostartPolicy}
|
||||||
|
onValueChange={(v) => set("autostartPolicy", v as AutostartPolicy)}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{policies.map((p) => (
|
||||||
|
<SelectItem key={p.value} value={p.value}>
|
||||||
|
{p.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <p className="text-sm text-destructive">{error}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="ghost" onClick={() => setOpen(false)} disabled={submitting}>
|
||||||
|
{t("create_server_cancel")}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={submit} disabled={!canSubmit}>
|
||||||
|
{submitting ? t("edit_server_updating") : t("edit_server_submit")}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -128,5 +128,12 @@
|
|||||||
"create_server_creating": "Creating…",
|
"create_server_creating": "Creating…",
|
||||||
"no_servers_managed": "No servers under your management yet",
|
"no_servers_managed": "No servers under your management yet",
|
||||||
"no_servers_managed_hint": "Use \"New server\" to provision one from the vetted spec.",
|
"no_servers_managed_hint": "Use \"New server\" to provision one from the vetted spec.",
|
||||||
"server_admin_footer": "Server creation goes through the structured form only — the platform maps your choices onto a vetted Kubernetes spec. Raw cluster config (host networking, host paths, arbitrary images, privileged pods) is never expressible here."
|
"server_admin_footer": "Server creation goes through the structured form only — the platform maps your choices onto a vetted Kubernetes spec. Raw cluster config (host networking, host paths, arbitrary images, privileged pods) is never expressible here.",
|
||||||
|
"edit_server_title": "Edit Server Config",
|
||||||
|
"edit_server_desc": "Configure display name, autostart policy, image, memory, and CPU",
|
||||||
|
"edit_server_desc_long": "Updating server spec. Fields left unchanged will retain their current values.",
|
||||||
|
"edit_server_unchanged": "Leave unchanged",
|
||||||
|
"edit_server_immutable": "Unchanged (Immutable)",
|
||||||
|
"edit_server_submit": "Save Config",
|
||||||
|
"edit_server_updating": "Saving…"
|
||||||
}
|
}
|
||||||
@@ -128,5 +128,12 @@
|
|||||||
"create_server_creating": "创建中…",
|
"create_server_creating": "创建中…",
|
||||||
"no_servers_managed": "你还没有管理的服务器",
|
"no_servers_managed": "你还没有管理的服务器",
|
||||||
"no_servers_managed_hint": "使用「新建服务器」在线分配。",
|
"no_servers_managed_hint": "使用「新建服务器」在线分配。",
|
||||||
"server_admin_footer": "服务器仅通过此结构化表单创建——平台将选择映射为审核后的 Kubernetes spec。宿主机网络、宿主机路径、任意镜像、特权 Pod 等原始集群配置在此不可表达。"
|
"server_admin_footer": "服务器仅通过此结构化表单创建——平台将选择映射为审核后的 Kubernetes spec。宿主机网络、宿主机路径、任意镜像、特权 Pod 等原始集群配置在此不可表达。",
|
||||||
|
"edit_server_title": "编辑服务器配置",
|
||||||
|
"edit_server_desc": "配置显示名、自启策略、镜像、内存与CPU",
|
||||||
|
"edit_server_desc_long": "正在修改服务器的 spec 配置。未修改的项将保持原样。",
|
||||||
|
"edit_server_unchanged": "保持不变",
|
||||||
|
"edit_server_immutable": "保持不变 (不可修改)",
|
||||||
|
"edit_server_submit": "保存配置",
|
||||||
|
"edit_server_updating": "正在保存…"
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import type {
|
import type {
|
||||||
AccessResult,
|
AccessResult,
|
||||||
ApiError,
|
ApiError,
|
||||||
|
AutostartPolicy,
|
||||||
BackupView,
|
BackupView,
|
||||||
BanlistResult,
|
BanlistResult,
|
||||||
CreateServerRequest,
|
CreateServerRequest,
|
||||||
@@ -162,6 +163,24 @@ export const api = {
|
|||||||
req,
|
req,
|
||||||
),
|
),
|
||||||
|
|
||||||
|
patchServer: (name: string, req: {
|
||||||
|
displayName?: string;
|
||||||
|
autostartPolicy?: AutostartPolicy;
|
||||||
|
image?: string;
|
||||||
|
memory?: string;
|
||||||
|
resources?: {
|
||||||
|
cpu?: string;
|
||||||
|
cpuRequest?: string;
|
||||||
|
memory?: string;
|
||||||
|
memoryRequest?: string;
|
||||||
|
};
|
||||||
|
}) =>
|
||||||
|
request<{ name: string; desiredState: string }>(
|
||||||
|
"PATCH",
|
||||||
|
`/servers/${name}`,
|
||||||
|
req,
|
||||||
|
),
|
||||||
|
|
||||||
// World backups (spec §7). listBackups is the app-tier read: an admin sees every
|
// World backups (spec §7). listBackups is the app-tier read: an admin sees every
|
||||||
// present backup, a user only the backups of worlds they formerly owned — the
|
// present backup, a user only the backups of worlds they formerly owned — the
|
||||||
// scope is decided server-side from the principal, not by any client filter, so a
|
// scope is decided server-side from the principal, not by any client filter, so a
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ export interface ServerInfo {
|
|||||||
claimable?: boolean;
|
claimable?: boolean;
|
||||||
/** Whether the caller owns it. */
|
/** Whether the caller owns it. */
|
||||||
owned?: boolean;
|
owned?: boolean;
|
||||||
|
image?: string;
|
||||||
|
javaMemory?: string;
|
||||||
|
storageSize?: string;
|
||||||
|
cpu?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** WhitelistResult projects GET /servers/{name}/access/whitelist (spec §7 access).
|
/** WhitelistResult projects GET /servers/{name}/access/whitelist (spec §7 access).
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ import { LogConsole } from "@/components/LogConsole";
|
|||||||
import { Loading, ErrorState } from "@/components/States";
|
import { Loading, ErrorState } from "@/components/States";
|
||||||
import { api, consoleStreamURL, humanizeError } from "@/lib/api";
|
import { api, consoleStreamURL, humanizeError } from "@/lib/api";
|
||||||
import { useAsync, useConfig } from "@/lib/hooks";
|
import { useAsync, useConfig } from "@/lib/hooks";
|
||||||
|
import { useTier } from "@/lib/tier";
|
||||||
import { hostFor } from "@/lib/config";
|
import { hostFor } from "@/lib/config";
|
||||||
import type { Phase } from "@/lib/types";
|
import type { Phase, AutostartPolicy } from "@/lib/types";
|
||||||
|
import { EditServerDialog } from "@/components/EditServerDialog";
|
||||||
|
|
||||||
// notStreamingCopy explains why there is no live feed for a phase that has no
|
// notStreamingCopy explains why there is no live feed for a phase that has no
|
||||||
// streamable pod. The read path only has something to relay once a pod is up, so
|
// streamable pod. The read path only has something to relay once a pod is up, so
|
||||||
@@ -172,6 +174,7 @@ function CommandInput({ name }: { name: string }) {
|
|||||||
export function ServerConsole() {
|
export function ServerConsole() {
|
||||||
const { name = "" } = useParams();
|
const { name = "" } = useParams();
|
||||||
const cfg = useConfig();
|
const cfg = useConfig();
|
||||||
|
const { isAdmin } = useTier();
|
||||||
const { t } = useTranslation("servers");
|
const { t } = useTranslation("servers");
|
||||||
const { data, error, loading, reload } = useAsync(
|
const { data, error, loading, reload } = useAsync(
|
||||||
() => api.status(name),
|
() => api.status(name),
|
||||||
@@ -267,6 +270,18 @@ export function ServerConsole() {
|
|||||||
|
|
||||||
{/* Right/Sidebar column: Navigation */}
|
{/* Right/Sidebar column: Navigation */}
|
||||||
<div className="flex flex-col gap-4 lg:col-span-1 shrink-0">
|
<div className="flex flex-col gap-4 lg:col-span-1 shrink-0">
|
||||||
|
{isAdmin && (
|
||||||
|
<EditServerDialog
|
||||||
|
serverName={name}
|
||||||
|
currentDisplayName={data.displayName}
|
||||||
|
currentPolicy={data.autostartPolicy as AutostartPolicy}
|
||||||
|
currentImage={data.image}
|
||||||
|
currentMemory={data.javaMemory}
|
||||||
|
currentStorage={data.storageSize}
|
||||||
|
currentCpu={data.cpu}
|
||||||
|
onUpdated={reload}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{/* Player management lives on its own subpage (whitelist / online / bans),
|
{/* Player management lives on its own subpage (whitelist / online / bans),
|
||||||
not crammed under the console. This is the doorway to it; the page
|
not crammed under the console. This is the doorway to it; the page
|
||||||
itself owns the ownership + readiness gating. */}
|
itself owns the ownership + readiness gating. */}
|
||||||
|
|||||||
Reference in new issue
Block a user