Start to build VM page
This commit is contained in:
		| @@ -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() { | ||||
|         <Route path="*" element={<BaseAuthenticatedPage />}> | ||||
|           <Route path="iso" element={<IsoFilesRoute />} /> | ||||
|  | ||||
|           <Route path="vms" element={<VirtualMachinesRoute />} /> | ||||
|           <Route path="vms" element={<VMListRoute />} /> | ||||
|           <Route path="vms/new" element={<CreateVMRoute />} /> | ||||
|           <Route path="vm/:uuid" element={<VMRoute />} /> | ||||
|           <Route path="vm/:uuid/edit" element={<EditVMRoute />} /> | ||||
|  | ||||
|           <Route path="sysinfo" element={<SysInfoRoute />} /> | ||||
|   | ||||
| @@ -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 ( | ||||
|     <VirtWebRouteContainer | ||||
|       label={p.isCreating ? "Create a Virtual Machine" : "Edit Virtual Machine"} | ||||
| @@ -126,127 +121,7 @@ function EditVMInner(p: { | ||||
|         </span> | ||||
|       } | ||||
|     > | ||||
|       <Grid container spacing={2}> | ||||
|         {/* Metadata section */} | ||||
|         <EditSection title="Metadata"> | ||||
|           <TextInput | ||||
|             label="Name" | ||||
|             editable={true} | ||||
|             value={p.vm.name} | ||||
|             onValueChange={(v) => { | ||||
|               p.vm.name = v ?? ""; | ||||
|               valueChanged(); | ||||
|             }} | ||||
|             size={ServerApi.Config.constraints.name_size} | ||||
|           /> | ||||
|  | ||||
|           <TextInput label="UUID" editable={false} value={p.vm.uuid} /> | ||||
|  | ||||
|           <TextInput | ||||
|             label="VM genid" | ||||
|             editable={true} | ||||
|             value={p.vm.genid} | ||||
|             onValueChange={(v) => { | ||||
|               p.vm.genid = v; | ||||
|               valueChanged(); | ||||
|             }} | ||||
|             checkValue={(v) => validateUUID(v)} | ||||
|           /> | ||||
|  | ||||
|           <TextInput | ||||
|             label="Title" | ||||
|             editable={true} | ||||
|             value={p.vm.title} | ||||
|             onValueChange={(v) => { | ||||
|               p.vm.title = v; | ||||
|               valueChanged(); | ||||
|             }} | ||||
|             size={ServerApi.Config.constraints.title_size} | ||||
|           /> | ||||
|  | ||||
|           <TextInput | ||||
|             label="Description" | ||||
|             editable={true} | ||||
|             value={p.vm.description} | ||||
|             onValueChange={(v) => { | ||||
|               p.vm.description = v; | ||||
|               valueChanged(); | ||||
|             }} | ||||
|             multiline={true} | ||||
|           /> | ||||
|         </EditSection> | ||||
|  | ||||
|         {/* General section */} | ||||
|         <EditSection title="General"> | ||||
|           <SelectInput | ||||
|             editing={true} | ||||
|             label="CPU Architecture" | ||||
|             onValueChange={(v) => { | ||||
|               p.vm.architecture = v! as any; | ||||
|               valueChanged(); | ||||
|             }} | ||||
|             value={p.vm.architecture} | ||||
|             options={[ | ||||
|               { label: "i686", value: "i686" }, | ||||
|               { label: "x86_64", value: "x86_64" }, | ||||
|             ]} | ||||
|           /> | ||||
|  | ||||
|           <SelectInput | ||||
|             editing={true} | ||||
|             label="Boot type" | ||||
|             onValueChange={(v) => { | ||||
|               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" }, | ||||
|             ]} | ||||
|           /> | ||||
|  | ||||
|           <TextInput | ||||
|             label="Memory (MB)" | ||||
|             editable={true} | ||||
|             type="number" | ||||
|             value={p.vm.memory.toString()} | ||||
|             onValueChange={(v) => { | ||||
|               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 | ||||
|             } | ||||
|           /> | ||||
|  | ||||
|           <CheckboxInput | ||||
|             editable={true} | ||||
|             label="Enable VNC access" | ||||
|             checked={p.vm.vnc_access} | ||||
|             onValueChange={(v) => { | ||||
|               p.vm.vnc_access = v; | ||||
|               valueChanged(); | ||||
|             }} | ||||
|           /> | ||||
|         </EditSection> | ||||
|       </Grid> | ||||
|       <VMDetails vm={p.vm} editable={true} onChange={valueChanged} /> | ||||
|     </VirtWebRouteContainer> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function EditSection( | ||||
|   p: { title: string } & PropsWithChildren | ||||
| ): React.ReactElement { | ||||
|   return ( | ||||
|     <Grid item sm={12} md={6}> | ||||
|       <Paper style={{ margin: "10px", padding: "10px" }}> | ||||
|         <Typography variant="h5" style={{ marginBottom: "15px" }}> | ||||
|           {p.title} | ||||
|         </Typography> | ||||
|         {p.children} | ||||
|       </Paper> | ||||
|     </Grid> | ||||
|   ); | ||||
| } | ||||
|   | ||||
| @@ -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<VMInfo[] | undefined>(); | ||||
| 
 | ||||
|   const loadKey = React.useRef(1); | ||||
| @@ -119,7 +119,7 @@ function VMListWidget(p: { | ||||
|               <TableCell>{row.description ?? ""}</TableCell> | ||||
|               <TableCell>{filesize(row.memory * 1000 * 1000)}</TableCell> | ||||
|               <TableCell> | ||||
|                 <VMStatusWidget d={row} /> | ||||
|                 <VMStatusWidget vm={row} /> | ||||
|               </TableCell> | ||||
|               <TableCell> | ||||
|                 <Tooltip title="View this VM"> | ||||
							
								
								
									
										56
									
								
								virtweb_frontend/src/routes/VMRoute.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								virtweb_frontend/src/routes/VMRoute.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -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<VMInfo>(); | ||||
|  | ||||
|   const load = async () => { | ||||
|     setVM(await VMApi.GetSingle(uuid!)); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <AsyncWidget | ||||
|       loadKey={uuid} | ||||
|       load={load} | ||||
|       errMsg="Failed to load VM information!" | ||||
|       build={() => <VMRouteBody vm={vm!} />} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function VMRouteBody(p: { vm: VMInfo }): React.ReactElement { | ||||
|   const navigate = useNavigate(); | ||||
|  | ||||
|   const [state, setState] = React.useState<VMState | undefined>(); | ||||
|  | ||||
|   return ( | ||||
|     <VirtWebRouteContainer | ||||
|       label={`VM ${p.vm.name}`} | ||||
|       actions={ | ||||
|         <span> | ||||
|           <VMStatusWidget vm={p.vm} onChange={setState} /> | ||||
|  | ||||
|           {(state === "Shutdown" || state === "Shutoff") && ( | ||||
|             <Button | ||||
|               variant="contained" | ||||
|               style={{ marginLeft: "15px" }} | ||||
|               onClick={() => navigate(p.vm.EditURL)} | ||||
|             > | ||||
|               Edit | ||||
|             </Button> | ||||
|           )} | ||||
|         </span> | ||||
|       } | ||||
|     > | ||||
|       <VMDetails vm={p.vm} editable={false} /> | ||||
|     </VirtWebRouteContainer> | ||||
|   ); | ||||
| } | ||||
| @@ -6,15 +6,16 @@ export function CheckboxInput(p: { | ||||
|   checked: boolean | undefined; | ||||
|   onValueChange: (v: boolean) => void; | ||||
| }): React.ReactElement { | ||||
|   if (!p.editable && p.checked) | ||||
|     return <Typography variant="body2">{p.label}</Typography>; | ||||
|   //if (!p.editable && p.checked) | ||||
|   //  return <Typography variant="body2">{p.label}</Typography>; | ||||
|  | ||||
|   if (!p.editable) return <></>; | ||||
|   //if (!p.editable) return <></>; | ||||
|  | ||||
|   return ( | ||||
|     <FormControlLabel | ||||
|       control={ | ||||
|         <Checkbox | ||||
|           disabled={!p.editable} | ||||
|           checked={p.checked} | ||||
|           onChange={(e) => p.onValueChange(e.target.checked)} | ||||
|         /> | ||||
|   | ||||
| @@ -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 <TextInput label={p.label} editable={p.editing} value={value} />; | ||||
|     return <TextInput label={p.label} editable={p.editable} value={value} />; | ||||
|   } | ||||
|   return ( | ||||
|     <FormControl fullWidth variant="standard" style={{ marginBottom: "15px" }}> | ||||
|   | ||||
							
								
								
									
										138
									
								
								virtweb_frontend/src/widgets/vms/VMDetails.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								virtweb_frontend/src/widgets/vms/VMDetails.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -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 ( | ||||
|     <Grid container spacing={2}> | ||||
|       {/* Metadata section */} | ||||
|       <EditSection title="Metadata"> | ||||
|         <TextInput | ||||
|           label="Name" | ||||
|           editable={p.editable} | ||||
|           value={p.vm.name} | ||||
|           onValueChange={(v) => { | ||||
|             p.vm.name = v ?? ""; | ||||
|             p.onChange?.(); | ||||
|           }} | ||||
|           size={ServerApi.Config.constraints.name_size} | ||||
|         /> | ||||
|  | ||||
|         <TextInput label="UUID" editable={false} value={p.vm.uuid} /> | ||||
|  | ||||
|         <TextInput | ||||
|           label="VM genid" | ||||
|           editable={p.editable} | ||||
|           value={p.vm.genid} | ||||
|           onValueChange={(v) => { | ||||
|             p.vm.genid = v; | ||||
|             p.onChange?.(); | ||||
|           }} | ||||
|           checkValue={(v) => validateUUID(v)} | ||||
|         /> | ||||
|  | ||||
|         <TextInput | ||||
|           label="Title" | ||||
|           editable={p.editable} | ||||
|           value={p.vm.title} | ||||
|           onValueChange={(v) => { | ||||
|             p.vm.title = v; | ||||
|             p.onChange?.(); | ||||
|           }} | ||||
|           size={ServerApi.Config.constraints.title_size} | ||||
|         /> | ||||
|  | ||||
|         <TextInput | ||||
|           label="Description" | ||||
|           editable={p.editable} | ||||
|           value={p.vm.description} | ||||
|           onValueChange={(v) => { | ||||
|             p.vm.description = v; | ||||
|             p.onChange?.(); | ||||
|           }} | ||||
|           multiline={true} | ||||
|         /> | ||||
|       </EditSection> | ||||
|  | ||||
|       {/* General section */} | ||||
|       <EditSection title="General"> | ||||
|         <SelectInput | ||||
|           editable={p.editable} | ||||
|           label="CPU Architecture" | ||||
|           onValueChange={(v) => { | ||||
|             p.vm.architecture = v! as any; | ||||
|             p.onChange?.(); | ||||
|           }} | ||||
|           value={p.vm.architecture} | ||||
|           options={[ | ||||
|             { label: "i686", value: "i686" }, | ||||
|             { label: "x86_64", value: "x86_64" }, | ||||
|           ]} | ||||
|         /> | ||||
|  | ||||
|         <SelectInput | ||||
|           editable={p.editable} | ||||
|           label="Boot type" | ||||
|           onValueChange={(v) => { | ||||
|             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" }, | ||||
|           ]} | ||||
|         /> | ||||
|  | ||||
|         <TextInput | ||||
|           label="Memory (MB)" | ||||
|           editable={p.editable} | ||||
|           type="number" | ||||
|           value={p.vm.memory.toString()} | ||||
|           onValueChange={(v) => { | ||||
|             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 | ||||
|           } | ||||
|         /> | ||||
|  | ||||
|         <CheckboxInput | ||||
|           editable={p.editable} | ||||
|           label="Enable VNC access" | ||||
|           checked={p.vm.vnc_access} | ||||
|           onValueChange={(v) => { | ||||
|             p.vm.vnc_access = v; | ||||
|             p.onChange?.(); | ||||
|           }} | ||||
|         /> | ||||
|       </EditSection> | ||||
|     </Grid> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function EditSection( | ||||
|   p: { title: string } & PropsWithChildren | ||||
| ): React.ReactElement { | ||||
|   return ( | ||||
|     <Grid item sm={12} md={6}> | ||||
|       <Paper style={{ margin: "10px", padding: "10px" }}> | ||||
|         <Typography variant="h5" style={{ marginBottom: "15px" }}> | ||||
|           {p.title} | ||||
|         </Typography> | ||||
|         {p.children} | ||||
|       </Paper> | ||||
|     </Grid> | ||||
|   ); | ||||
| } | ||||
| @@ -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={<PlayArrowIcon />} | ||||
|         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={<PlayArrowIcon />} | ||||
|         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={<PauseIcon />} | ||||
|         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={<PowerSettingsNewIcon />} | ||||
|         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={<StopIcon />} | ||||
|         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={<ReplayIcon />} | ||||
|         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} | ||||
|       /> | ||||
|     </div> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user