Can export VM config from UI
This commit is contained in:
		| @@ -5,6 +5,7 @@ import { VMApi, VMInfo } from "../api/VMApi"; | ||||
| import { useAlert } from "../hooks/providers/AlertDialogProvider"; | ||||
| import { useSnackbar } from "../hooks/providers/SnackbarProvider"; | ||||
| import { AsyncWidget } from "../widgets/AsyncWidget"; | ||||
| import { ConfigImportExportButtons } from "../widgets/ConfigImportExportButtons"; | ||||
| import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; | ||||
| import { VMDetails } from "../widgets/vms/VMDetails"; | ||||
|  | ||||
| @@ -13,7 +14,7 @@ export function CreateVMRoute(): React.ReactElement { | ||||
|   const alert = useAlert(); | ||||
|   const navigate = useNavigate(); | ||||
|  | ||||
|   const [vm] = React.useState(VMInfo.NewEmpty); | ||||
|   const [vm, setVM] = React.useState(VMInfo.NewEmpty); | ||||
|  | ||||
|   const create = async (v: VMInfo) => { | ||||
|     try { | ||||
| @@ -30,6 +31,7 @@ export function CreateVMRoute(): React.ReactElement { | ||||
|   return ( | ||||
|     <EditVMInner | ||||
|       vm={vm} | ||||
|       onReplace={setVM} | ||||
|       isCreating={true} | ||||
|       onSave={create} | ||||
|       onCancel={() => navigate("/vms")} | ||||
| @@ -76,6 +78,7 @@ export function EditVMRoute(): React.ReactElement { | ||||
|       build={() => ( | ||||
|         <EditVMInner | ||||
|           vm={vm!} | ||||
|           onReplace={setVM} | ||||
|           isCreating={false} | ||||
|           onCancel={() => { | ||||
|             navigate(vm!.ViewURL); | ||||
| @@ -92,6 +95,7 @@ function EditVMInner(p: { | ||||
|   isCreating: boolean; | ||||
|   onCancel: () => void; | ||||
|   onSave: (vm: VMInfo) => Promise<void>; | ||||
|   onReplace: (vm: VMInfo) => void; | ||||
| }): React.ReactElement { | ||||
|   const [changed, setChanged] = React.useState(false); | ||||
|  | ||||
| @@ -107,6 +111,14 @@ function EditVMInner(p: { | ||||
|       label={p.isCreating ? "Create a Virtual Machine" : "Edit Virtual Machine"} | ||||
|       actions={ | ||||
|         <span> | ||||
|           <ConfigImportExportButtons | ||||
|             filename={`vm-${p.vm.name}.json`} | ||||
|             currentConf={p.vm} | ||||
|             importConf={(conf) => { | ||||
|               p.onReplace(new VMInfo(conf)); | ||||
|               valueChanged(); | ||||
|             }} | ||||
|           /> | ||||
|           {changed && ( | ||||
|             <Button | ||||
|               variant="contained" | ||||
|   | ||||
| @@ -1,14 +1,15 @@ | ||||
| import { mdiXml } from "@mdi/js"; | ||||
| import Icon from "@mdi/react"; | ||||
| import { Button, IconButton, Tooltip } from "@mui/material"; | ||||
| import React from "react"; | ||||
| import { useNavigate, useParams } from "react-router-dom"; | ||||
| import { VMApi, VMInfo, VMState } from "../api/VMApi"; | ||||
| import React from "react"; | ||||
| import { AsyncWidget } from "../widgets/AsyncWidget"; | ||||
| import { ConfigImportExportButtons } from "../widgets/ConfigImportExportButtons"; | ||||
| import { RouterLink } from "../widgets/RouterLink"; | ||||
| import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; | ||||
| import { VMDetails } from "../widgets/vms/VMDetails"; | ||||
| import { VMStatusWidget } from "../widgets/vms/VMStatusWidget"; | ||||
| import { Button, IconButton } from "@mui/material"; | ||||
| import Icon from "@mdi/react"; | ||||
| import { mdiXml } from "@mdi/js"; | ||||
| import { RouterLink } from "../widgets/RouterLink"; | ||||
|  | ||||
| export function VMRoute(): React.ReactElement { | ||||
|   const { uuid } = useParams(); | ||||
| @@ -42,11 +43,18 @@ function VMRouteBody(p: { vm: VMInfo }): React.ReactElement { | ||||
|           <VMStatusWidget vm={p.vm} onChange={setState} /> | ||||
|  | ||||
|           <RouterLink to={p.vm.XMLURL}> | ||||
|             <IconButton size="small"> | ||||
|               <Icon path={mdiXml} style={{ width: "1em" }} /> | ||||
|             </IconButton> | ||||
|             <Tooltip title="View domain definition"> | ||||
|               <IconButton size="small"> | ||||
|                 <Icon path={mdiXml} style={{ width: "1em" }} /> | ||||
|               </IconButton> | ||||
|             </Tooltip> | ||||
|           </RouterLink> | ||||
|  | ||||
|           <ConfigImportExportButtons | ||||
|             filename={`vm-${p.vm.name}.json`} | ||||
|             currentConf={p.vm} | ||||
|           /> | ||||
|  | ||||
|           {(state === "Shutdown" || state === "Shutoff") && ( | ||||
|             <Button | ||||
|               variant="contained" | ||||
|   | ||||
							
								
								
									
										74
									
								
								virtweb_frontend/src/widgets/ConfigImportExportButtons.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								virtweb_frontend/src/widgets/ConfigImportExportButtons.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,74 @@ | ||||
| import FolderOpenIcon from "@mui/icons-material/FolderOpen"; | ||||
| import IosShareIcon from "@mui/icons-material/IosShare"; | ||||
| import { IconButton, Tooltip } from "@mui/material"; | ||||
| import { useAlert } from "../hooks/providers/AlertDialogProvider"; | ||||
|  | ||||
| export function ConfigImportExportButtons(p: { | ||||
|   filename: string; | ||||
|   currentConf: any; | ||||
|   importConf?: (content: any) => any; | ||||
| }): React.ReactElement { | ||||
|   const alert = useAlert(); | ||||
|  | ||||
|   const exportConf = () => { | ||||
|     const conf = JSON.stringify(p.currentConf); | ||||
|     const blob = new Blob([conf], { type: "application/json" }); | ||||
|  | ||||
|     const a = document.createElement("a"); | ||||
|     a.href = window.URL.createObjectURL(blob); | ||||
|     a.download = p.filename; | ||||
|     document.body.appendChild(a); | ||||
|     a.click(); | ||||
|     document.body.removeChild(a); | ||||
|   }; | ||||
|  | ||||
|   const importConf = async () => { | ||||
|     try { | ||||
|       // Create file element | ||||
|       const fileEl = document.createElement("input"); | ||||
|       fileEl.type = "file"; | ||||
|       fileEl.accept = "application/json"; | ||||
|       fileEl.click(); | ||||
|  | ||||
|       // Wait for a file to be chosen | ||||
|       await new Promise((res, _rej) => | ||||
|         fileEl.addEventListener("change", () => res(null)) | ||||
|       ); | ||||
|  | ||||
|       if ((fileEl.files?.length ?? 0) === 0) return null; | ||||
|  | ||||
|       // Import conf | ||||
|       let file = fileEl.files![0]; | ||||
|       const content = await file.text(); | ||||
|       p.importConf?.(JSON.parse(content)); | ||||
|     } catch (e) { | ||||
|       console.error(e); | ||||
|       alert(`Failed to load config from file!\n${e}`); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <Tooltip title={"Export current config"}> | ||||
|         <IconButton | ||||
|           onClick={exportConf} | ||||
|           size="small" | ||||
|           style={{ paddingBottom: "0px", paddingTop: "0px" }} | ||||
|         > | ||||
|           <IosShareIcon /> | ||||
|         </IconButton> | ||||
|       </Tooltip> | ||||
|       {p.importConf && ( | ||||
|         <Tooltip title={"Import config from file"}> | ||||
|           <IconButton | ||||
|             onClick={importConf} | ||||
|             size="small" | ||||
|             style={{ paddingBottom: "0px", paddingTop: "0px" }} | ||||
|           > | ||||
|             <FolderOpenIcon /> | ||||
|           </IconButton> | ||||
|         </Tooltip> | ||||
|       )} | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
		Reference in New Issue
	
	Block a user