diff --git a/virtweb_frontend/src/App.tsx b/virtweb_frontend/src/App.tsx index 33a96d3..23312d9 100644 --- a/virtweb_frontend/src/App.tsx +++ b/virtweb_frontend/src/App.tsx @@ -15,8 +15,9 @@ import { AuthApi } from "./api/AuthApi"; import { IsoFilesRoute } from "./routes/IsoFilesRoute"; import { ServerApi } from "./api/ServerApi"; import { SysInfoRoute } from "./routes/SysInfoRoute"; -import { VirtualMachinesRoute } from "./routes/VirtualMachinesRoute"; +import { VMListRoute } from "./routes/VMListRoute"; import { CreateVMRoute, EditVMRoute } from "./routes/EditVMRoute"; +import { VMRoute } from "./routes/VMRoute"; interface AuthContext { signedIn: boolean; @@ -39,8 +40,9 @@ export function App() { }> } /> - } /> + } /> } /> + } /> } /> } /> diff --git a/virtweb_frontend/src/routes/EditVMRoute.tsx b/virtweb_frontend/src/routes/EditVMRoute.tsx index ec6abfb..ceeb079 100644 --- a/virtweb_frontend/src/routes/EditVMRoute.tsx +++ b/virtweb_frontend/src/routes/EditVMRoute.tsx @@ -1,16 +1,12 @@ -import { Button, Grid, Paper, Typography } from "@mui/material"; -import React, { PropsWithChildren } from "react"; +import { Button } from "@mui/material"; +import React from "react"; import { useNavigate, useParams } from "react-router-dom"; -import { validate as validateUUID } from "uuid"; -import { ServerApi } from "../api/ServerApi"; import { VMApi, VMInfo } from "../api/VMApi"; import { useAlert } from "../hooks/providers/AlertDialogProvider"; import { useSnackbar } from "../hooks/providers/SnackbarProvider"; import { AsyncWidget } from "../widgets/AsyncWidget"; import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; -import { CheckboxInput } from "../widgets/forms/CheckboxInput"; -import { SelectInput } from "../widgets/forms/SelectInput"; -import { TextInput } from "../widgets/forms/TextInput"; +import { VMDetails } from "../widgets/vms/VMDetails"; export function CreateVMRoute(): React.ReactElement { const snackbar = useSnackbar(); @@ -105,7 +101,6 @@ function EditVMInner(p: { setChanged(true); forceUpdate(); }; - return ( } > - - {/* Metadata section */} - - { - p.vm.name = v ?? ""; - valueChanged(); - }} - size={ServerApi.Config.constraints.name_size} - /> - - - - { - p.vm.genid = v; - valueChanged(); - }} - checkValue={(v) => validateUUID(v)} - /> - - { - p.vm.title = v; - valueChanged(); - }} - size={ServerApi.Config.constraints.title_size} - /> - - { - p.vm.description = v; - valueChanged(); - }} - multiline={true} - /> - - - {/* General section */} - - { - p.vm.architecture = v! as any; - valueChanged(); - }} - value={p.vm.architecture} - options={[ - { label: "i686", value: "i686" }, - { label: "x86_64", value: "x86_64" }, - ]} - /> - - { - p.vm.boot_type = v! as any; - valueChanged(); - }} - value={p.vm.boot_type} - options={[ - { label: "UEFI with Secure Boot", value: "UEFISecureBoot" }, - { label: "UEFI", value: "UEFI" }, - ]} - /> - - { - p.vm.memory = Number(v ?? "0"); - valueChanged(); - }} - checkValue={(v) => - Number(v) > ServerApi.Config.constraints.memory_size.min && - Number(v) < ServerApi.Config.constraints.memory_size.max - } - /> - - { - p.vm.vnc_access = v; - valueChanged(); - }} - /> - - + ); } - -function EditSection( - p: { title: string } & PropsWithChildren -): React.ReactElement { - return ( - - - - {p.title} - - {p.children} - - - ); -} diff --git a/virtweb_frontend/src/routes/VirtualMachinesRoute.tsx b/virtweb_frontend/src/routes/VMListRoute.tsx similarity index 97% rename from virtweb_frontend/src/routes/VirtualMachinesRoute.tsx rename to virtweb_frontend/src/routes/VMListRoute.tsx index 842c45d..a82452c 100644 --- a/virtweb_frontend/src/routes/VirtualMachinesRoute.tsx +++ b/virtweb_frontend/src/routes/VMListRoute.tsx @@ -22,7 +22,7 @@ import { VMStatusWidget } from "../widgets/vms/VMStatusWidget"; import { useSnackbar } from "../hooks/providers/SnackbarProvider"; import { useConfirm } from "../hooks/providers/ConfirmDialogProvider"; -export function VirtualMachinesRoute(): React.ReactElement { +export function VMListRoute(): React.ReactElement { const [list, setList] = React.useState(); const loadKey = React.useRef(1); @@ -119,7 +119,7 @@ function VMListWidget(p: { {row.description ?? ""} {filesize(row.memory * 1000 * 1000)} - + diff --git a/virtweb_frontend/src/routes/VMRoute.tsx b/virtweb_frontend/src/routes/VMRoute.tsx new file mode 100644 index 0000000..4bfb42f --- /dev/null +++ b/virtweb_frontend/src/routes/VMRoute.tsx @@ -0,0 +1,56 @@ +import { useNavigate, useParams } from "react-router-dom"; +import { VMApi, VMInfo, VMState } from "../api/VMApi"; +import React from "react"; +import { AsyncWidget } from "../widgets/AsyncWidget"; +import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; +import { VMDetails } from "../widgets/vms/VMDetails"; +import { VMStatusWidget } from "../widgets/vms/VMStatusWidget"; +import { Button } from "@mui/material"; + +export function VMRoute(): React.ReactElement { + const { uuid } = useParams(); + + const [vm, setVM] = React.useState(); + + const load = async () => { + setVM(await VMApi.GetSingle(uuid!)); + }; + + return ( + } + /> + ); +} + +function VMRouteBody(p: { vm: VMInfo }): React.ReactElement { + const navigate = useNavigate(); + + const [state, setState] = React.useState(); + + return ( + + + + {(state === "Shutdown" || state === "Shutoff") && ( + + )} + + } + > + + + ); +} diff --git a/virtweb_frontend/src/widgets/forms/CheckboxInput.tsx b/virtweb_frontend/src/widgets/forms/CheckboxInput.tsx index e3b6ecd..b8c27d7 100644 --- a/virtweb_frontend/src/widgets/forms/CheckboxInput.tsx +++ b/virtweb_frontend/src/widgets/forms/CheckboxInput.tsx @@ -6,15 +6,16 @@ export function CheckboxInput(p: { checked: boolean | undefined; onValueChange: (v: boolean) => void; }): React.ReactElement { - if (!p.editable && p.checked) - return {p.label}; + //if (!p.editable && p.checked) + // return {p.label}; - if (!p.editable) return <>; + //if (!p.editable) return <>; return ( p.onValueChange(e.target.checked)} /> diff --git a/virtweb_frontend/src/widgets/forms/SelectInput.tsx b/virtweb_frontend/src/widgets/forms/SelectInput.tsx index f0a9f5a..fc02b2c 100644 --- a/virtweb_frontend/src/widgets/forms/SelectInput.tsx +++ b/virtweb_frontend/src/widgets/forms/SelectInput.tsx @@ -8,16 +8,16 @@ export interface SelectOption { export function SelectInput(p: { value?: string; - editing: boolean; + editable: boolean; label: string; options: SelectOption[]; onValueChange: (o?: string) => void; }): React.ReactElement { - if (!p.editing && !p.value) return <>; + if (!p.editable && !p.value) return <>; - if (!p.editing) { + if (!p.editable) { const value = p.options.find((o) => o.value === p.value)?.label; - return ; + return ; } return ( diff --git a/virtweb_frontend/src/widgets/vms/VMDetails.tsx b/virtweb_frontend/src/widgets/vms/VMDetails.tsx new file mode 100644 index 0000000..59e6bb4 --- /dev/null +++ b/virtweb_frontend/src/widgets/vms/VMDetails.tsx @@ -0,0 +1,138 @@ +import { Grid, Paper, Typography } from "@mui/material"; +import { PropsWithChildren } from "react"; +import { validate as validateUUID } from "uuid"; +import { ServerApi } from "../../api/ServerApi"; +import { VMInfo } from "../../api/VMApi"; +import { CheckboxInput } from "../forms/CheckboxInput"; +import { SelectInput } from "../forms/SelectInput"; +import { TextInput } from "../forms/TextInput"; + +export function VMDetails(p: { + vm: VMInfo; + editable: boolean; + onChange?: () => void; +}): React.ReactElement { + return ( + + {/* Metadata section */} + + { + p.vm.name = v ?? ""; + p.onChange?.(); + }} + size={ServerApi.Config.constraints.name_size} + /> + + + + { + p.vm.genid = v; + p.onChange?.(); + }} + checkValue={(v) => validateUUID(v)} + /> + + { + p.vm.title = v; + p.onChange?.(); + }} + size={ServerApi.Config.constraints.title_size} + /> + + { + p.vm.description = v; + p.onChange?.(); + }} + multiline={true} + /> + + + {/* General section */} + + { + p.vm.architecture = v! as any; + p.onChange?.(); + }} + value={p.vm.architecture} + options={[ + { label: "i686", value: "i686" }, + { label: "x86_64", value: "x86_64" }, + ]} + /> + + { + p.vm.boot_type = v! as any; + p.onChange?.(); + }} + value={p.vm.boot_type} + options={[ + { label: "UEFI with Secure Boot", value: "UEFISecureBoot" }, + { label: "UEFI", value: "UEFI" }, + ]} + /> + + { + p.vm.memory = Number(v ?? "0"); + p.onChange?.(); + }} + checkValue={(v) => + Number(v) > ServerApi.Config.constraints.memory_size.min && + Number(v) < ServerApi.Config.constraints.memory_size.max + } + /> + + { + p.vm.vnc_access = v; + p.onChange?.(); + }} + /> + + + ); +} + +function EditSection( + p: { title: string } & PropsWithChildren +): React.ReactElement { + return ( + + + + {p.title} + + {p.children} + + + ); +} diff --git a/virtweb_frontend/src/widgets/vms/VMStatusWidget.tsx b/virtweb_frontend/src/widgets/vms/VMStatusWidget.tsx index 66c0305..118e833 100644 --- a/virtweb_frontend/src/widgets/vms/VMStatusWidget.tsx +++ b/virtweb_frontend/src/widgets/vms/VMStatusWidget.tsx @@ -11,7 +11,7 @@ import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider"; import { useSnackbar } from "../../hooks/providers/SnackbarProvider"; export function VMStatusWidget(p: { - d: VMInfo; + vm: VMInfo; onChange?: (s: VMState) => void; }): React.ReactElement { const snackbar = useSnackbar(); @@ -20,7 +20,7 @@ export function VMStatusWidget(p: { const refresh = async () => { try { - const s = await VMApi.GetState(p.d); + const s = await VMApi.GetState(p.vm); if (s !== state) p.onChange?.(s); setState(s); } catch (e) { @@ -32,6 +32,7 @@ export function VMStatusWidget(p: { const changedAction = () => setState(undefined); React.useEffect(() => { + refresh(); const i = setInterval(() => refresh(), 3000); return () => clearInterval(i); @@ -54,7 +55,7 @@ export function VMStatusWidget(p: { cond={["Shutdown", "Shutoff", "Crashed"]} icon={} tooltip="Start the Virtual Machine" - performAction={() => VMApi.StartVM(p.d)} + performAction={() => VMApi.StartVM(p.vm)} onExecuted={changedAction} /> @@ -64,7 +65,7 @@ export function VMStatusWidget(p: { cond={["Paused", "PowerManagementSuspended"]} icon={} tooltip="Resume the Virtual Machine" - performAction={() => VMApi.ResumeVM(p.d)} + performAction={() => VMApi.ResumeVM(p.vm)} onExecuted={changedAction} /> @@ -75,7 +76,7 @@ export function VMStatusWidget(p: { icon={} tooltip="Suspend the Virtual Machine" confirmMessage="Do you really want to supsend this VM?" - performAction={() => VMApi.SuspendVM(p.d)} + performAction={() => VMApi.SuspendVM(p.vm)} onExecuted={changedAction} /> @@ -86,7 +87,7 @@ export function VMStatusWidget(p: { icon={} tooltip="Shutdown the Virtual Machine" confirmMessage="Do you really want to shutdown this VM?" - performAction={() => VMApi.ShutdownVM(p.d)} + performAction={() => VMApi.ShutdownVM(p.vm)} onExecuted={changedAction} /> @@ -97,7 +98,7 @@ export function VMStatusWidget(p: { icon={} tooltip="Kill the Virtual Machine" confirmMessage="Do you really want to kill this VM? This could lead to data loss / corruption!" - performAction={() => VMApi.KillVM(p.d)} + performAction={() => VMApi.KillVM(p.vm)} onExecuted={changedAction} /> @@ -108,7 +109,7 @@ export function VMStatusWidget(p: { icon={} tooltip="Reset the Virtual Machine" confirmMessage="Do you really want to reset this VM?" - performAction={() => VMApi.ResetVM(p.d)} + performAction={() => VMApi.ResetVM(p.vm)} onExecuted={changedAction} />