Add groups support #146
							
								
								
									
										79
									
								
								remote_backend/src/controllers/group_controller.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								remote_backend/src/controllers/group_controller.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,79 @@ | ||||
| use crate::controllers::HttpResult; | ||||
| use crate::virtweb_client; | ||||
| use crate::virtweb_client::{GroupID, VMUuid}; | ||||
| use actix_web::{web, HttpResponse}; | ||||
|  | ||||
| #[derive(serde::Deserialize)] | ||||
| pub struct GroupIDInPath { | ||||
|     gid: GroupID, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Deserialize)] | ||||
| pub struct VMIDInQuery { | ||||
|     vm_id: Option<VMUuid>, | ||||
| } | ||||
|  | ||||
| /// Get the state of one or all VM | ||||
| pub async fn vm_state( | ||||
|     path: web::Path<GroupIDInPath>, | ||||
|     query: web::Query<VMIDInQuery>, | ||||
| ) -> HttpResult { | ||||
|     Ok(HttpResponse::Ok().json(virtweb_client::group_vm_state(&path.gid, query.vm_id).await?)) | ||||
| } | ||||
|  | ||||
| /// Start one or all VM | ||||
| pub async fn vm_start( | ||||
|     path: web::Path<GroupIDInPath>, | ||||
|     query: web::Query<VMIDInQuery>, | ||||
| ) -> HttpResult { | ||||
|     Ok(HttpResponse::Ok().json(virtweb_client::group_vm_start(&path.gid, query.vm_id).await?)) | ||||
| } | ||||
|  | ||||
| /// Shutdown one or all VM | ||||
| pub async fn vm_shutdown( | ||||
|     path: web::Path<GroupIDInPath>, | ||||
|     query: web::Query<VMIDInQuery>, | ||||
| ) -> HttpResult { | ||||
|     Ok(HttpResponse::Ok().json(virtweb_client::group_vm_shutdown(&path.gid, query.vm_id).await?)) | ||||
| } | ||||
|  | ||||
| /// Kill one or all VM | ||||
| pub async fn vm_kill(path: web::Path<GroupIDInPath>, query: web::Query<VMIDInQuery>) -> HttpResult { | ||||
|     Ok(HttpResponse::Ok().json(virtweb_client::group_vm_kill(&path.gid, query.vm_id).await?)) | ||||
| } | ||||
|  | ||||
| /// Reset one or all VM | ||||
| pub async fn vm_reset( | ||||
|     path: web::Path<GroupIDInPath>, | ||||
|     query: web::Query<VMIDInQuery>, | ||||
| ) -> HttpResult { | ||||
|     Ok(HttpResponse::Ok().json(virtweb_client::group_vm_reset(&path.gid, query.vm_id).await?)) | ||||
| } | ||||
|  | ||||
| /// Suspend one or all VM | ||||
| pub async fn vm_suspend( | ||||
|     path: web::Path<GroupIDInPath>, | ||||
|     query: web::Query<VMIDInQuery>, | ||||
| ) -> HttpResult { | ||||
|     Ok(HttpResponse::Ok().json(virtweb_client::group_vm_suspend(&path.gid, query.vm_id).await?)) | ||||
| } | ||||
|  | ||||
| /// Resume one or all VM | ||||
| pub async fn vm_resume( | ||||
|     path: web::Path<GroupIDInPath>, | ||||
|     query: web::Query<VMIDInQuery>, | ||||
| ) -> HttpResult { | ||||
|     Ok(HttpResponse::Ok().json(virtweb_client::group_vm_resume(&path.gid, query.vm_id).await?)) | ||||
| } | ||||
|  | ||||
| /// Screenshot one or all VM | ||||
| pub async fn vm_screenshot( | ||||
|     path: web::Path<GroupIDInPath>, | ||||
|     query: web::Query<VMIDInQuery>, | ||||
| ) -> HttpResult { | ||||
|     let screenshot = virtweb_client::group_vm_screenshot(&path.gid, query.vm_id).await?; | ||||
|  | ||||
|     Ok(HttpResponse::Ok() | ||||
|         .insert_header(("content-type", "image/png")) | ||||
|         .body(screenshot)) | ||||
| } | ||||
| @@ -6,6 +6,7 @@ use std::fmt::{Display, Formatter}; | ||||
| use std::io::ErrorKind; | ||||
|  | ||||
| pub mod auth_controller; | ||||
| pub mod group_controller; | ||||
| pub mod server_controller; | ||||
| pub mod static_controller; | ||||
| pub mod sys_info_controller; | ||||
|   | ||||
| @@ -2,7 +2,7 @@ use crate::app_config::AppConfig; | ||||
| use crate::controllers::HttpResult; | ||||
| use crate::extractors::auth_extractor::AuthExtractor; | ||||
| use crate::virtweb_client; | ||||
| use crate::virtweb_client::VMUuid; | ||||
| use crate::virtweb_client::{GroupID, VMCaps, VMInfo}; | ||||
| use actix_web::HttpResponse; | ||||
|  | ||||
| #[derive(serde::Serialize)] | ||||
| @@ -20,54 +20,70 @@ pub async fn config(auth: AuthExtractor) -> HttpResult { | ||||
|  | ||||
| #[derive(Default, Debug, serde::Serialize)] | ||||
| pub struct Rights { | ||||
|     groups: Vec<GroupInfo>, | ||||
|     vms: Vec<VMInfoAndCaps>, | ||||
|     sys_info: bool, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, serde::Serialize)] | ||||
| pub struct GroupInfo { | ||||
|     id: GroupID, | ||||
|     vms: Vec<VMInfo>, | ||||
|     #[serde(flatten)] | ||||
|     caps: VMCaps, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, serde::Serialize)] | ||||
| pub struct VMInfoAndCaps { | ||||
|     uiid: VMUuid, | ||||
|     name: String, | ||||
|     description: Option<String>, | ||||
|     architecture: String, | ||||
|     memory: usize, | ||||
|     number_vcpu: usize, | ||||
|     can_get_state: bool, | ||||
|     can_start: bool, | ||||
|     can_shutdown: bool, | ||||
|     can_kill: bool, | ||||
|     can_reset: bool, | ||||
|     can_suspend: bool, | ||||
|     can_resume: bool, | ||||
|     can_screenshot: bool, | ||||
|     #[serde(flatten)] | ||||
|     info: VMInfo, | ||||
|     #[serde(flatten)] | ||||
|     caps: VMCaps, | ||||
| } | ||||
|  | ||||
| pub async fn rights() -> HttpResult { | ||||
|     let rights = virtweb_client::get_token_info().await?; | ||||
|  | ||||
|     let mut res = Rights { | ||||
|         groups: vec![], | ||||
|         vms: vec![], | ||||
|         sys_info: rights.can_retrieve_system_info(), | ||||
|     }; | ||||
|  | ||||
|     for g in rights.list_groups() { | ||||
|         let group_vms = virtweb_client::group_vm_info(&g).await?; | ||||
|  | ||||
|         res.groups.push(GroupInfo { | ||||
|             id: g.clone(), | ||||
|             vms: group_vms, | ||||
|             caps: VMCaps { | ||||
|                 can_get_state: rights.is_route_allowed("GET", &g.route_vm_state(None)), | ||||
|                 can_start: rights.is_route_allowed("GET", &g.route_vm_start(None)), | ||||
|                 can_shutdown: rights.is_route_allowed("GET", &g.route_vm_shutdown(None)), | ||||
|                 can_kill: rights.is_route_allowed("GET", &g.route_vm_kill(None)), | ||||
|                 can_reset: rights.is_route_allowed("GET", &g.route_vm_reset(None)), | ||||
|                 can_suspend: rights.is_route_allowed("GET", &g.route_vm_suspend(None)), | ||||
|                 can_resume: rights.is_route_allowed("GET", &g.route_vm_resume(None)), | ||||
|                 can_screenshot: rights.is_route_allowed("GET", &g.route_vm_screenshot(None)), | ||||
|             }, | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     for v in rights.list_vm() { | ||||
|         let vm_info = virtweb_client::vm_info(v).await?; | ||||
|  | ||||
|         res.vms.push(VMInfoAndCaps { | ||||
|             uiid: vm_info.uuid, | ||||
|             name: vm_info.name, | ||||
|             description: vm_info.description.clone(), | ||||
|             architecture: vm_info.architecture.to_string(), | ||||
|             memory: vm_info.memory, | ||||
|             number_vcpu: vm_info.number_vcpu, | ||||
|             can_get_state: rights.is_route_allowed("GET", &v.route_state()), | ||||
|             can_start: rights.is_route_allowed("GET", &v.route_start()), | ||||
|             can_shutdown: rights.is_route_allowed("GET", &v.route_shutdown()), | ||||
|             can_kill: rights.is_route_allowed("GET", &v.route_kill()), | ||||
|             can_reset: rights.is_route_allowed("GET", &v.route_reset()), | ||||
|             can_suspend: rights.is_route_allowed("GET", &v.route_suspend()), | ||||
|             can_resume: rights.is_route_allowed("GET", &v.route_resume()), | ||||
|             can_screenshot: rights.is_route_allowed("GET", &v.route_screenshot()), | ||||
|             info: vm_info, | ||||
|             caps: VMCaps { | ||||
|                 can_get_state: rights.is_route_allowed("GET", &v.route_state()), | ||||
|                 can_start: rights.is_route_allowed("GET", &v.route_start()), | ||||
|                 can_shutdown: rights.is_route_allowed("GET", &v.route_shutdown()), | ||||
|                 can_kill: rights.is_route_allowed("GET", &v.route_kill()), | ||||
|                 can_reset: rights.is_route_allowed("GET", &v.route_reset()), | ||||
|                 can_suspend: rights.is_route_allowed("GET", &v.route_suspend()), | ||||
|                 can_resume: rights.is_route_allowed("GET", &v.route_resume()), | ||||
|                 can_screenshot: rights.is_route_allowed("GET", &v.route_screenshot()), | ||||
|             }, | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -12,7 +12,8 @@ use light_openid::basic_state_manager::BasicStateManager; | ||||
| use remote_backend::app_config::AppConfig; | ||||
| use remote_backend::constants; | ||||
| use remote_backend::controllers::{ | ||||
|     auth_controller, server_controller, static_controller, sys_info_controller, vm_controller, | ||||
|     auth_controller, group_controller, server_controller, static_controller, sys_info_controller, | ||||
|     vm_controller, | ||||
| }; | ||||
| use remote_backend::middlewares::auth_middleware::AuthChecker; | ||||
| use std::time::Duration; | ||||
| @@ -86,6 +87,39 @@ async fn main() -> std::io::Result<()> { | ||||
|                 "/api/server/rights", | ||||
|                 web::get().to(server_controller::rights), | ||||
|             ) | ||||
|             // Groups routes | ||||
|             .route( | ||||
|                 "/api/group/{gid}/vm/state", | ||||
|                 web::get().to(group_controller::vm_state), | ||||
|             ) | ||||
|             .route( | ||||
|                 "/api/group/{gid}/vm/start", | ||||
|                 web::get().to(group_controller::vm_start), | ||||
|             ) | ||||
|             .route( | ||||
|                 "/api/group/{gid}/vm/shutdown", | ||||
|                 web::get().to(group_controller::vm_shutdown), | ||||
|             ) | ||||
|             .route( | ||||
|                 "/api/group/{gid}/vm/kill", | ||||
|                 web::get().to(group_controller::vm_kill), | ||||
|             ) | ||||
|             .route( | ||||
|                 "/api/group/{gid}/vm/reset", | ||||
|                 web::get().to(group_controller::vm_reset), | ||||
|             ) | ||||
|             .route( | ||||
|                 "/api/group/{gid}/vm/suspend", | ||||
|                 web::get().to(group_controller::vm_suspend), | ||||
|             ) | ||||
|             .route( | ||||
|                 "/api/group/{gid}/vm/resume", | ||||
|                 web::get().to(group_controller::vm_resume), | ||||
|             ) | ||||
|             .route( | ||||
|                 "/api/group/{gid}/vm/screenshot", | ||||
|                 web::get().to(group_controller::vm_screenshot), | ||||
|             ) | ||||
|             // VM routes | ||||
|             .route("/api/vm/{uid}/state", web::get().to(vm_controller::state)) | ||||
|             .route("/api/vm/{uid}/start", web::get().to(vm_controller::start)) | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| use crate::app_config::AppConfig; | ||||
| use crate::utils::time; | ||||
| use lazy_regex::regex; | ||||
| use std::collections::HashMap; | ||||
| use std::fmt::Display; | ||||
| use std::str::FromStr; | ||||
| use thiserror::Error; | ||||
| @@ -12,9 +13,105 @@ pub enum VirtWebClientError { | ||||
|     InvalidStatusCode(u16), | ||||
| } | ||||
|  | ||||
| #[derive(Eq, PartialEq, Debug, Copy, Clone, serde::Serialize, serde::Deserialize)] | ||||
| #[derive(Eq, PartialEq, Debug, Clone, serde::Serialize, serde::Deserialize)] | ||||
| pub struct GroupID(String); | ||||
|  | ||||
| impl GroupID { | ||||
|     pub fn route_vm_info(&self) -> String { | ||||
|         format!("/api/group/{}/vm/info", self.0) | ||||
|     } | ||||
|  | ||||
|     pub fn route_vm_state(&self, vm: Option<VMUuid>) -> String { | ||||
|         format!( | ||||
|             "/api/group/{}/vm/state{}", | ||||
|             self.0, | ||||
|             match vm { | ||||
|                 None => "".to_string(), | ||||
|                 Some(id) => format!("?vm_id={}", id.0), | ||||
|             } | ||||
|         ) | ||||
|     } | ||||
|     pub fn route_vm_start(&self, vm: Option<VMUuid>) -> String { | ||||
|         format!( | ||||
|             "/api/group/{}/vm/start{}", | ||||
|             self.0, | ||||
|             match vm { | ||||
|                 None => "".to_string(), | ||||
|                 Some(id) => format!("?vm_id={}", id.0), | ||||
|             } | ||||
|         ) | ||||
|     } | ||||
|     pub fn route_vm_shutdown(&self, vm: Option<VMUuid>) -> String { | ||||
|         format!( | ||||
|             "/api/group/{}/vm/shutdown{}", | ||||
|             self.0, | ||||
|             match vm { | ||||
|                 None => "".to_string(), | ||||
|                 Some(id) => format!("?vm_id={}", id.0), | ||||
|             } | ||||
|         ) | ||||
|     } | ||||
|     pub fn route_vm_suspend(&self, vm: Option<VMUuid>) -> String { | ||||
|         format!( | ||||
|             "/api/group/{}/vm/suspend{}", | ||||
|             self.0, | ||||
|             match vm { | ||||
|                 None => "".to_string(), | ||||
|                 Some(id) => format!("?vm_id={}", id.0), | ||||
|             } | ||||
|         ) | ||||
|     } | ||||
|     pub fn route_vm_resume(&self, vm: Option<VMUuid>) -> String { | ||||
|         format!( | ||||
|             "/api/group/{}/vm/resume{}", | ||||
|             self.0, | ||||
|             match vm { | ||||
|                 None => "".to_string(), | ||||
|                 Some(id) => format!("?vm_id={}", id.0), | ||||
|             } | ||||
|         ) | ||||
|     } | ||||
|     pub fn route_vm_kill(&self, vm: Option<VMUuid>) -> String { | ||||
|         format!( | ||||
|             "/api/group/{}/vm/kill{}", | ||||
|             self.0, | ||||
|             match vm { | ||||
|                 None => "".to_string(), | ||||
|                 Some(id) => format!("?vm_id={}", id.0), | ||||
|             } | ||||
|         ) | ||||
|     } | ||||
|     pub fn route_vm_reset(&self, vm: Option<VMUuid>) -> String { | ||||
|         format!( | ||||
|             "/api/group/{}/vm/reset{}", | ||||
|             self.0, | ||||
|             match vm { | ||||
|                 None => "".to_string(), | ||||
|                 Some(id) => format!("?vm_id={}", id.0), | ||||
|             } | ||||
|         ) | ||||
|     } | ||||
|     pub fn route_vm_screenshot(&self, vm: Option<VMUuid>) -> String { | ||||
|         format!( | ||||
|             "/api/group/{}/vm/screenshot{}", | ||||
|             self.0, | ||||
|             match vm { | ||||
|                 None => "".to_string(), | ||||
|                 Some(id) => format!("?vm_id={}", id.0), | ||||
|             } | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Eq, PartialEq, Debug, Copy, Clone, serde::Serialize, serde::Deserialize, Hash)] | ||||
| pub struct VMUuid(Uuid); | ||||
|  | ||||
| #[derive(Default, serde::Deserialize, serde::Serialize)] | ||||
| pub struct TreatmentResult { | ||||
|     ok: usize, | ||||
|     failed: usize, | ||||
| } | ||||
|  | ||||
| impl VMUuid { | ||||
|     pub fn route_info(&self) -> String { | ||||
|         format!("/api/vm/{}", self.0) | ||||
| @@ -69,7 +166,7 @@ pub struct TokenClaims { | ||||
|     pub nonce: String, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Deserialize, Debug)] | ||||
| #[derive(serde::Deserialize, serde::Serialize, Debug)] | ||||
| pub struct VMInfo { | ||||
|     pub uuid: VMUuid, | ||||
|     pub name: String, | ||||
| @@ -79,6 +176,18 @@ pub struct VMInfo { | ||||
|     pub number_vcpu: usize, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Deserialize, serde::Serialize, Debug)] | ||||
| pub struct VMCaps { | ||||
|     pub can_get_state: bool, | ||||
|     pub can_start: bool, | ||||
|     pub can_shutdown: bool, | ||||
|     pub can_kill: bool, | ||||
|     pub can_reset: bool, | ||||
|     pub can_suspend: bool, | ||||
|     pub can_resume: bool, | ||||
|     pub can_screenshot: bool, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Deserialize, serde::Serialize, Debug)] | ||||
| pub struct VMState { | ||||
|     pub state: String, | ||||
| @@ -147,6 +256,16 @@ impl TokenInfo { | ||||
|         false | ||||
|     } | ||||
|  | ||||
|     /// List the groups with access | ||||
|     pub fn list_groups(&self) -> Vec<GroupID> { | ||||
|         self.rights | ||||
|             .iter() | ||||
|             .filter(|r| r.verb == "GET") | ||||
|             .filter(|r| regex!("^/api/group/[^/]+/vm/info$").is_match(&r.path)) | ||||
|             .map(|r| GroupID(r.path.split("/").nth(3).unwrap().to_string())) | ||||
|             .collect::<Vec<_>>() | ||||
|     } | ||||
|  | ||||
|     /// List the virtual machines with access | ||||
|     pub fn list_vm(&self) -> Vec<VMUuid> { | ||||
|         self.rights | ||||
| @@ -168,12 +287,13 @@ async fn request<D: Display>(uri: D) -> anyhow::Result<reqwest::Response> { | ||||
|     let url = format!("{}{}", AppConfig::get().virtweb_base_url, uri); | ||||
|     log::debug!("Will query {uri}..."); | ||||
|  | ||||
|     let uri = uri.to_string(); | ||||
|     let jwt = TokenClaims { | ||||
|         sub: AppConfig::get().virtweb_token_id.to_string(), | ||||
|         iat: time() - 60 * 2, | ||||
|         exp: time() + 60 * 3, | ||||
|         verb: "GET".to_string(), | ||||
|         path: uri.to_string(), | ||||
|         path: uri.split_once('?').map(|s| s.0).unwrap_or(&uri).to_string(), | ||||
|         nonce: Uuid::new_v4().to_string(), | ||||
|     }; | ||||
|     let jwt = AppConfig::get().token_private_key().sign_jwt(&jwt)?; | ||||
| @@ -260,6 +380,73 @@ pub async fn vm_screenshot(id: VMUuid) -> anyhow::Result<Vec<u8>> { | ||||
|         .to_vec()) | ||||
| } | ||||
|  | ||||
| /// Get the VM of a group | ||||
| pub async fn group_vm_info(id: &GroupID) -> anyhow::Result<Vec<VMInfo>> { | ||||
|     json_request(id.route_vm_info()).await | ||||
| } | ||||
|  | ||||
| /// Get the state of one or all VMs of a group | ||||
| pub async fn group_vm_state( | ||||
|     id: &GroupID, | ||||
|     vm_id: Option<VMUuid>, | ||||
| ) -> anyhow::Result<HashMap<VMUuid, String>> { | ||||
|     json_request(id.route_vm_state(vm_id)).await | ||||
| } | ||||
|  | ||||
| /// Start one or all VMs of a group | ||||
| pub async fn group_vm_start( | ||||
|     id: &GroupID, | ||||
|     vm_id: Option<VMUuid>, | ||||
| ) -> anyhow::Result<TreatmentResult> { | ||||
|     json_request(id.route_vm_start(vm_id)).await | ||||
| } | ||||
|  | ||||
| /// Shutdown one or all VMs of a group | ||||
| pub async fn group_vm_shutdown( | ||||
|     id: &GroupID, | ||||
|     vm_id: Option<VMUuid>, | ||||
| ) -> anyhow::Result<TreatmentResult> { | ||||
|     json_request(id.route_vm_shutdown(vm_id)).await | ||||
| } | ||||
|  | ||||
| /// Kill one or all VMs of a group | ||||
| pub async fn group_vm_kill(id: &GroupID, vm_id: Option<VMUuid>) -> anyhow::Result<TreatmentResult> { | ||||
|     json_request(id.route_vm_kill(vm_id)).await | ||||
| } | ||||
|  | ||||
| /// Reset one or all VMs of a group | ||||
| pub async fn group_vm_reset( | ||||
|     id: &GroupID, | ||||
|     vm_id: Option<VMUuid>, | ||||
| ) -> anyhow::Result<TreatmentResult> { | ||||
|     json_request(id.route_vm_reset(vm_id)).await | ||||
| } | ||||
|  | ||||
| /// Suspend one or all VMs of a group | ||||
| pub async fn group_vm_suspend( | ||||
|     id: &GroupID, | ||||
|     vm_id: Option<VMUuid>, | ||||
| ) -> anyhow::Result<TreatmentResult> { | ||||
|     json_request(id.route_vm_suspend(vm_id)).await | ||||
| } | ||||
|  | ||||
| /// Resume one or all VMs of a group | ||||
| pub async fn group_vm_resume( | ||||
|     id: &GroupID, | ||||
|     vm_id: Option<VMUuid>, | ||||
| ) -> anyhow::Result<TreatmentResult> { | ||||
|     json_request(id.route_vm_resume(vm_id)).await | ||||
| } | ||||
|  | ||||
| /// Get the screenshot of one or all VMs of a group | ||||
| pub async fn group_vm_screenshot(id: &GroupID, vm_id: Option<VMUuid>) -> anyhow::Result<Vec<u8>> { | ||||
|     Ok(request(id.route_vm_screenshot(vm_id)) | ||||
|         .await? | ||||
|         .bytes() | ||||
|         .await? | ||||
|         .to_vec()) | ||||
| } | ||||
|  | ||||
| /// Get current server information | ||||
| pub async fn get_server_info() -> anyhow::Result<SystemInfo> { | ||||
|     json_request("/api/server/info").await | ||||
|   | ||||
| @@ -5,6 +5,8 @@ import { | ||||
|   typographyStyles, | ||||
| } from "@fluentui/react-components"; | ||||
| import { | ||||
|   AppsListDetailFilled, | ||||
|   AppsListDetailRegular, | ||||
|   DesktopFilled, | ||||
|   DesktopRegular, | ||||
|   InfoFilled, | ||||
| @@ -18,6 +20,7 @@ import { AsyncWidget } from "./widgets/AsyncWidget"; | ||||
| import { MainMenu } from "./widgets/MainMenu"; | ||||
| import { SystemInfoWidget } from "./widgets/SystemInfoWidget"; | ||||
| import { VirtualMachinesWidget } from "./widgets/VirtualMachinesWidget"; | ||||
| import { GroupsWidget } from "./widgets/GroupsWidget"; | ||||
|  | ||||
| const useStyles = makeStyles({ | ||||
|   title: typographyStyles.title2, | ||||
| @@ -27,6 +30,8 @@ const InfoIcon = bundleIcon(InfoFilled, InfoRegular); | ||||
|  | ||||
| const DesktopIcon = bundleIcon(DesktopFilled, DesktopRegular); | ||||
|  | ||||
| const AppListIcon = bundleIcon(AppsListDetailFilled, AppsListDetailRegular); | ||||
|  | ||||
| export function App() { | ||||
|   return ( | ||||
|     <AsyncWidget | ||||
| @@ -48,12 +53,17 @@ function AppInner(): React.ReactElement { | ||||
|  | ||||
| function AuthenticatedApp(): React.ReactElement { | ||||
|   const styles = useStyles(); | ||||
|   const [tab, setTab] = React.useState<"vm" | "info">("vm"); | ||||
|   const [tab, setTab] = React.useState<"group" | "vm" | "info">("group"); | ||||
|  | ||||
|   const [rights, setRights] = React.useState<Rights | undefined>(); | ||||
|  | ||||
|   const load = async () => { | ||||
|     setRights(await ServerApi.GetRights()); | ||||
|     const rights = await ServerApi.GetRights(); | ||||
|     setRights(rights); | ||||
|  | ||||
|     if (rights!.groups.length > 0) setTab("group"); | ||||
|     else if (rights!.vms.length > 0) setTab("vm"); | ||||
|     else setTab("info"); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
| @@ -82,25 +92,27 @@ function AuthenticatedApp(): React.ReactElement { | ||||
|                 selectedValue={tab} | ||||
|                 onTabSelect={(_, d) => setTab(d.value as any)} | ||||
|               > | ||||
|                 <Tab | ||||
|                   value="vm" | ||||
|                   icon={<DesktopIcon />} | ||||
|                   disabled={rights!.vms.length === 0} | ||||
|                 > | ||||
|                   Virtual machines | ||||
|                 </Tab> | ||||
|                 <Tab | ||||
|                   value="info" | ||||
|                   icon={<InfoIcon />} | ||||
|                   disabled={!rights!.sys_info} | ||||
|                 > | ||||
|                   System info | ||||
|                 </Tab> | ||||
|                 {rights!.groups.length > 0 && ( | ||||
|                   <Tab value="group" icon={<AppListIcon />}> | ||||
|                     Groups | ||||
|                   </Tab> | ||||
|                 )} | ||||
|                 {rights!.vms.length > 0 && ( | ||||
|                   <Tab value="vm" icon={<DesktopIcon />}> | ||||
|                     Virtual machines | ||||
|                   </Tab> | ||||
|                 )} | ||||
|                 {rights!.sys_info && ( | ||||
|                   <Tab value="info" icon={<InfoIcon />}> | ||||
|                     System info | ||||
|                   </Tab> | ||||
|                 )} | ||||
|               </TabList> | ||||
|               <div> | ||||
|                 <MainMenu /> | ||||
|               </div> | ||||
|             </div> | ||||
|             {tab === "group" && <GroupsWidget rights={rights!} />} | ||||
|             {tab === "vm" && <VirtualMachinesWidget rights={rights!} />} | ||||
|             {tab === "info" && <SystemInfoWidget />} | ||||
|           </div> | ||||
|   | ||||
							
								
								
									
										107
									
								
								remote_frontend/src/api/GroupApi.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								remote_frontend/src/api/GroupApi.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,107 @@ | ||||
| import { APIClient } from "./ApiClient"; | ||||
| import { VMGroup } from "./ServerApi"; | ||||
| import { VMInfo, VMState } from "./VMApi"; | ||||
|  | ||||
| export interface GroupVMState { | ||||
|   [key: string]: VMState; | ||||
| } | ||||
|  | ||||
| export interface TreatmentResult { | ||||
|   ok: number; | ||||
|   failed: number; | ||||
| } | ||||
|  | ||||
| export class GroupApi { | ||||
|   /** | ||||
|    * Get the state of the VMs of a group | ||||
|    */ | ||||
|   static async State(g: VMGroup): Promise<GroupVMState> { | ||||
|     return ( | ||||
|       await APIClient.exec({ method: "GET", uri: `/group/${g.id}/vm/state` }) | ||||
|     ).data; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Request to start the VM of a group | ||||
|    */ | ||||
|   static async StartVM(g: VMGroup, vm?: VMInfo): Promise<TreatmentResult> { | ||||
|     return ( | ||||
|       await APIClient.exec({ | ||||
|         method: "GET", | ||||
|         uri: `/group/${g.id}/vm/start` + (vm ? `?vm_id=${vm.uuid}` : ""), | ||||
|       }) | ||||
|     ).data; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Request to suspend the VM of a group | ||||
|    */ | ||||
|   static async SuspendVM(g: VMGroup, vm?: VMInfo): Promise<TreatmentResult> { | ||||
|     return ( | ||||
|       await APIClient.exec({ | ||||
|         method: "GET", | ||||
|         uri: `/group/${g.id}/vm/suspend` + (vm ? `?vm_id=${vm.uuid}` : ""), | ||||
|       }) | ||||
|     ).data; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Request to resume the VM of a group | ||||
|    */ | ||||
|   static async ResumeVM(g: VMGroup, vm?: VMInfo): Promise<TreatmentResult> { | ||||
|     return ( | ||||
|       await APIClient.exec({ | ||||
|         method: "GET", | ||||
|         uri: `/group/${g.id}/vm/resume` + (vm ? `?vm_id=${vm.uuid}` : ""), | ||||
|       }) | ||||
|     ).data; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Request to shutdown the VM of a group | ||||
|    */ | ||||
|   static async ShutdownVM(g: VMGroup, vm?: VMInfo): Promise<TreatmentResult> { | ||||
|     return ( | ||||
|       await APIClient.exec({ | ||||
|         method: "GET", | ||||
|         uri: `/group/${g.id}/vm/shutdown` + (vm ? `?vm_id=${vm.uuid}` : ""), | ||||
|       }) | ||||
|     ).data; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Request to kill the VM of a group | ||||
|    */ | ||||
|   static async KillVM(g: VMGroup, vm?: VMInfo): Promise<TreatmentResult> { | ||||
|     return ( | ||||
|       await APIClient.exec({ | ||||
|         method: "GET", | ||||
|         uri: `/group/${g.id}/vm/kill` + (vm ? `?vm_id=${vm.uuid}` : ""), | ||||
|       }) | ||||
|     ).data; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Request to reset the VM of a group | ||||
|    */ | ||||
|   static async ResetVM(g: VMGroup, vm?: VMInfo): Promise<TreatmentResult> { | ||||
|     return ( | ||||
|       await APIClient.exec({ | ||||
|         method: "GET", | ||||
|         uri: `/group/${g.id}/vm/reset` + (vm ? `?vm_id=${vm.uuid}` : ""), | ||||
|       }) | ||||
|     ).data; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Request a screenshot of the VM of group | ||||
|    */ | ||||
|   static async ScreenshotVM(g: VMGroup, vm?: VMInfo): Promise<Blob> { | ||||
|     return ( | ||||
|       await APIClient.exec({ | ||||
|         method: "GET", | ||||
|         uri: `/group/${g.id}/vm/screenshot` + (vm ? `?vm_id=${vm.uuid}` : ""), | ||||
|       }) | ||||
|     ).data; | ||||
|   } | ||||
| } | ||||
| @@ -1,5 +1,5 @@ | ||||
| import { APIClient } from "./ApiClient"; | ||||
| import { VMInfo } from "./VMApi"; | ||||
| import { VMCaps, VMInfo, VMInfoAndCaps } from "./VMApi"; | ||||
|  | ||||
| export interface ServerConfig { | ||||
|   authenticated: boolean; | ||||
| @@ -7,10 +7,18 @@ export interface ServerConfig { | ||||
| } | ||||
|  | ||||
| export interface Rights { | ||||
|   vms: VMInfo[]; | ||||
|   groups: VMGroup[]; | ||||
|   vms: VMInfoAndCaps[]; | ||||
|   sys_info: boolean; | ||||
| } | ||||
|  | ||||
| export type VMGroup = VMGroupInfo & VMCaps; | ||||
|  | ||||
| export interface VMGroupInfo { | ||||
|   id: string; | ||||
|   vms: VMInfo[]; | ||||
| } | ||||
|  | ||||
| let config: ServerConfig | null = null; | ||||
|  | ||||
| export class ServerApi { | ||||
|   | ||||
| @@ -1,12 +1,15 @@ | ||||
| import { APIClient } from "./ApiClient"; | ||||
|  | ||||
| export interface VMInfo { | ||||
|   uiid: string; | ||||
|   uuid: string; | ||||
|   name: string; | ||||
|   description?: string; | ||||
|   architecture: string; | ||||
|   memory: number; | ||||
|   number_vcpu: number; | ||||
| } | ||||
|  | ||||
| export interface VMCaps { | ||||
|   can_get_state: boolean; | ||||
|   can_start: boolean; | ||||
|   can_shutdown: boolean; | ||||
| @@ -17,6 +20,8 @@ export interface VMInfo { | ||||
|   can_screenshot: boolean; | ||||
| } | ||||
|  | ||||
| export type VMInfoAndCaps = VMInfo & VMCaps; | ||||
|  | ||||
| export type VMState = | ||||
|   | "NoState" | ||||
|   | "Running" | ||||
| @@ -34,7 +39,7 @@ export class VMApi { | ||||
|    */ | ||||
|   static async State(vm: VMInfo): Promise<VMState> { | ||||
|     return ( | ||||
|       await APIClient.exec({ method: "GET", uri: `/vm/${vm.uiid}/state` }) | ||||
|       await APIClient.exec({ method: "GET", uri: `/vm/${vm.uuid}/state` }) | ||||
|     ).data.state; | ||||
|   } | ||||
|  | ||||
| @@ -42,42 +47,42 @@ export class VMApi { | ||||
|    * Request to start VM | ||||
|    */ | ||||
|   static async StartVM(vm: VMInfo): Promise<void> { | ||||
|     await APIClient.exec({ method: "GET", uri: `/vm/${vm.uiid}/start` }); | ||||
|     await APIClient.exec({ method: "GET", uri: `/vm/${vm.uuid}/start` }); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Request to suspend VM | ||||
|    */ | ||||
|   static async SuspendVM(vm: VMInfo): Promise<void> { | ||||
|     await APIClient.exec({ method: "GET", uri: `/vm/${vm.uiid}/suspend` }); | ||||
|     await APIClient.exec({ method: "GET", uri: `/vm/${vm.uuid}/suspend` }); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Request to resume VM | ||||
|    */ | ||||
|   static async ResumeVM(vm: VMInfo): Promise<void> { | ||||
|     await APIClient.exec({ method: "GET", uri: `/vm/${vm.uiid}/resume` }); | ||||
|     await APIClient.exec({ method: "GET", uri: `/vm/${vm.uuid}/resume` }); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Request to shutdown VM | ||||
|    */ | ||||
|   static async ShutdownVM(vm: VMInfo): Promise<void> { | ||||
|     await APIClient.exec({ method: "GET", uri: `/vm/${vm.uiid}/shutdown` }); | ||||
|     await APIClient.exec({ method: "GET", uri: `/vm/${vm.uuid}/shutdown` }); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Request to kill VM | ||||
|    */ | ||||
|   static async KillVM(vm: VMInfo): Promise<void> { | ||||
|     await APIClient.exec({ method: "GET", uri: `/vm/${vm.uiid}/kill` }); | ||||
|     await APIClient.exec({ method: "GET", uri: `/vm/${vm.uuid}/kill` }); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Request to reset VM | ||||
|    */ | ||||
|   static async ResetVM(vm: VMInfo): Promise<void> { | ||||
|     await APIClient.exec({ method: "GET", uri: `/vm/${vm.uiid}/reset` }); | ||||
|     await APIClient.exec({ method: "GET", uri: `/vm/${vm.uuid}/reset` }); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
| @@ -86,7 +91,7 @@ export class VMApi { | ||||
|   static async Screenshot(vm: VMInfo): Promise<Blob> { | ||||
|     return ( | ||||
|       await APIClient.exec({ | ||||
|         uri: `/vm/${vm.uiid}/screenshot`, | ||||
|         uri: `/vm/${vm.uuid}/screenshot`, | ||||
|         method: "GET", | ||||
|       }) | ||||
|     ).data; | ||||
|   | ||||
| @@ -20,7 +20,7 @@ type ThemeContext = { theme: Theme; set: (theme: Theme) => void }; | ||||
| const ThemeContextK = React.createContext<ThemeContext | null>(null); | ||||
|  | ||||
| export function ThemeProvider(p: React.PropsWithChildren): React.ReactElement { | ||||
|   const [theme, setTheme] = React.useState<Theme>("highcontrast"); | ||||
|   const [theme, setTheme] = React.useState<Theme>("teamsdark"); | ||||
|  | ||||
|   let fluentTheme = teamsHighContrastTheme; | ||||
|   switch (theme) { | ||||
|   | ||||
							
								
								
									
										177
									
								
								remote_frontend/src/widgets/GroupVMAction.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										177
									
								
								remote_frontend/src/widgets/GroupVMAction.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,177 @@ | ||||
| import { Button, Spinner, Toolbar, Tooltip } from "@fluentui/react-components"; | ||||
| import { | ||||
|   ArrowResetRegular, | ||||
|   PauseRegular, | ||||
|   PlayCircleRegular, | ||||
|   PlayFilled, | ||||
|   PowerRegular, | ||||
|   StopRegular, | ||||
| } from "@fluentui/react-icons"; | ||||
| import React from "react"; | ||||
| import { GroupApi, TreatmentResult } from "../api/GroupApi"; | ||||
| import { VMGroup } from "../api/ServerApi"; | ||||
| import { VMInfo, VMState } from "../api/VMApi"; | ||||
| import { useAlert } from "../hooks/providers/AlertDialogProvider"; | ||||
| import { useConfirm } from "../hooks/providers/ConfirmDialogProvider"; | ||||
| import { useToast } from "../hooks/providers/ToastProvider"; | ||||
|  | ||||
| export function GroupVMAction(p: { | ||||
|   group: VMGroup; | ||||
|   state?: VMState; | ||||
|   vm?: VMInfo; | ||||
| }): React.ReactElement { | ||||
|   return ( | ||||
|     <Toolbar> | ||||
|       <GroupVMButton | ||||
|         enabled={p.group.can_start} | ||||
|         icon={<PlayFilled />} | ||||
|         tooltip="Start" | ||||
|         group={p.group} | ||||
|         vm={p.vm} | ||||
|         allowedStates={["Shutdown", "Shutoff", "Crashed"]} | ||||
|         currState={p.state} | ||||
|         needConfirm={false} | ||||
|         action={GroupApi.StartVM} | ||||
|       /> | ||||
|       <GroupVMButton | ||||
|         enabled={p.group.can_suspend} | ||||
|         icon={<PauseRegular />} | ||||
|         tooltip="Suspend" | ||||
|         group={p.group} | ||||
|         vm={p.vm} | ||||
|         allowedStates={["Running"]} | ||||
|         currState={p.state} | ||||
|         needConfirm={true} | ||||
|         action={GroupApi.SuspendVM} | ||||
|       /> | ||||
|       <GroupVMButton | ||||
|         enabled={p.group.can_resume} | ||||
|         icon={<PlayCircleRegular />} | ||||
|         tooltip="Resume" | ||||
|         group={p.group} | ||||
|         vm={p.vm} | ||||
|         allowedStates={["Paused", "PowerManagementSuspended"]} | ||||
|         currState={p.state} | ||||
|         needConfirm={false} | ||||
|         action={GroupApi.ResumeVM} | ||||
|       /> | ||||
|       <GroupVMButton | ||||
|         enabled={p.group.can_shutdown} | ||||
|         icon={<PowerRegular />} | ||||
|         tooltip="Shutdown" | ||||
|         group={p.group} | ||||
|         vm={p.vm} | ||||
|         allowedStates={["Running"]} | ||||
|         currState={p.state} | ||||
|         needConfirm={true} | ||||
|         action={GroupApi.ShutdownVM} | ||||
|       /> | ||||
|       <GroupVMButton | ||||
|         enabled={p.group.can_kill} | ||||
|         icon={<StopRegular />} | ||||
|         tooltip="Kill" | ||||
|         group={p.group} | ||||
|         vm={p.vm} | ||||
|         allowedStates={[ | ||||
|           "Running", | ||||
|           "Paused", | ||||
|           "PowerManagementSuspended", | ||||
|           "Blocked", | ||||
|         ]} | ||||
|         currState={p.state} | ||||
|         needConfirm={true} | ||||
|         action={GroupApi.KillVM} | ||||
|       /> | ||||
|       <GroupVMButton | ||||
|         enabled={p.group.can_reset} | ||||
|         icon={<ArrowResetRegular />} | ||||
|         tooltip="Reset" | ||||
|         group={p.group} | ||||
|         vm={p.vm} | ||||
|         allowedStates={[ | ||||
|           "Running", | ||||
|           "Paused", | ||||
|           "PowerManagementSuspended", | ||||
|           "Blocked", | ||||
|         ]} | ||||
|         currState={p.state} | ||||
|         needConfirm={true} | ||||
|         action={GroupApi.ResetVM} | ||||
|       /> | ||||
|     </Toolbar> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function GroupVMButton(p: { | ||||
|   enabled: boolean; | ||||
|   icon: React.ReactElement; | ||||
|   action: (group: VMGroup, vm?: VMInfo) => Promise<TreatmentResult>; | ||||
|   tooltip: string; | ||||
|   currState?: VMState; | ||||
|   allowedStates: VMState[]; | ||||
|   group: VMGroup; | ||||
|   vm?: VMInfo; | ||||
|   needConfirm: boolean; | ||||
| }): React.ReactElement { | ||||
|   const toast = useToast(); | ||||
|   const confirm = useConfirm(); | ||||
|   const alert = useAlert(); | ||||
|  | ||||
|   const [running, setRunning] = React.useState(false); | ||||
|  | ||||
|   const target = p.vm | ||||
|     ? `the VM ${p.vm.name}` | ||||
|     : `all the VM of the group ${p.group.id}`; | ||||
|  | ||||
|   const allowed = | ||||
|     !p.vm || (p.currState && p.allowedStates.includes(p.currState)); | ||||
|  | ||||
|   const perform = async () => { | ||||
|     if (running || !allowed) return; | ||||
|     try { | ||||
|       if ( | ||||
|         (!p.vm || p.needConfirm) && | ||||
|         !(await confirm( | ||||
|           `Do you want to perform ${p.tooltip} action on ${target}?`, | ||||
|           `Confirmation`, | ||||
|           p.tooltip | ||||
|         )) | ||||
|       ) { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       setRunning(true); | ||||
|  | ||||
|       const result = await p.action(p.group, p.vm); | ||||
|  | ||||
|       toast( | ||||
|         p.tooltip, | ||||
|         `${p.tooltip} action on ${target}: ${result.ok} OK / ${result.failed} Failed`, | ||||
|         "success" | ||||
|       ); | ||||
|     } catch (e) { | ||||
|       console.error("Failed to perform group action!", e); | ||||
|  | ||||
|       alert(`Failed to perform ${p.tooltip} action on ${target}: ${e}`); | ||||
|     } finally { | ||||
|       setRunning(false); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   if (!p.enabled) return <></>; | ||||
|  | ||||
|   return ( | ||||
|     <Tooltip | ||||
|       content={`${p.tooltip} ${target}`} | ||||
|       relationship="description" | ||||
|       withArrow | ||||
|     > | ||||
|       <Button | ||||
|         icon={running ? <Spinner size="tiny" /> : p.icon} | ||||
|         onClick={allowed ? perform : undefined} | ||||
|         disabled={!allowed} | ||||
|         appearance="subtle" | ||||
|       /> | ||||
|     </Tooltip> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										171
									
								
								remote_frontend/src/widgets/GroupsWidget.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										171
									
								
								remote_frontend/src/widgets/GroupsWidget.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,171 @@ | ||||
| import { | ||||
|   Button, | ||||
|   Card, | ||||
|   Dialog, | ||||
|   DialogActions, | ||||
|   DialogBody, | ||||
|   DialogContent, | ||||
|   DialogSurface, | ||||
|   DialogTitle, | ||||
|   DialogTrigger, | ||||
|   Table, | ||||
|   TableBody, | ||||
|   TableCell, | ||||
|   TableCellActions, | ||||
|   TableCellLayout, | ||||
|   TableHeader, | ||||
|   TableHeaderCell, | ||||
|   TableRow, | ||||
|   Title3, | ||||
|   Tooltip, | ||||
| } from "@fluentui/react-components"; | ||||
| import { Desktop24Regular, ScreenshotRegular } from "@fluentui/react-icons"; | ||||
| import { filesize } from "filesize"; | ||||
| import React from "react"; | ||||
| import { GroupApi, GroupVMState } from "../api/GroupApi"; | ||||
| import { Rights, VMGroup } from "../api/ServerApi"; | ||||
| import { VMInfo } from "../api/VMApi"; | ||||
| import { useToast } from "../hooks/providers/ToastProvider"; | ||||
| import { GroupVMAction } from "./GroupVMAction"; | ||||
| import { VMLiveScreenshot } from "./VMLiveScreenshot"; | ||||
|  | ||||
| export function GroupsWidget(p: { rights: Rights }): React.ReactElement { | ||||
|   return ( | ||||
|     <> | ||||
|       {p.rights.groups.map((g) => ( | ||||
|         <GroupInfo group={g} /> | ||||
|       ))} | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function GroupInfo(p: { group: VMGroup }): React.ReactElement { | ||||
|   const toast = useToast(); | ||||
|  | ||||
|   const [state, setState] = React.useState<GroupVMState | undefined>(); | ||||
|   const [screenshotVM, setScreenshotVM] = React.useState<VMInfo | undefined>(); | ||||
|  | ||||
|   const load = async () => { | ||||
|     const newState = await GroupApi.State(p.group); | ||||
|     if (state !== newState) setState(newState); | ||||
|   }; | ||||
|  | ||||
|   const screenshot = (vm: VMInfo) => { | ||||
|     setScreenshotVM(vm); | ||||
|   }; | ||||
|  | ||||
|   React.useEffect(() => { | ||||
|     const interval = setInterval(async () => { | ||||
|       try { | ||||
|         if (p.group.can_get_state) await load(); | ||||
|       } catch (e) { | ||||
|         console.error(e); | ||||
|         toast( | ||||
|           "Error", | ||||
|           `Failed to refresh group ${p.group.id} VMs status!`, | ||||
|           "error" | ||||
|         ); | ||||
|       } | ||||
|     }, 1000); | ||||
|     return () => clearInterval(interval); | ||||
|   }); | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <Card | ||||
|         style={{ | ||||
|           margin: "50px 10px", | ||||
|           display: "flex", | ||||
|           flexDirection: "column", | ||||
|         }} | ||||
|       > | ||||
|         <div style={{ display: "flex", justifyContent: "space-between" }}> | ||||
|           <Title3 style={{ marginLeft: "10px" }}>{p.group.id}</Title3> | ||||
|           <GroupVMAction group={p.group} /> | ||||
|         </div> | ||||
|         <Table sortable> | ||||
|           <TableHeader> | ||||
|             <TableRow> | ||||
|               <TableHeaderCell>VM</TableHeaderCell> | ||||
|               <TableHeaderCell>Resources</TableHeaderCell> | ||||
|               <TableHeaderCell>State</TableHeaderCell> | ||||
|               <TableHeaderCell>Actions</TableHeaderCell> | ||||
|             </TableRow> | ||||
|           </TableHeader> | ||||
|           <TableBody> | ||||
|             {p.group.vms.map((item) => ( | ||||
|               <TableRow key={item.uuid}> | ||||
|                 <TableCell> | ||||
|                   <TableCellLayout | ||||
|                     media={<Desktop24Regular />} | ||||
|                     appearance="primary" | ||||
|                     description={item.description} | ||||
|                   > | ||||
|                     {item.name} | ||||
|                   </TableCellLayout> | ||||
|                   <TableCellActions> | ||||
|                     {state?.[item.uuid] === "Running" && ( | ||||
|                       <Tooltip | ||||
|                         relationship="description" | ||||
|                         content={"Take a screenshot of the VM screen"} | ||||
|                         withArrow | ||||
|                       > | ||||
|                         <Button | ||||
|                           icon={<ScreenshotRegular />} | ||||
|                           appearance="subtle" | ||||
|                           aria-label="Edit" | ||||
|                           disabled={!p.group.can_screenshot} | ||||
|                           onClick={() => screenshot(item)} | ||||
|                         /> | ||||
|                       </Tooltip> | ||||
|                     )} | ||||
|                   </TableCellActions> | ||||
|                 </TableCell> | ||||
|                 <TableCell> | ||||
|                   {item.architecture} • RAM :{" "} | ||||
|                   {filesize(item.memory * 1000 * 1000)} •{" "} | ||||
|                   {item.number_vcpu} vCPU | ||||
|                 </TableCell> | ||||
|                 <TableCell>{state?.[item.uuid] ?? ""}</TableCell> | ||||
|                 <TableCell> | ||||
|                   <GroupVMAction | ||||
|                     group={p.group} | ||||
|                     state={state?.[item.uuid]} | ||||
|                     vm={item} | ||||
|                   /> | ||||
|                 </TableCell> | ||||
|               </TableRow> | ||||
|             ))} | ||||
|           </TableBody> | ||||
|         </Table> | ||||
|       </Card> | ||||
|       <Dialog | ||||
|         open={!!screenshotVM} | ||||
|         onOpenChange={(_event, _data) => { | ||||
|           if (!screenshotVM) setScreenshotVM(undefined); | ||||
|         }} | ||||
|       > | ||||
|         <DialogSurface> | ||||
|           <DialogBody> | ||||
|             <DialogTitle> | ||||
|               <em>{screenshotVM?.name}</em> screen | ||||
|             </DialogTitle> | ||||
|             <DialogContent> | ||||
|               <VMLiveScreenshot vm={screenshotVM!} group={p.group} /> | ||||
|             </DialogContent> | ||||
|             <DialogActions> | ||||
|               <DialogTrigger disableButtonEnhancement> | ||||
|                 <Button | ||||
|                   appearance="secondary" | ||||
|                   onClick={() => setScreenshotVM(undefined)} | ||||
|                 > | ||||
|                   Close | ||||
|                 </Button> | ||||
|               </DialogTrigger> | ||||
|             </DialogActions> | ||||
|           </DialogBody> | ||||
|         </DialogSurface> | ||||
|       </Dialog> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
| @@ -1,8 +1,13 @@ | ||||
| import React from "react"; | ||||
| import { GroupApi } from "../api/GroupApi"; | ||||
| import { VMGroup } from "../api/ServerApi"; | ||||
| import { VMApi, VMInfo } from "../api/VMApi"; | ||||
| import { useToast } from "../hooks/providers/ToastProvider"; | ||||
|  | ||||
| export function VMLiveScreenshot(p: { vm: VMInfo }): React.ReactElement { | ||||
| export function VMLiveScreenshot(p: { | ||||
|   vm: VMInfo; | ||||
|   group?: VMGroup; | ||||
| }): React.ReactElement { | ||||
|   const toast = useToast(); | ||||
|  | ||||
|   const [screenshotURL, setScreenshotURL] = React.useState< | ||||
| @@ -14,7 +19,9 @@ export function VMLiveScreenshot(p: { vm: VMInfo }): React.ReactElement { | ||||
|   React.useEffect(() => { | ||||
|     const refresh = async () => { | ||||
|       try { | ||||
|         const screenshot = await VMApi.Screenshot(p.vm); | ||||
|         const screenshot = p.group | ||||
|           ? await GroupApi.ScreenshotVM(p.group, p.vm) | ||||
|           : await VMApi.Screenshot(p.vm); | ||||
|         const u = URL.createObjectURL(screenshot); | ||||
|         setScreenshotURL(u); | ||||
|       } catch (e) { | ||||
|   | ||||
| @@ -22,11 +22,11 @@ import { | ||||
| import { filesize } from "filesize"; | ||||
| import React from "react"; | ||||
| import { Rights } from "../api/ServerApi"; | ||||
| import { VMApi, VMInfo, VMState } from "../api/VMApi"; | ||||
| import { VMApi, VMInfo, VMInfoAndCaps, VMState } from "../api/VMApi"; | ||||
| import { useConfirm } from "../hooks/providers/ConfirmDialogProvider"; | ||||
| import { useToast } from "../hooks/providers/ToastProvider"; | ||||
| import { VMLiveScreenshot } from "./VMLiveScreenshot"; | ||||
| import { SectionContainer } from "./SectionContainer"; | ||||
| import { VMLiveScreenshot } from "./VMLiveScreenshot"; | ||||
|  | ||||
| const useStyles = makeStyles({ | ||||
|   body1Stronger: typographyStyles.body1Stronger, | ||||
| @@ -54,7 +54,7 @@ export function VirtualMachinesWidget(p: { | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function VMWidget(p: { vm: VMInfo }): React.ReactElement { | ||||
| function VMWidget(p: { vm: VMInfoAndCaps }): React.ReactElement { | ||||
|   const toast = useToast(); | ||||
|  | ||||
|   const [state, setState] = React.useState<VMState | undefined>(); | ||||
| @@ -189,7 +189,10 @@ function VMWidget(p: { vm: VMInfo }): React.ReactElement { | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function VMPreview(p: { vm: VMInfo; state?: VMState }): React.ReactElement { | ||||
| function VMPreview(p: { | ||||
|   vm: VMInfoAndCaps; | ||||
|   state?: VMState; | ||||
| }): React.ReactElement { | ||||
|   const styles = useStyles(); | ||||
|   if (!p.vm.can_screenshot || p.state !== "Running") { | ||||
|     return ( | ||||
|   | ||||
		Reference in New Issue
	
	Block a user