From 302fe7475fa7d9cee738902adeb92eecaac633b8 Mon Sep 17 00:00:00 2001 From: Tilo-K Date: Fri, 26 Dec 2025 13:23:07 +0100 Subject: [PATCH] feat: add edit hostname --- frontend/src/components/hostname-edit.tsx | 89 +++++++++++ frontend/src/components/ui/dialog.tsx | 141 ++++++++++++++++++ frontend/src/routes/server.$serverId.tsx | 6 + scp_core/src/apis/default_api.rs | 11 +- ...pi_v1_servers__server_id__patch_request.rs | 34 +++++ src/main.rs | 1 + src/servers.rs | 31 +++- 7 files changed, 311 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/hostname-edit.tsx create mode 100644 frontend/src/components/ui/dialog.tsx create mode 100644 scp_core/src/models/api_v1_servers__server_id__patch_request.rs diff --git a/frontend/src/components/hostname-edit.tsx b/frontend/src/components/hostname-edit.tsx new file mode 100644 index 0000000..574fa82 --- /dev/null +++ b/frontend/src/components/hostname-edit.tsx @@ -0,0 +1,89 @@ +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import type { Server } from "@/models/server"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { useRef } from "react"; +import { toast } from "sonner"; + +export function HostnameEditDialog({ serverId }: { serverId: number }) { + const queryClient = useQueryClient(); + const query = useQuery({ + queryKey: ["server", serverId], + enabled: Boolean(serverId), + queryFn: async () => { + const response = await fetch(`/api/servers/${serverId}`); + if (!response.ok) { + throw new Error(`Failed to load server (${response.status})`); + } + return (await response.json()) as Server; + }, + refetchInterval: 5000, + }); + const ref = useRef(null); + async function updateHostname() { + let newHostname = ref.current?.value || ""; + if (!newHostname || newHostname.trim().length === 0) { + toast("Hostname is required", { + description: `Could not update hostname for server ${query.data?.nickname ?? query.data?.name}`, + }); + return; + } + let url = `/api/servers/${serverId}/hostname`; + let response = await fetch(url + "?new_hostname=" + newHostname, { + method: "PATCH", + }); + if (!response.ok) { + toast(`Failed to update server (${response.status})`); + return; + } + await queryClient.invalidateQueries({ queryKey: ["server", serverId] }); + } + return ( + +
+ + + + + + Edit Hostname + + Make changes to the server hostname here. Click save when + you're done. + + +
+
+ + +
+
+ + + + + + +
+
+
+ ); +} diff --git a/frontend/src/components/ui/dialog.tsx b/frontend/src/components/ui/dialog.tsx new file mode 100644 index 0000000..60cc10e --- /dev/null +++ b/frontend/src/components/ui/dialog.tsx @@ -0,0 +1,141 @@ +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { XIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Dialog({ + ...props +}: React.ComponentProps) { + return +} + +function DialogTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function DialogPortal({ + ...props +}: React.ComponentProps) { + return +} + +function DialogClose({ + ...props +}: React.ComponentProps) { + return +} + +function DialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogContent({ + className, + children, + showCloseButton = true, + ...props +}: React.ComponentProps & { + showCloseButton?: boolean +}) { + return ( + + + + {children} + {showCloseButton && ( + + + Close + + )} + + + ) +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +} diff --git a/frontend/src/routes/server.$serverId.tsx b/frontend/src/routes/server.$serverId.tsx index 73ef01e..c32f7b6 100644 --- a/frontend/src/routes/server.$serverId.tsx +++ b/frontend/src/routes/server.$serverId.tsx @@ -18,6 +18,7 @@ import { import { Skeleton } from "@/components/ui/skeleton"; import { Button } from "@/components/ui/button"; import { cityToFlag } from "@/models/cityflags"; +import { HostnameEditDialog } from "@/components/hostname-edit"; export const Route = createFileRoute("/server/$serverId")({ component: RouteComponent, @@ -126,6 +127,11 @@ function ServerDetails({ server }: { server: Server }) { Max CPU: {server.maxCpuCount}
+ + +
+ +
diff --git a/scp_core/src/apis/default_api.rs b/scp_core/src/apis/default_api.rs index 605bc5f..c6d9431 100644 --- a/scp_core/src/apis/default_api.rs +++ b/scp_core/src/apis/default_api.rs @@ -2724,14 +2724,23 @@ pub async fn api_v1_servers_server_id_patch( if let Some(ref user_agent) = configuration.user_agent { req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone()); } - req_builder = req_builder.json(&p_body_api_v1_servers_server_id_patch_request); + + req_builder = req_builder + .body(serde_json::to_string(&p_body_api_v1_servers_server_id_patch_request).unwrap()); if let Some(ref token) = configuration.bearer_access_token { req_builder = req_builder.header(reqwest::header::AUTHORIZATION, format!("Bearer {}", token)); } + req_builder = req_builder.header(reqwest::header::ACCEPT, "application/json"); + req_builder = req_builder.header( + reqwest::header::CONTENT_TYPE, + "application/merge-patch+json", + ); + let req = req_builder.build()?; + dbg!(req.body().unwrap()); let resp = configuration.client.execute(req).await?; let status = resp.status(); diff --git a/scp_core/src/models/api_v1_servers__server_id__patch_request.rs b/scp_core/src/models/api_v1_servers__server_id__patch_request.rs new file mode 100644 index 0000000..37a8ef4 --- /dev/null +++ b/scp_core/src/models/api_v1_servers__server_id__patch_request.rs @@ -0,0 +1,34 @@ +/* + * SCP (Server Control Panel) REST API + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 2025.1218.164029 + * + * Generated by: https://openapi-generator.tech + */ + +use crate::models; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum ApiV1ServersServerIdPatchRequest { + ServerStatePatch(Box), + ServerAutostartPatch(Box), + ServerBootorderPatch(Box), + ServerOsOptimizationPatch(Box), + ServerCpuTopologyPatch(Box), + ServerUefiPatch(Box), + ServerHostnamePatch(Box), + ServerNicknamePatch(Box), + ServerKeyboardLayoutPatch(Box), + ServerSetRootPasswordPatch(Box), +} + +impl Default for ApiV1ServersServerIdPatchRequest { + fn default() -> Self { + Self::ServerStatePatch(Default::default()) + } +} + diff --git a/src/main.rs b/src/main.rs index bd4127f..a8f4a36 100644 --- a/src/main.rs +++ b/src/main.rs @@ -56,6 +56,7 @@ async fn main() -> std::io::Result<()> { .service(auth::get_user) .service(servers::list_servers) .service(servers::get_server) + .service(servers::update_server_hostname) .service( Files::new("/", "./frontend/dist") .index_file("index.html") diff --git a/src/servers.rs b/src/servers.rs index 3c9835b..4eb1a6b 100644 --- a/src/servers.rs +++ b/src/servers.rs @@ -1,4 +1,5 @@ -use actix_web::{Responder, get, web}; +use actix_web::{Responder, get, patch, web}; +use serde::Deserialize; use crate::helper; @@ -26,3 +27,31 @@ pub async fn get_server(id: web::Path) -> impl Responder { serde_json::to_string(&response).unwrap() } + +#[derive(Deserialize)] +struct HostnameRequest { + new_hostname: String, +} + +#[patch("/api/servers/{id}/hostname")] +pub async fn update_server_hostname( + id: web::Path, + new_hostname: web::Query, +) -> impl Responder { + let config = helper::get_authed_api_config().await.unwrap(); + let hostname_patch = scp_core::models::server_hostname_patch::ServerHostnamePatch { + hostname: Some(new_hostname.new_hostname.clone()), + }; + let patch_request = + scp_core::models::_api_v1_servers__server_id__patch_request::ApiV1ServersServerIdPatchRequest::ServerHostnamePatch(Box::new(hostname_patch)); + let response = scp_core::apis::default_api::api_v1_servers_server_id_patch( + &config, + id.into_inner(), + patch_request, + None, + ) + .await + .unwrap(); + + serde_json::to_string(&response).unwrap() +}