From e579a3aadd0f00f841e0384d6acd2a70b00d69c4 Mon Sep 17 00:00:00 2001 From: Pierre Hubert Date: Mon, 4 Dec 2023 20:16:32 +0100 Subject: [PATCH] Ready to implement network routes contents --- virtweb_backend/src/actors/libvirt_actor.rs | 82 ++++++++++++ virtweb_backend/src/app_config.rs | 9 +- .../src/controllers/network_controller.rs | 60 +++++++++ .../src/controllers/server_controller.rs | 13 +- virtweb_backend/src/libvirt_client.rs | 27 ++++ virtweb_backend/src/main.rs | 20 +++ virtweb_frontend/src/App.tsx | 26 ++-- virtweb_frontend/src/api/NetworksApi.ts | 40 +++++- virtweb_frontend/src/api/ServerApi.ts | 6 +- .../src/routes/EditNetworkRoute.tsx | 122 ++++++++++++++++++ .../src/routes/ViewNetworkRoute.tsx | 51 ++++++++ .../src/widgets/BaseAuthenticatedPage.tsx | 3 +- .../src/widgets/forms/EditSection.tsx | 17 +++ .../src/widgets/forms/VMSelectIsoInput.tsx | 2 +- .../src/widgets/net/NetworkDetails.tsx | 55 ++++++++ .../src/widgets/vms/VMDetails.tsx | 36 ++---- 16 files changed, 523 insertions(+), 46 deletions(-) create mode 100644 virtweb_frontend/src/routes/EditNetworkRoute.tsx create mode 100644 virtweb_frontend/src/routes/ViewNetworkRoute.tsx create mode 100644 virtweb_frontend/src/widgets/forms/EditSection.tsx create mode 100644 virtweb_frontend/src/widgets/net/NetworkDetails.tsx diff --git a/virtweb_backend/src/actors/libvirt_actor.rs b/virtweb_backend/src/actors/libvirt_actor.rs index 45f90d9..474422f 100644 --- a/virtweb_backend/src/actors/libvirt_actor.rs +++ b/virtweb_backend/src/actors/libvirt_actor.rs @@ -367,10 +367,12 @@ impl Handler for LibVirtActor { msg.0.ips = vec![]; let mut network_xml = serde_xml_rs::to_string(&msg.0)?; + log::trace!("Serialize network XML start: {network_xml}"); let ips_xml = ips_xml.join("\n"); network_xml = network_xml.replacen("", &format!("{ips_xml}"), 1); + log::debug!("Source network structure: {:#?}", msg.0); log::debug!("Define network XML: {network_xml}"); let network = Network::define_xml(&self.m, &network_xml)?; @@ -428,3 +430,83 @@ impl Handler for LibVirtActor { Ok(()) } } + +#[derive(Message)] +#[rtype(result = "anyhow::Result")] +pub struct IsNetworkAutostart(pub XMLUuid); + +impl Handler for LibVirtActor { + type Result = anyhow::Result; + + fn handle(&mut self, msg: IsNetworkAutostart, _ctx: &mut Self::Context) -> Self::Result { + log::debug!( + "Check if autostart is enabled for a network: {}", + msg.0.as_string() + ); + let network = Network::lookup_by_uuid_string(&self.m, &msg.0.as_string())?; + Ok(network.get_autostart()?) + } +} + +#[derive(Message)] +#[rtype(result = "anyhow::Result<()>")] +pub struct SetNetworkAutostart(pub XMLUuid, pub bool); + +impl Handler for LibVirtActor { + type Result = anyhow::Result<()>; + + fn handle(&mut self, msg: SetNetworkAutostart, _ctx: &mut Self::Context) -> Self::Result { + log::debug!( + "Set autostart enabled={} for a network: {}", + msg.1, + msg.0.as_string() + ); + let network = Network::lookup_by_uuid_string(&self.m, &msg.0.as_string())?; + network.set_autostart(msg.1)?; + Ok(()) + } +} + +#[derive(Message)] +#[rtype(result = "anyhow::Result")] +pub struct IsNetworkStarted(pub XMLUuid); + +impl Handler for LibVirtActor { + type Result = anyhow::Result; + + fn handle(&mut self, msg: IsNetworkStarted, _ctx: &mut Self::Context) -> Self::Result { + log::debug!("Check if a network is started: {}", msg.0.as_string()); + let network = Network::lookup_by_uuid_string(&self.m, &msg.0.as_string())?; + Ok(network.is_active()?) + } +} + +#[derive(Message)] +#[rtype(result = "anyhow::Result<()>")] +pub struct StartNetwork(pub XMLUuid); + +impl Handler for LibVirtActor { + type Result = anyhow::Result<()>; + + fn handle(&mut self, msg: StartNetwork, _ctx: &mut Self::Context) -> Self::Result { + log::debug!("Start a network: {}", msg.0.as_string()); + let network = Network::lookup_by_uuid_string(&self.m, &msg.0.as_string())?; + network.create()?; + Ok(()) + } +} + +#[derive(Message)] +#[rtype(result = "anyhow::Result<()>")] +pub struct StopNetwork(pub XMLUuid); + +impl Handler for LibVirtActor { + type Result = anyhow::Result<()>; + + fn handle(&mut self, msg: StopNetwork, _ctx: &mut Self::Context) -> Self::Result { + log::debug!("Stop a network: {}", msg.0.as_string()); + let network = Network::lookup_by_uuid_string(&self.m, &msg.0.as_string())?; + network.destroy()?; + Ok(()) + } +} diff --git a/virtweb_backend/src/app_config.rs b/virtweb_backend/src/app_config.rs index 4b434af..533bc3d 100644 --- a/virtweb_backend/src/app_config.rs +++ b/virtweb_backend/src/app_config.rs @@ -149,7 +149,14 @@ impl AppConfig { /// Get root storage directory pub fn storage_path(&self) -> PathBuf { - Path::new(&self.storage).canonicalize().unwrap() + let storage_path = Path::new(&self.storage); + if !storage_path.is_dir() { + panic!( + "Specified storage path ({}) is not a directory!", + self.storage + ); + } + storage_path.canonicalize().unwrap() } /// Get iso storage directory diff --git a/virtweb_backend/src/controllers/network_controller.rs b/virtweb_backend/src/controllers/network_controller.rs index 7c7dbc3..90154c1 100644 --- a/virtweb_backend/src/controllers/network_controller.rs +++ b/virtweb_backend/src/controllers/network_controller.rs @@ -17,6 +17,7 @@ pub async fn create(client: LibVirtReq, req: web::Json) -> HttpResu return Ok(HttpResponse::BadRequest().body(e.to_string())); } }; + let uid = client.update_network(network).await?; Ok(HttpResponse::Ok().json(NetworkID { uid })) @@ -60,3 +61,62 @@ pub async fn delete(client: LibVirtReq, path: web::Path) -> HttpResul Ok(HttpResponse::Ok().json("Network deleted")) } + +#[derive(serde::Serialize, serde::Deserialize)] +pub struct NetworkAutostart { + autostart: bool, +} + +/// Get autostart value of a network +pub async fn get_autostart(client: LibVirtReq, id: web::Path) -> HttpResult { + Ok(HttpResponse::Ok().json(NetworkAutostart { + autostart: client.is_network_autostart(id.uid).await?, + })) +} + +/// Configure autostart value for a network +pub async fn set_autostart( + client: LibVirtReq, + id: web::Path, + body: web::Json, +) -> HttpResult { + client.set_network_autostart(id.uid, body.autostart).await?; + Ok(HttpResponse::Accepted().json("OK")) +} + +#[derive(serde::Serialize)] +enum NetworkStatus { + Started, + Stopped, +} + +#[derive(serde::Serialize)] +struct NetworkStatusResponse { + status: NetworkStatus, +} + +/// Get network status +pub async fn status(client: LibVirtReq, id: web::Path) -> HttpResult { + let started = client.is_network_started(id.uid).await?; + + Ok(HttpResponse::Ok().json(NetworkStatusResponse { + status: match started { + true => NetworkStatus::Started, + false => NetworkStatus::Stopped, + }, + })) +} + +/// Start a network +pub async fn start(client: LibVirtReq, id: web::Path) -> HttpResult { + client.start_network(id.uid).await?; + + Ok(HttpResponse::Accepted().json("Network started")) +} + +/// Stop a network +pub async fn stop(client: LibVirtReq, id: web::Path) -> HttpResult { + client.stop_network(id.uid).await?; + + Ok(HttpResponse::Accepted().json("Network stopped")) +} diff --git a/virtweb_backend/src/controllers/server_controller.rs b/virtweb_backend/src/controllers/server_controller.rs index fbb0602..4b9c0d4 100644 --- a/virtweb_backend/src/controllers/server_controller.rs +++ b/virtweb_backend/src/controllers/server_controller.rs @@ -29,11 +29,13 @@ struct LenConstraints { #[derive(serde::Serialize)] struct ServerConstraints { iso_max_size: usize, - name_size: LenConstraints, - title_size: LenConstraints, + vm_name_size: LenConstraints, + vm_title_size: LenConstraints, memory_size: LenConstraints, disk_name_size: LenConstraints, disk_size: LenConstraints, + net_name_size: LenConstraints, + net_title_size: LenConstraints, } pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder { @@ -45,8 +47,8 @@ pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder { constraints: ServerConstraints { iso_max_size: constants::ISO_MAX_SIZE, - name_size: LenConstraints { min: 2, max: 50 }, - title_size: LenConstraints { min: 0, max: 50 }, + vm_name_size: LenConstraints { min: 2, max: 50 }, + vm_title_size: LenConstraints { min: 0, max: 50 }, memory_size: LenConstraints { min: constants::MIN_VM_MEMORY, max: constants::MAX_VM_MEMORY, @@ -59,6 +61,9 @@ pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder { min: DISK_SIZE_MIN, max: DISK_SIZE_MAX, }, + + net_name_size: LenConstraints { min: 2, max: 50 }, + net_title_size: LenConstraints { min: 0, max: 50 }, }, }) } diff --git a/virtweb_backend/src/libvirt_client.rs b/virtweb_backend/src/libvirt_client.rs index 29234ef..6948577 100644 --- a/virtweb_backend/src/libvirt_client.rs +++ b/virtweb_backend/src/libvirt_client.rs @@ -116,4 +116,31 @@ impl LibVirtClient { pub async fn delete_network(&self, id: XMLUuid) -> anyhow::Result<()> { self.0.send(libvirt_actor::DeleteNetwork(id)).await? } + + /// Get auto-start status of a network + pub async fn is_network_autostart(&self, id: XMLUuid) -> anyhow::Result { + self.0.send(libvirt_actor::IsNetworkAutostart(id)).await? + } + + /// Update autostart value of a network + pub async fn set_network_autostart(&self, id: XMLUuid, autostart: bool) -> anyhow::Result<()> { + self.0 + .send(libvirt_actor::SetNetworkAutostart(id, autostart)) + .await? + } + + /// Check out whether a network is started or not + pub async fn is_network_started(&self, id: XMLUuid) -> anyhow::Result { + self.0.send(libvirt_actor::IsNetworkStarted(id)).await? + } + + /// Start a network + pub async fn start_network(&self, id: XMLUuid) -> anyhow::Result<()> { + self.0.send(libvirt_actor::StartNetwork(id)).await? + } + + /// Stop a network + pub async fn stop_network(&self, id: XMLUuid) -> anyhow::Result<()> { + self.0.send(libvirt_actor::StopNetwork(id)).await? + } } diff --git a/virtweb_backend/src/main.rs b/virtweb_backend/src/main.rs index 1e8272f..fcd1a98 100644 --- a/virtweb_backend/src/main.rs +++ b/virtweb_backend/src/main.rs @@ -194,6 +194,26 @@ async fn main() -> std::io::Result<()> { "/api/network/{uid}", web::delete().to(network_controller::delete), ) + .route( + "/api/network/{uid}/autostart", + web::get().to(network_controller::get_autostart), + ) + .route( + "/api/network/{uid}/autostart", + web::put().to(network_controller::set_autostart), + ) + .route( + "/api/network/{uid}/status", + web::get().to(network_controller::status), + ) + .route( + "/api/network/{uid}/start", + web::get().to(network_controller::start), + ) + .route( + "/api/network/{uid}/stop", + web::get().to(network_controller::stop), + ) }) .bind(&AppConfig::get().listen_address)? .run() diff --git a/virtweb_frontend/src/App.tsx b/virtweb_frontend/src/App.tsx index ed5628b..76d85a0 100644 --- a/virtweb_frontend/src/App.tsx +++ b/virtweb_frontend/src/App.tsx @@ -1,25 +1,30 @@ import React from "react"; -import "./App.css"; import { Route, RouterProvider, createBrowserRouter, createRoutesFromElements, } from "react-router-dom"; -import { NotFoundRoute } from "./routes/NotFound"; -import { OIDCCbRoute } from "./routes/auth/OIDCCbRoute"; -import { BaseLoginPage } from "./widgets/BaseLoginPage"; -import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage"; -import { LoginRoute } from "./routes/auth/LoginRoute"; +import "./App.css"; import { AuthApi } from "./api/AuthApi"; -import { IsoFilesRoute } from "./routes/IsoFilesRoute"; import { ServerApi } from "./api/ServerApi"; +import { + CreateNetworkRoute, + EditNetworkRoute, +} from "./routes/EditNetworkRoute"; +import { CreateVMRoute, EditVMRoute } from "./routes/EditVMRoute"; +import { IsoFilesRoute } from "./routes/IsoFilesRoute"; +import { NetworksListRoute } from "./routes/NetworksListRoute"; +import { NotFoundRoute } from "./routes/NotFound"; import { SysInfoRoute } from "./routes/SysInfoRoute"; import { VMListRoute } from "./routes/VMListRoute"; -import { CreateVMRoute, EditVMRoute } from "./routes/EditVMRoute"; import { VMRoute } from "./routes/VMRoute"; import { VNCRoute } from "./routes/VNCRoute"; -import { NetworksListRoute } from "./routes/NetworksListRoute"; +import { LoginRoute } from "./routes/auth/LoginRoute"; +import { OIDCCbRoute } from "./routes/auth/OIDCCbRoute"; +import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage"; +import { BaseLoginPage } from "./widgets/BaseLoginPage"; +import { ViewNetworkRoute } from "./routes/ViewNetworkRoute"; interface AuthContext { signedIn: boolean; @@ -49,6 +54,9 @@ export function App() { } /> } /> + } /> + } /> + } /> } /> } /> diff --git a/virtweb_frontend/src/api/NetworksApi.ts b/virtweb_frontend/src/api/NetworksApi.ts index 11bc291..fa2c4f8 100644 --- a/virtweb_frontend/src/api/NetworksApi.ts +++ b/virtweb_frontend/src/api/NetworksApi.ts @@ -8,7 +8,7 @@ export interface IpConfig { export interface NetworkInfo { name: string; - uuid: string; + uuid?: string; title?: string; description?: string; forward_mode: "NAT" | "Isolated"; @@ -24,6 +24,19 @@ export function NetworkURL(n: NetworkInfo, edit: boolean = false): string { } export class NetworkApi { + /** + * Create a new network + */ + static async Create(n: NetworkInfo): Promise<{ uid: string }> { + return ( + await APIClient.exec({ + method: "POST", + uri: "/network/create", + jsonData: n, + }) + ).data; + } + /** * Get the entire list of networks */ @@ -36,6 +49,31 @@ export class NetworkApi { ).data; } + /** + * Get the information about a single network + */ + static async GetSingle(uuid: string): Promise { + return ( + await APIClient.exec({ + method: "GET", + uri: `/network/${uuid}`, + }) + ).data; + } + + /** + * Update an existing network + */ + static async Update(n: NetworkInfo): Promise<{ uid: string }> { + return ( + await APIClient.exec({ + method: "PUT", + uri: `/network/${n.uuid}`, + jsonData: n, + }) + ).data; + } + /** * Delete a network */ diff --git a/virtweb_frontend/src/api/ServerApi.ts b/virtweb_frontend/src/api/ServerApi.ts index 792ffa5..351b725 100644 --- a/virtweb_frontend/src/api/ServerApi.ts +++ b/virtweb_frontend/src/api/ServerApi.ts @@ -10,11 +10,13 @@ export interface ServerConfig { export interface ServerConstraints { iso_max_size: number; - name_size: LenConstraint; - title_size: LenConstraint; + vm_name_size: LenConstraint; + vm_title_size: LenConstraint; memory_size: LenConstraint; disk_name_size: LenConstraint; disk_size: LenConstraint; + net_name_size: LenConstraint; + net_title_size: LenConstraint; } export interface LenConstraint { diff --git a/virtweb_frontend/src/routes/EditNetworkRoute.tsx b/virtweb_frontend/src/routes/EditNetworkRoute.tsx new file mode 100644 index 0000000..b84b0b1 --- /dev/null +++ b/virtweb_frontend/src/routes/EditNetworkRoute.tsx @@ -0,0 +1,122 @@ +import { useNavigate, useParams } from "react-router-dom"; +import { NetworkApi, NetworkInfo } from "../api/NetworksApi"; +import { useAlert } from "../hooks/providers/AlertDialogProvider"; +import { useSnackbar } from "../hooks/providers/SnackbarProvider"; +import React from "react"; +import { AsyncWidget } from "../widgets/AsyncWidget"; +import { NetworkDetails } from "../widgets/net/NetworkDetails"; +import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; +import { Button } from "@mui/material"; + +export function CreateNetworkRoute(): React.ReactElement { + const alert = useAlert(); + const snackbar = useSnackbar(); + const navigate = useNavigate(); + + const [network] = React.useState({ + name: "NewNetwork", + forward_mode: "Isolated", + }); + + const createNetwork = async (n: NetworkInfo) => { + try { + const res = await NetworkApi.Create(n); + snackbar("The network was successfully created!"); + navigate(`/net/${res.uid}`); + } catch (e) { + console.error(e); + alert("Failed to create network!"); + } + }; + + return ( + navigate("/net")} + onSave={createNetwork} + /> + ); +} + +export function EditNetworkRoute(): React.ReactElement { + const alert = useAlert(); + const snackbar = useSnackbar(); + + const { uuid } = useParams(); + const navigate = useNavigate(); + + const [network, setNetwork] = React.useState(); + + const load = async () => { + setNetwork(await NetworkApi.GetSingle(uuid!)); + }; + + const updateNetwork = async (n: NetworkInfo) => { + try { + await NetworkApi.Update(n); + snackbar("The network was successfully updated!"); + navigate(`/net/${network!.uuid}`); + } catch (e) { + console.error(e); + alert("Failed to update network!"); + } + }; + + return ( + ( + navigate(`/net/${uuid}`)} + onSave={updateNetwork} + /> + )} + /> + ); +} + +function EditNetworkRouteInner(p: { + network: NetworkInfo; + creating: boolean; + onCancel: () => void; + onSave: (vm: NetworkInfo) => Promise; +}): React.ReactElement { + const [changed, setChanged] = React.useState(false); + + const [, updateState] = React.useState(); + const forceUpdate = React.useCallback(() => updateState({}), []); + + const valueChanged = () => { + setChanged(true); + forceUpdate(); + }; + return ( + + {changed && ( + + )} + + + } + > + + + ); +} diff --git a/virtweb_frontend/src/routes/ViewNetworkRoute.tsx b/virtweb_frontend/src/routes/ViewNetworkRoute.tsx new file mode 100644 index 0000000..3a6e7cc --- /dev/null +++ b/virtweb_frontend/src/routes/ViewNetworkRoute.tsx @@ -0,0 +1,51 @@ +import React from "react"; +import { NetworkApi, NetworkInfo } from "../api/NetworksApi"; +import { AsyncWidget } from "../widgets/AsyncWidget"; +import { useNavigate, useParams } from "react-router-dom"; +import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; +import { Button } from "@mui/material"; +import { NetworkDetails } from "../widgets/net/NetworkDetails"; + +export function ViewNetworkRoute() { + const { uuid } = useParams(); + + const [network, setNetwork] = React.useState(); + + const load = async () => { + setNetwork(await NetworkApi.GetSingle(uuid!)); + }; + + return ( + } + /> + ); +} + +function ViewNetworkRouteInner(p: { + network: NetworkInfo; +}): React.ReactElement { + const navigate = useNavigate(); + + return ( + navigate(`/net/${p.network.uuid}/edit`)} + > + Edit + + } + > + + + ); +} diff --git a/virtweb_frontend/src/widgets/BaseAuthenticatedPage.tsx b/virtweb_frontend/src/widgets/BaseAuthenticatedPage.tsx index 48f1263..4a91996 100644 --- a/virtweb_frontend/src/widgets/BaseAuthenticatedPage.tsx +++ b/virtweb_frontend/src/widgets/BaseAuthenticatedPage.tsx @@ -3,8 +3,7 @@ import { mdiDisc, mdiHome, mdiInformation, - mdiLan, - mdiNetwork, + mdiLan } from "@mdi/js"; import Icon from "@mdi/react"; import { diff --git a/virtweb_frontend/src/widgets/forms/EditSection.tsx b/virtweb_frontend/src/widgets/forms/EditSection.tsx new file mode 100644 index 0000000..01a0c1b --- /dev/null +++ b/virtweb_frontend/src/widgets/forms/EditSection.tsx @@ -0,0 +1,17 @@ +import { Grid, Paper, Typography } from "@mui/material"; +import { PropsWithChildren } from "react"; + +export function EditSection( + p: { title: string } & PropsWithChildren +): React.ReactElement { + return ( + + + + {p.title} + + {p.children} + + + ); +} diff --git a/virtweb_frontend/src/widgets/forms/VMSelectIsoInput.tsx b/virtweb_frontend/src/widgets/forms/VMSelectIsoInput.tsx index 7859105..f4d7228 100644 --- a/virtweb_frontend/src/widgets/forms/VMSelectIsoInput.tsx +++ b/virtweb_frontend/src/widgets/forms/VMSelectIsoInput.tsx @@ -22,7 +22,7 @@ export function VMSelectIsoInput(p: { if (!p.value && !p.editable) return <>; if (p.value) { - const iso = p.isoList.find((d) => d.filename == p.value); + const iso = p.isoList.find((d) => d.filename === p.value); return ( void; +}): React.ReactElement { + return ( + + {/* Metadata section */} + + { + p.net.name = v ?? ""; + p.onChange?.(); + }} + checkValue={(v) => /^[a-zA-Z0-9]+$/.test(v)} + size={ServerApi.Config.constraints.net_name_size} + /> + + + + { + p.net.title = v; + p.onChange?.(); + }} + size={ServerApi.Config.constraints.net_title_size} + /> + + { + p.net.description = v; + p.onChange?.(); + }} + multiline={true} + /> + + TODO:continue + + ); +} diff --git a/virtweb_frontend/src/widgets/vms/VMDetails.tsx b/virtweb_frontend/src/widgets/vms/VMDetails.tsx index 33cb20b..177ebb7 100644 --- a/virtweb_frontend/src/widgets/vms/VMDetails.tsx +++ b/virtweb_frontend/src/widgets/vms/VMDetails.tsx @@ -1,19 +1,18 @@ -import { Grid, Paper, Typography } from "@mui/material"; -import { PropsWithChildren } from "react"; +import { Grid } from "@mui/material"; +import React from "react"; import { validate as validateUUID } from "uuid"; +import { IsoFile, IsoFilesApi } from "../../api/IsoFilesApi"; import { ServerApi } from "../../api/ServerApi"; import { VMInfo } from "../../api/VMApi"; +import { AsyncWidget } from "../AsyncWidget"; import { CheckboxInput } from "../forms/CheckboxInput"; +import { EditSection } from "../forms/EditSection"; import { SelectInput } from "../forms/SelectInput"; import { TextInput } from "../forms/TextInput"; -import { VMScreenshot } from "./VMScreenshot"; -import { IsoFile, IsoFilesApi } from "../../api/IsoFilesApi"; -import { AsyncWidget } from "../AsyncWidget"; -import React from "react"; -import { filesize } from "filesize"; +import { VMAutostartInput } from "../forms/VMAutostartInput"; import { VMDisksList } from "../forms/VMDisksList"; import { VMSelectIsoInput } from "../forms/VMSelectIsoInput"; -import { VMAutostartInput } from "../forms/VMAutostartInput"; +import { VMScreenshot } from "./VMScreenshot"; interface DetailsProps { vm: VMInfo; @@ -34,7 +33,7 @@ export function VMDetails(p: DetailsProps): React.ReactElement { loadKey={"1"} load={load} errMsg="Failed to load the list of ISO files" - build={() => } + build={() => } /> ); } @@ -63,7 +62,7 @@ function VMDetailsInner( p.onChange?.(); }} checkValue={(v) => /^[a-zA-Z0-9]+$/.test(v)} - size={ServerApi.Config.constraints.name_size} + size={ServerApi.Config.constraints.vm_name_size} /> @@ -87,7 +86,7 @@ function VMDetailsInner( p.vm.title = v; p.onChange?.(); }} - size={ServerApi.Config.constraints.title_size} + size={ServerApi.Config.constraints.vm_title_size} /> ); } - -function EditSection( - p: { title: string } & PropsWithChildren -): React.ReactElement { - return ( - - - - {p.title} - - {p.children} - - - ); -}