From b7d44f309126149fd4c460bccc24bdde4ec16a16 Mon Sep 17 00:00:00 2001 From: Pierre Hubert Date: Wed, 6 Dec 2023 15:30:30 +0100 Subject: [PATCH] Can edit more network settings --- .../src/controllers/server_controller.rs | 15 +++- virtweb_backend/src/libvirt_lib_structures.rs | 6 +- .../src/libvirt_rest_structures.rs | 10 ++- virtweb_backend/src/main.rs | 4 + virtweb_frontend/package-lock.json | 68 +++++++++++++++ virtweb_frontend/package.json | 1 + virtweb_frontend/src/api/NetworksApi.ts | 34 ++++++++ virtweb_frontend/src/api/ServerApi.ts | 12 +++ .../src/routes/EditNetworkRoute.tsx | 4 +- .../src/routes/NetworksListRoute.tsx | 5 ++ .../src/routes/ViewNetworkRoute.tsx | 31 +++++-- .../src/widgets/StateActionButton.tsx | 41 +++++++++ .../src/widgets/forms/SelectInput.tsx | 6 +- .../src/widgets/forms/TextInput.tsx | 4 +- .../src/widgets/net/NetworkDetails.tsx | 84 ++++++++++++++++++- .../src/widgets/net/NetworkStatusWidget.tsx | 74 ++++++++++++++++ .../src/widgets/vms/VMStatusWidget.tsx | 62 ++------------ 17 files changed, 384 insertions(+), 77 deletions(-) create mode 100644 virtweb_frontend/src/widgets/StateActionButton.tsx create mode 100644 virtweb_frontend/src/widgets/net/NetworkStatusWidget.tsx diff --git a/virtweb_backend/src/controllers/server_controller.rs b/virtweb_backend/src/controllers/server_controller.rs index 4b9c0d4..84cdb53 100644 --- a/virtweb_backend/src/controllers/server_controller.rs +++ b/virtweb_backend/src/controllers/server_controller.rs @@ -5,7 +5,7 @@ use crate::controllers::{HttpResult, LibVirtReq}; use crate::extractors::local_auth_extractor::LocalAuthEnabled; use crate::libvirt_rest_structures::HypervisorInfo; use actix_web::{HttpResponse, Responder}; -use sysinfo::{System, SystemExt}; +use sysinfo::{NetworksExt, System, SystemExt}; pub async fn root_index() -> impl Responder { HttpResponse::Ok().body("Hello world!") @@ -86,3 +86,16 @@ pub async fn server_info(client: LibVirtReq) -> HttpResult { system, })) } + +pub async fn networks_list() -> HttpResult { + let mut system = System::new(); + system.refresh_networks_list(); + + Ok(HttpResponse::Ok().json( + system + .networks() + .iter() + .map(|n| n.0.to_string()) + .collect::>(), + )) +} diff --git a/virtweb_backend/src/libvirt_lib_structures.rs b/virtweb_backend/src/libvirt_lib_structures.rs index 0e2aece..29ac23f 100644 --- a/virtweb_backend/src/libvirt_lib_structures.rs +++ b/virtweb_backend/src/libvirt_lib_structures.rs @@ -229,10 +229,10 @@ pub struct NetworkForwardXML { pub mode: String, #[serde( default, - rename(serialize = "@mode"), - skip_serializing_if = "Option::is_none" + rename(serialize = "@dev"), + skip_serializing_if = "String::is_empty" )] - pub dev: Option, + pub dev: String, } /// Network DNS information diff --git a/virtweb_backend/src/libvirt_rest_structures.rs b/virtweb_backend/src/libvirt_rest_structures.rs index 139b0da..966429f 100644 --- a/virtweb_backend/src/libvirt_rest_structures.rs +++ b/virtweb_backend/src/libvirt_rest_structures.rs @@ -421,7 +421,7 @@ impl NetworkInfo { forward: match self.forward_mode { NetworkForwardMode::NAT => Some(NetworkForwardXML { mode: "nat".to_string(), - dev: self.device, + dev: self.device.unwrap_or_default(), }), NetworkForwardMode::Isolated => None, }, @@ -443,7 +443,13 @@ impl NetworkInfo { None => NetworkForwardMode::Isolated, Some(_) => NetworkForwardMode::NAT, }, - device: xml.forward.map(|f| f.dev).unwrap_or(None), + device: xml + .forward + .map(|f| match f.dev.is_empty() { + true => None, + false => Some(f.dev), + }) + .unwrap_or(None), dns_server: xml.dns.map(|d| d.forwarder.addr), domain: xml.domain.map(|d| d.name), ip_v4: xml diff --git a/virtweb_backend/src/main.rs b/virtweb_backend/src/main.rs index fcd1a98..78e11f4 100644 --- a/virtweb_backend/src/main.rs +++ b/virtweb_backend/src/main.rs @@ -101,6 +101,10 @@ async fn main() -> std::io::Result<()> { "/api/server/info", web::get().to(server_controller::server_info), ) + .route( + "/api/server/networks", + web::get().to(server_controller::networks_list), + ) // Auth controller .route( "/api/auth/local", diff --git a/virtweb_frontend/package-lock.json b/virtweb_frontend/package-lock.json index cc5dc51..f50c18b 100644 --- a/virtweb_frontend/package-lock.json +++ b/virtweb_frontend/package-lock.json @@ -31,6 +31,7 @@ "mui-file-input": "^3.0.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-imask": "^7.1.3", "react-router-dom": "^6.15.0", "react-scripts": "5.0.1", "react-vnc": "^1.0.0", @@ -1971,6 +1972,19 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/runtime-corejs3": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.23.5.tgz", + "integrity": "sha512-7+ziVclejQTLYhXl+Oi1f6gTGD1XDCeLa4R472TNGQxb08zbEJ0OdNoh5Piz+57Ltmui6xR88BXR4gS3/Toslw==", + "license": "MIT", + "dependencies": { + "core-js-pure": "^3.30.2", + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", @@ -9641,6 +9655,18 @@ "node": ">= 4" } }, + "node_modules/imask": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/imask/-/imask-7.1.3.tgz", + "integrity": "sha512-jZCqTI5Jgukhl2ff+znBQd8BiHOTlnFYCIgggzHYDdoJsHmSSWr1BaejcYBxsjy4ZIs8Rm0HhbOxQcobcdESRQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime-corejs3": "^7.22.6" + }, + "engines": { + "npm": ">=4.0.0" + } + }, "node_modules/immer": { "version": "9.0.21", "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", @@ -15154,6 +15180,22 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" }, + "node_modules/react-imask": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/react-imask/-/react-imask-7.1.3.tgz", + "integrity": "sha512-anCnzdkqpDzNwe7ot76kQSvmnz4Sw7AW/QFjjLh3B87HVNv9e2oHC+1m9hQKSIui2Tqm7w68ooMgDFsCQlDMyg==", + "license": "MIT", + "dependencies": { + "imask": "^7.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "npm": ">=4.0.0" + }, + "peerDependencies": { + "react": ">=0.14.0" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -19550,6 +19592,15 @@ "regenerator-runtime": "^0.14.0" } }, + "@babel/runtime-corejs3": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.23.5.tgz", + "integrity": "sha512-7+ziVclejQTLYhXl+Oi1f6gTGD1XDCeLa4R472TNGQxb08zbEJ0OdNoh5Piz+57Ltmui6xR88BXR4gS3/Toslw==", + "requires": { + "core-js-pure": "^3.30.2", + "regenerator-runtime": "^0.14.0" + } + }, "@babel/template": { "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", @@ -25051,6 +25102,14 @@ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==" }, + "imask": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/imask/-/imask-7.1.3.tgz", + "integrity": "sha512-jZCqTI5Jgukhl2ff+znBQd8BiHOTlnFYCIgggzHYDdoJsHmSSWr1BaejcYBxsjy4ZIs8Rm0HhbOxQcobcdESRQ==", + "requires": { + "@babel/runtime-corejs3": "^7.22.6" + } + }, "immer": { "version": "9.0.21", "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", @@ -28839,6 +28898,15 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" }, + "react-imask": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/react-imask/-/react-imask-7.1.3.tgz", + "integrity": "sha512-anCnzdkqpDzNwe7ot76kQSvmnz4Sw7AW/QFjjLh3B87HVNv9e2oHC+1m9hQKSIui2Tqm7w68ooMgDFsCQlDMyg==", + "requires": { + "imask": "^7.1.3", + "prop-types": "^15.8.1" + } + }, "react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", diff --git a/virtweb_frontend/package.json b/virtweb_frontend/package.json index 2ce6bfc..94221d6 100644 --- a/virtweb_frontend/package.json +++ b/virtweb_frontend/package.json @@ -26,6 +26,7 @@ "mui-file-input": "^3.0.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-imask": "^7.1.3", "react-router-dom": "^6.15.0", "react-scripts": "5.0.1", "react-vnc": "^1.0.0", diff --git a/virtweb_frontend/src/api/NetworksApi.ts b/virtweb_frontend/src/api/NetworksApi.ts index fa2c4f8..41f6b0e 100644 --- a/virtweb_frontend/src/api/NetworksApi.ts +++ b/virtweb_frontend/src/api/NetworksApi.ts @@ -19,6 +19,8 @@ export interface NetworkInfo { ip_v6?: IpConfig; } +export type NetworkStatus = "Started" | "Stopped"; + export function NetworkURL(n: NetworkInfo, edit: boolean = false): string { return `/net/${n.uuid}${edit ? "/edit" : ""}`; } @@ -61,6 +63,38 @@ export class NetworkApi { ).data; } + /** + * Get the status of network + */ + static async GetState(net: NetworkInfo): Promise { + return ( + await APIClient.exec({ + method: "GET", + uri: `/network/${net.uuid}/status`, + }) + ).data.status; + } + + /** + * Start the network + */ + static async Start(net: NetworkInfo): Promise { + await APIClient.exec({ + method: "GET", + uri: `/network/${net.uuid}/start`, + }); + } + + /** + * Stop the network + */ + static async Stop(net: NetworkInfo): Promise { + await APIClient.exec({ + method: "GET", + uri: `/network/${net.uuid}/stop`, + }); + } + /** * Update an existing network */ diff --git a/virtweb_frontend/src/api/ServerApi.ts b/virtweb_frontend/src/api/ServerApi.ts index 351b725..afc541d 100644 --- a/virtweb_frontend/src/api/ServerApi.ts +++ b/virtweb_frontend/src/api/ServerApi.ts @@ -170,4 +170,16 @@ export class ServerApi { }) ).data; } + + /** + * Get host networks card list + */ + static async GetNetworksList(): Promise { + return ( + await APIClient.exec({ + method: "GET", + uri: "/server/networks", + }) + ).data; + } } diff --git a/virtweb_frontend/src/routes/EditNetworkRoute.tsx b/virtweb_frontend/src/routes/EditNetworkRoute.tsx index b84b0b1..c016a7f 100644 --- a/virtweb_frontend/src/routes/EditNetworkRoute.tsx +++ b/virtweb_frontend/src/routes/EditNetworkRoute.tsx @@ -1,5 +1,5 @@ import { useNavigate, useParams } from "react-router-dom"; -import { NetworkApi, NetworkInfo } from "../api/NetworksApi"; +import { NetworkApi, NetworkInfo, NetworkURL } from "../api/NetworksApi"; import { useAlert } from "../hooks/providers/AlertDialogProvider"; import { useSnackbar } from "../hooks/providers/SnackbarProvider"; import React from "react"; @@ -56,7 +56,7 @@ export function EditNetworkRoute(): React.ReactElement { try { await NetworkApi.Update(n); snackbar("The network was successfully updated!"); - navigate(`/net/${network!.uuid}`); + navigate(NetworkURL(network!)); } catch (e) { console.error(e); alert("Failed to update network!"); diff --git a/virtweb_frontend/src/routes/NetworksListRoute.tsx b/virtweb_frontend/src/routes/NetworksListRoute.tsx index fccf43c..8aad823 100644 --- a/virtweb_frontend/src/routes/NetworksListRoute.tsx +++ b/virtweb_frontend/src/routes/NetworksListRoute.tsx @@ -20,6 +20,7 @@ import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; import { useConfirm } from "../hooks/providers/ConfirmDialogProvider"; import { useSnackbar } from "../hooks/providers/SnackbarProvider"; import { useAlert } from "../hooks/providers/AlertDialogProvider"; +import { NetworkStatusWidget } from "../widgets/net/NetworkStatusWidget"; export function NetworksListRoute(): React.ReactElement { const confirm = useConfirm(); @@ -93,6 +94,7 @@ function NetworksListRouteInner(p: { Description Network type IP + State Actions @@ -112,6 +114,9 @@ function NetworksListRouteInner(p: { {t.ip_v4 && "IPv4"} {t.ip_v6 && "IPv6"} + + + diff --git a/virtweb_frontend/src/routes/ViewNetworkRoute.tsx b/virtweb_frontend/src/routes/ViewNetworkRoute.tsx index 3a6e7cc..0517d5f 100644 --- a/virtweb_frontend/src/routes/ViewNetworkRoute.tsx +++ b/virtweb_frontend/src/routes/ViewNetworkRoute.tsx @@ -1,10 +1,16 @@ import React from "react"; -import { NetworkApi, NetworkInfo } from "../api/NetworksApi"; +import { + NetworkApi, + NetworkInfo, + NetworkStatus, + NetworkURL, +} 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"; +import { NetworkStatusWidget } from "../widgets/net/NetworkStatusWidget"; export function ViewNetworkRoute() { const { uuid } = useParams(); @@ -31,18 +37,25 @@ function ViewNetworkRouteInner(p: { }): React.ReactElement { const navigate = useNavigate(); + const [netStatus, setNetStatus] = React.useState(); + return ( navigate(`/net/${p.network.uuid}/edit`)} - > - Edit - + + + + {netStatus === "Stopped" && ( + + )} + } > diff --git a/virtweb_frontend/src/widgets/StateActionButton.tsx b/virtweb_frontend/src/widgets/StateActionButton.tsx new file mode 100644 index 0000000..ed69ba9 --- /dev/null +++ b/virtweb_frontend/src/widgets/StateActionButton.tsx @@ -0,0 +1,41 @@ +import { IconButton, Tooltip } from "@mui/material"; +import { useAlert } from "../hooks/providers/AlertDialogProvider"; +import { useConfirm } from "../hooks/providers/ConfirmDialogProvider"; + +export function StateActionButton(p: { + currState: S; + cond: S[]; + icon: React.ReactElement; + tooltip: string; + confirmMessage?: string; + performAction: () => Promise; + onExecuted: () => void; +}): React.ReactElement { + const confirm = useConfirm(); + const alert = useAlert(); + + if (!p.cond.includes(p.currState)) return <>; + + const performAction = async () => { + try { + if (p.confirmMessage && !(await confirm(p.confirmMessage))) return; + await p.performAction(); + p.onExecuted(); + } catch (e) { + console.error(e); + alert("Failed to perform action! " + e); + } + }; + + return ( + + + {p.icon} + + + ); +} diff --git a/virtweb_frontend/src/widgets/forms/SelectInput.tsx b/virtweb_frontend/src/widgets/forms/SelectInput.tsx index 8cbca78..dc0d78a 100644 --- a/virtweb_frontend/src/widgets/forms/SelectInput.tsx +++ b/virtweb_frontend/src/widgets/forms/SelectInput.tsx @@ -28,7 +28,11 @@ export function SelectInput(p: { onChange={(e) => p.onValueChange(e.target.value)} > {p.options.map((e) => ( - + {e.label} ))} diff --git a/virtweb_frontend/src/widgets/forms/TextInput.tsx b/virtweb_frontend/src/widgets/forms/TextInput.tsx index 6264f5f..ca3cc64 100644 --- a/virtweb_frontend/src/widgets/forms/TextInput.tsx +++ b/virtweb_frontend/src/widgets/forms/TextInput.tsx @@ -34,7 +34,7 @@ export function TextInput(p: { return ( p.onValueChange?.( e.target.value.length === 0 ? undefined : e.target.value @@ -47,7 +47,7 @@ export function TextInput(p: { readOnly: !p.editable, type: p.type, }} - variant={p.editable ? "standard" : "standard"} + variant={"standard"} style={{ width: "100%", marginBottom: "15px" }} multiline={p.multiline} minRows={p.minRows} diff --git a/virtweb_frontend/src/widgets/net/NetworkDetails.tsx b/virtweb_frontend/src/widgets/net/NetworkDetails.tsx index 7f69fd6..41d9f20 100644 --- a/virtweb_frontend/src/widgets/net/NetworkDetails.tsx +++ b/virtweb_frontend/src/widgets/net/NetworkDetails.tsx @@ -3,19 +3,44 @@ import { NetworkInfo } from "../../api/NetworksApi"; import { ServerApi } from "../../api/ServerApi"; import { EditSection } from "../forms/EditSection"; import { TextInput } from "../forms/TextInput"; +import { SelectInput } from "../forms/SelectInput"; +import React from "react"; +import { AsyncWidget } from "../AsyncWidget"; +import { IPv4Input } from "../forms/IPv4Input"; -export function NetworkDetails(p: { +interface DetailsProps { net: NetworkInfo; editable: boolean; onChange?: () => void; -}): React.ReactElement { +} + +export function NetworkDetails(p: DetailsProps): React.ReactElement { + const [cardsList, setCardsList] = React.useState(); + + const load = async () => { + setCardsList(await ServerApi.GetNetworksList()); + }; + + return ( + } + /> + ); +} + +function NetworkDetailsInner( + p: DetailsProps & { cardsList: string[] } +): React.ReactElement { return ( {/* Metadata section */} { p.net.name = v ?? ""; @@ -49,7 +74,58 @@ export function NetworkDetails(p: { multiline={true} /> - TODO:continue + {/* TODO : autostart */} + + + { + p.net.forward_mode = v as any; + p.onChange?.(); + }} + value={p.net.forward_mode} + options={[ + { + label: "NAT", + value: "NAT", + }, + + { + label: "Isolated network", + value: "Isolated", + }, + ]} + /> + + {p.net.forward_mode === "NAT" && ( + { + p.net.device = v; + p.onChange?.(); + }} + value={p.net.device} + options={[ + { label: "Default interface", value: undefined }, + ...p.cardsList.map((d) => { + return { label: d, value: d }; + }), + ]} + /> + )} + + { + p.net.dns_server = v; + p.onChange?.(); + }} + /> + ); } diff --git a/virtweb_frontend/src/widgets/net/NetworkStatusWidget.tsx b/virtweb_frontend/src/widgets/net/NetworkStatusWidget.tsx new file mode 100644 index 0000000..0cdc0dd --- /dev/null +++ b/virtweb_frontend/src/widgets/net/NetworkStatusWidget.tsx @@ -0,0 +1,74 @@ +import PlayArrowIcon from "@mui/icons-material/PlayArrow"; +import StopIcon from "@mui/icons-material/Stop"; +import { CircularProgress, Typography } from "@mui/material"; +import React from "react"; +import { + NetworkApi, + NetworkInfo, + NetworkStatus as NetworkState, +} from "../../api/NetworksApi"; +import { useSnackbar } from "../../hooks/providers/SnackbarProvider"; +import { StateActionButton } from "../StateActionButton"; + +export function NetworkStatusWidget(p: { + net: NetworkInfo; + onChange?: (s: NetworkState) => void; +}): React.ReactElement { + const snackbar = useSnackbar(); + + const [state, setState] = React.useState(); + + const refresh = async () => { + try { + const s = await NetworkApi.GetState(p.net); + if (s !== state) p.onChange?.(s); + setState(s); + } catch (e) { + console.error(e); + snackbar("Failed to refresh network status!"); + } + }; + + const changedAction = () => setState(undefined); + + React.useEffect(() => { + refresh(); + const i = setInterval(() => refresh(), 3000); + + return () => clearInterval(i); + }); + + if (state === undefined) + return ( + <> + + + ); + + return ( +
+ {state} + + {/* Start Network */} + } + tooltip="Start the Network" + performAction={() => NetworkApi.Start(p.net)} + onExecuted={changedAction} + /> + + {/* Stop network */} + } + tooltip="Stop the network" + confirmMessage="Do you really want to kill stop this network?" + performAction={() => NetworkApi.Stop(p.net)} + onExecuted={changedAction} + /> +
+ ); +} diff --git a/virtweb_frontend/src/widgets/vms/VMStatusWidget.tsx b/virtweb_frontend/src/widgets/vms/VMStatusWidget.tsx index 4d5ebb5..20a94ca 100644 --- a/virtweb_frontend/src/widgets/vms/VMStatusWidget.tsx +++ b/virtweb_frontend/src/widgets/vms/VMStatusWidget.tsx @@ -4,18 +4,12 @@ import PlayArrowIcon from "@mui/icons-material/PlayArrow"; import PowerSettingsNewIcon from "@mui/icons-material/PowerSettingsNew"; import ReplayIcon from "@mui/icons-material/Replay"; import StopIcon from "@mui/icons-material/Stop"; -import { - CircularProgress, - IconButton, - Tooltip, - Typography, -} from "@mui/material"; +import { CircularProgress, Typography } from "@mui/material"; import React from "react"; import { useNavigate } from "react-router-dom"; import { VMApi, VMInfo, VMState } from "../../api/VMApi"; -import { useAlert } from "../../hooks/providers/AlertDialogProvider"; -import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider"; import { useSnackbar } from "../../hooks/providers/SnackbarProvider"; +import { StateActionButton } from "../StateActionButton"; export function VMStatusWidget(p: { vm: VMInfo; @@ -59,7 +53,7 @@ export function VMStatusWidget(p: { { /* VNC console */ p.vm.vnc_access && ( - } @@ -71,7 +65,7 @@ export function VMStatusWidget(p: { } {/* Start VM */} - } @@ -81,7 +75,7 @@ export function VMStatusWidget(p: { /> {/* Resume VM */} - } @@ -91,7 +85,7 @@ export function VMStatusWidget(p: { /> {/* Suspend VM */} - } @@ -102,7 +96,7 @@ export function VMStatusWidget(p: { /> {/* Shutdown VM */} - } @@ -113,7 +107,7 @@ export function VMStatusWidget(p: { /> {/* Kill VM */} - } @@ -124,7 +118,7 @@ export function VMStatusWidget(p: { /> {/* Reset VM */} - } @@ -136,41 +130,3 @@ export function VMStatusWidget(p: { ); } - -function ActionButton(p: { - currState: VMState; - cond: VMState[]; - icon: React.ReactElement; - tooltip: string; - confirmMessage?: string; - performAction: () => Promise; - onExecuted: () => void; -}): React.ReactElement { - const confirm = useConfirm(); - const alert = useAlert(); - - if (!p.cond.includes(p.currState)) return <>; - - const performAction = async () => { - try { - if (p.confirmMessage && !(await confirm(p.confirmMessage))) return; - await p.performAction(); - p.onExecuted(); - } catch (e) { - console.error(e); - alert("Failed to perform action! " + e); - } - }; - - return ( - - - {p.icon} - - - ); -}