Compare commits
	
		
			1 Commits
		
	
	
		
			renovate/m
			...
			1e0cd6ea8c
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 1e0cd6ea8c | 
| @@ -5,7 +5,7 @@ name: default | |||||||
|  |  | ||||||
| steps: | steps: | ||||||
| - name: frontend_build | - name: frontend_build | ||||||
|   image: node:24 |   image: node:22 | ||||||
|   volumes: |   volumes: | ||||||
|     - name: frontend_app |     - name: frontend_app | ||||||
|       path: /tmp/frontend_build |       path: /tmp/frontend_build | ||||||
|   | |||||||
							
								
								
									
										11
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,11 +1,4 @@ | |||||||
| # VirtWeb Remote | # VirtWeb Remote | ||||||
| Web UI that allows to start and stop VMs managed by VirtWEB without having to expose the VirtWEB directly on the Internet. | WIP project | ||||||
|  |  | ||||||
| VirtWebRemote rely on OpenID to authenticate users. | This project aims to use the VirtWeb API to start and stop VM without directly exposing the VirtWEB API to the Internet. | ||||||
|  |  | ||||||
| VirtWebRemote authenticates against VirtWEB API using an API token. Both the token ID and private key are required to be able to authenticate against the VirtWEB API. |  | ||||||
|  |  | ||||||
| ## Docker image options |  | ||||||
| ```bash |  | ||||||
| docker run --rm -it pierre42100/virtweb_remote --help |  | ||||||
| ``` |  | ||||||
|   | |||||||
							
								
								
									
										1641
									
								
								remote_backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1641
									
								
								remote_backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,28 +1,28 @@ | |||||||
| [package] | [package] | ||||||
| name = "remote_backend" | name = "remote_backend" | ||||||
| version = "0.1.0" | version = "0.1.0" | ||||||
| edition = "2024" | edition = "2021" | ||||||
|  |  | ||||||
| # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||||||
|  |  | ||||||
| [dependencies] | [dependencies] | ||||||
| log = "0.4.28" | log = "0.4.21" | ||||||
| env_logger = "0.11.8" | env_logger = "0.11.3" | ||||||
| clap = { version = "4.5.51", features = ["derive", "env"] } | clap = { version = "4.5.4", features = ["derive", "env"] } | ||||||
| serde = { version = "1.0.228", features = ["derive"] } | serde = { version = "1.0.200", features = ["derive"] } | ||||||
| light-openid = { version = "1.0.4", features = ["crypto-wrapper"] } | light-openid = { version = "1.0.2", features = ["crypto-wrapper"] } | ||||||
| basic-jwt = "0.3.0" | basic-jwt = "0.2.0" | ||||||
| actix-web = "4.11.0" | actix-web = "4.5.1" | ||||||
| actix-remote-ip = "0.1.0" | actix-remote-ip = "0.1.0" | ||||||
| actix-session = { version = "0.10.1", features = ["cookie-session"] } | actix-session = { version = "0.9.0", features = ["cookie-session"] } | ||||||
| actix-identity = "0.9.0" | actix-identity = "0.7.1" | ||||||
| actix-cors = "0.7.1" | actix-cors = "0.7.0" | ||||||
| lazy_static = "1.5.0" | lazy_static = "1.4.0" | ||||||
| anyhow = "1.0.100" | anyhow = "1.0.83" | ||||||
| reqwest = { version = "0.12.24", features = ["json"] } | reqwest = { version = "0.12.4", features = ["json"] } | ||||||
| thiserror = "2.0.17" | thiserror = "1.0.59" | ||||||
| uuid = { version = "1.18.1", features = ["v4", "serde"] } | uuid = { version = "1.8.0", features = ["v4", "serde"] } | ||||||
| futures-util = "0.3.31" | futures-util = "0.3.30" | ||||||
| lazy-regex = "3.4.1" | lazy-regex = "3.1.0" | ||||||
| mime_guess = "2.0.5" | mime_guess = "2.0.4" | ||||||
| rust-embed = { version = "8.7.2" } | rust-embed = { version = "8.3.0" } | ||||||
| @@ -6,4 +6,4 @@ RUN apt-get update \ | |||||||
|  |  | ||||||
| COPY remote_backend /usr/local/bin/remote_backend | COPY remote_backend /usr/local/bin/remote_backend | ||||||
|  |  | ||||||
| ENTRYPOINT ["/usr/local/bin/remote_backend"] | ENTRYPOINT /usr/local/bin/remote_backend | ||||||
|   | |||||||
| @@ -29,7 +29,7 @@ pub struct AppConfig { | |||||||
|     #[arg( |     #[arg( | ||||||
|         long, |         long, | ||||||
|         env, |         env, | ||||||
|         default_value = "http://localhost:9001/dex/.well-known/openid-configuration" |         default_value = "http://localhost:9001/.well-known/openid-configuration" | ||||||
|     )] |     )] | ||||||
|     pub oidc_configuration_url: String, |     pub oidc_configuration_url: String, | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| use actix_remote_ip::RemoteIP; | use actix_remote_ip::RemoteIP; | ||||||
| use actix_web::web::Data; | use actix_web::web::Data; | ||||||
| use actix_web::{HttpResponse, Responder, web}; | use actix_web::{web, HttpResponse, Responder}; | ||||||
| use light_openid::basic_state_manager::BasicStateManager; | use light_openid::basic_state_manager::BasicStateManager; | ||||||
|  |  | ||||||
| use crate::app_config::AppConfig; | use crate::app_config::AppConfig; | ||||||
|   | |||||||
| @@ -1,79 +0,0 @@ | |||||||
| use crate::controllers::HttpResult; |  | ||||||
| use crate::virtweb_client; |  | ||||||
| use crate::virtweb_client::{GroupID, VMUuid}; |  | ||||||
| use actix_web::{HttpResponse, web}; |  | ||||||
|  |  | ||||||
| #[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)) |  | ||||||
| } |  | ||||||
| @@ -1,11 +1,11 @@ | |||||||
| use actix_web::HttpResponse; |  | ||||||
| use actix_web::body::BoxBody; | use actix_web::body::BoxBody; | ||||||
| use actix_web::http::StatusCode; | use actix_web::http::StatusCode; | ||||||
|  | use actix_web::HttpResponse; | ||||||
| use std::error::Error; | use std::error::Error; | ||||||
| use std::fmt::{Display, Formatter}; | use std::fmt::{Display, Formatter}; | ||||||
|  | use std::io::ErrorKind; | ||||||
|  |  | ||||||
| pub mod auth_controller; | pub mod auth_controller; | ||||||
| pub mod group_controller; |  | ||||||
| pub mod server_controller; | pub mod server_controller; | ||||||
| pub mod static_controller; | pub mod static_controller; | ||||||
| pub mod sys_info_controller; | pub mod sys_info_controller; | ||||||
| @@ -37,7 +37,7 @@ impl actix_web::error::ResponseError for HttpErr { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|     fn error_response(&self) -> HttpResponse<BoxBody> { |     fn error_response(&self) -> HttpResponse<BoxBody> { | ||||||
|         log::error!("Error while processing request! {self}"); |         log::error!("Error while processing request! {}", self); | ||||||
|  |  | ||||||
|         HttpResponse::InternalServerError().body("Failed to execute request!") |         HttpResponse::InternalServerError().body("Failed to execute request!") | ||||||
|     } |     } | ||||||
| @@ -51,7 +51,7 @@ impl From<anyhow::Error> for HttpErr { | |||||||
|  |  | ||||||
| impl From<Box<dyn Error>> for HttpErr { | impl From<Box<dyn Error>> for HttpErr { | ||||||
|     fn from(value: Box<dyn Error>) -> Self { |     fn from(value: Box<dyn Error>) -> Self { | ||||||
|         HttpErr::Err(std::io::Error::other(value.to_string()).into()) |         HttpErr::Err(std::io::Error::new(ErrorKind::Other, value.to_string()).into()) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -81,7 +81,7 @@ impl From<reqwest::header::ToStrError> for HttpErr { | |||||||
|  |  | ||||||
| impl From<actix_web::Error> for HttpErr { | impl From<actix_web::Error> for HttpErr { | ||||||
|     fn from(value: actix_web::Error) -> Self { |     fn from(value: actix_web::Error) -> Self { | ||||||
|         HttpErr::Err(std::io::Error::other(value.to_string()).into()) |         HttpErr::Err(std::io::Error::new(ErrorKind::Other, value.to_string()).into()) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,8 +1,6 @@ | |||||||
| use crate::app_config::AppConfig; | use crate::app_config::AppConfig; | ||||||
| use crate::controllers::HttpResult; | use crate::controllers::HttpResult; | ||||||
| use crate::extractors::auth_extractor::AuthExtractor; | use crate::extractors::auth_extractor::AuthExtractor; | ||||||
| use crate::virtweb_client; |  | ||||||
| use crate::virtweb_client::{GroupID, VMCaps, VMInfo}; |  | ||||||
| use actix_web::HttpResponse; | use actix_web::HttpResponse; | ||||||
|  |  | ||||||
| #[derive(serde::Serialize)] | #[derive(serde::Serialize)] | ||||||
| @@ -17,75 +15,3 @@ pub async fn config(auth: AuthExtractor) -> HttpResult { | |||||||
|         disable_auth: AppConfig::get().unsecure_disable_login, |         disable_auth: AppConfig::get().unsecure_disable_login, | ||||||
|     })) |     })) | ||||||
| } | } | ||||||
|  |  | ||||||
| #[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 { |  | ||||||
|     #[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 { |  | ||||||
|             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()), |  | ||||||
|             }, |  | ||||||
|         }) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     Ok(HttpResponse::Ok().json(res)) |  | ||||||
| } |  | ||||||
|   | |||||||
| @@ -18,7 +18,7 @@ mod serve_static_debug { | |||||||
|  |  | ||||||
| #[cfg(not(debug_assertions))] | #[cfg(not(debug_assertions))] | ||||||
| mod serve_static_release { | mod serve_static_release { | ||||||
|     use actix_web::{HttpResponse, Responder, web}; |     use actix_web::{web, HttpResponse, Responder}; | ||||||
|     use rust_embed::RustEmbed; |     use rust_embed::RustEmbed; | ||||||
|  |  | ||||||
|     #[derive(RustEmbed)] |     #[derive(RustEmbed)] | ||||||
|   | |||||||
| @@ -2,6 +2,20 @@ use crate::controllers::HttpResult; | |||||||
| use crate::virtweb_client; | use crate::virtweb_client; | ||||||
| use actix_web::HttpResponse; | use actix_web::HttpResponse; | ||||||
|  |  | ||||||
|  | #[derive(serde::Serialize)] | ||||||
|  | struct SysInfoStatus { | ||||||
|  |     allowed: bool, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// Check if system info can be retrieved | ||||||
|  | pub async fn config() -> HttpResult { | ||||||
|  |     let info = virtweb_client::get_token_info().await?; | ||||||
|  |  | ||||||
|  |     Ok(HttpResponse::Ok().json(SysInfoStatus { | ||||||
|  |         allowed: info.can_retrieve_system_info(), | ||||||
|  |     })) | ||||||
|  | } | ||||||
|  |  | ||||||
| /// Get current system status | /// Get current system status | ||||||
| pub async fn status() -> HttpResult { | pub async fn status() -> HttpResult { | ||||||
|     Ok(HttpResponse::Ok().json(virtweb_client::get_server_info().await?)) |     Ok(HttpResponse::Ok().json(virtweb_client::get_server_info().await?)) | ||||||
|   | |||||||
| @@ -3,7 +3,55 @@ | |||||||
| use crate::controllers::HttpResult; | use crate::controllers::HttpResult; | ||||||
| use crate::virtweb_client; | use crate::virtweb_client; | ||||||
| use crate::virtweb_client::VMUuid; | use crate::virtweb_client::VMUuid; | ||||||
| use actix_web::{HttpResponse, web}; | use actix_web::{web, HttpResponse}; | ||||||
|  |  | ||||||
|  | #[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, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// Get the list of VMs that can be controlled by VirtWeb remote | ||||||
|  | pub async fn list() -> HttpResult { | ||||||
|  |     let rights = virtweb_client::get_token_info().await?; | ||||||
|  |  | ||||||
|  |     let mut res = vec![]; | ||||||
|  |  | ||||||
|  |     for v in rights.list_vm() { | ||||||
|  |         let vm_info = virtweb_client::vm_info(v).await?; | ||||||
|  |  | ||||||
|  |         res.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()), | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     Ok(HttpResponse::Ok().json(res)) | ||||||
|  | } | ||||||
|  |  | ||||||
| #[derive(serde::Deserialize)] | #[derive(serde::Deserialize)] | ||||||
| pub struct ReqPath { | pub struct ReqPath { | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| use actix_identity::Identity; | use actix_identity::Identity; | ||||||
| use actix_web::dev::Payload; | use actix_web::dev::Payload; | ||||||
| use actix_web::{Error, FromRequest, HttpMessage, HttpRequest}; | use actix_web::{Error, FromRequest, HttpMessage, HttpRequest}; | ||||||
| use futures_util::future::{Ready, ready}; | use futures_util::future::{ready, Ready}; | ||||||
| use std::fmt::Display; | use std::fmt::Display; | ||||||
|  |  | ||||||
| pub struct AuthExtractor { | pub struct AuthExtractor { | ||||||
|   | |||||||
| @@ -1,19 +1,18 @@ | |||||||
| use actix_cors::Cors; | use actix_cors::Cors; | ||||||
|  | use actix_identity::config::LogoutBehaviour; | ||||||
| use actix_identity::IdentityMiddleware; | use actix_identity::IdentityMiddleware; | ||||||
| use actix_identity::config::LogoutBehavior; |  | ||||||
| use actix_remote_ip::RemoteIPConfig; | use actix_remote_ip::RemoteIPConfig; | ||||||
| use actix_session::SessionMiddleware; |  | ||||||
| use actix_session::storage::CookieSessionStore; | use actix_session::storage::CookieSessionStore; | ||||||
|  | use actix_session::SessionMiddleware; | ||||||
| use actix_web::cookie::{Key, SameSite}; | use actix_web::cookie::{Key, SameSite}; | ||||||
| use actix_web::middleware::Logger; | use actix_web::middleware::Logger; | ||||||
| use actix_web::web::Data; | use actix_web::web::Data; | ||||||
| use actix_web::{App, HttpServer, web}; | use actix_web::{web, App, HttpServer}; | ||||||
| use light_openid::basic_state_manager::BasicStateManager; | use light_openid::basic_state_manager::BasicStateManager; | ||||||
| use remote_backend::app_config::AppConfig; | use remote_backend::app_config::AppConfig; | ||||||
| use remote_backend::constants; | use remote_backend::constants; | ||||||
| use remote_backend::controllers::{ | use remote_backend::controllers::{ | ||||||
|     auth_controller, group_controller, server_controller, static_controller, sys_info_controller, |     auth_controller, server_controller, static_controller, sys_info_controller, vm_controller, | ||||||
|     vm_controller, |  | ||||||
| }; | }; | ||||||
| use remote_backend::middlewares::auth_middleware::AuthChecker; | use remote_backend::middlewares::auth_middleware::AuthChecker; | ||||||
| use std::time::Duration; | use std::time::Duration; | ||||||
| @@ -37,7 +36,7 @@ async fn main() -> std::io::Result<()> { | |||||||
|         .build(); |         .build(); | ||||||
|  |  | ||||||
|         let identity_middleware = IdentityMiddleware::builder() |         let identity_middleware = IdentityMiddleware::builder() | ||||||
|             .logout_behavior(LogoutBehavior::PurgeSession) |             .logout_behaviour(LogoutBehaviour::PurgeSession) | ||||||
|             .visit_deadline(Some(Duration::from_secs( |             .visit_deadline(Some(Duration::from_secs( | ||||||
|                 constants::MAX_INACTIVITY_DURATION, |                 constants::MAX_INACTIVITY_DURATION, | ||||||
|             ))) |             ))) | ||||||
| @@ -83,44 +82,8 @@ async fn main() -> std::io::Result<()> { | |||||||
|                 "/api/auth/sign_out", |                 "/api/auth/sign_out", | ||||||
|                 web::get().to(auth_controller::sign_out), |                 web::get().to(auth_controller::sign_out), | ||||||
|             ) |             ) | ||||||
|             .route( |  | ||||||
|                 "/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 |             // VM routes | ||||||
|  |             .route("/api/vm/list", web::get().to(vm_controller::list)) | ||||||
|             .route("/api/vm/{uid}/state", web::get().to(vm_controller::state)) |             .route("/api/vm/{uid}/state", web::get().to(vm_controller::state)) | ||||||
|             .route("/api/vm/{uid}/start", web::get().to(vm_controller::start)) |             .route("/api/vm/{uid}/start", web::get().to(vm_controller::start)) | ||||||
|             .route( |             .route( | ||||||
| @@ -139,6 +102,10 @@ async fn main() -> std::io::Result<()> { | |||||||
|                 web::get().to(vm_controller::screenshot), |                 web::get().to(vm_controller::screenshot), | ||||||
|             ) |             ) | ||||||
|             // Sys info routes |             // Sys info routes | ||||||
|  |             .route( | ||||||
|  |                 "/api/sysinfo/config", | ||||||
|  |                 web::get().to(sys_info_controller::config), | ||||||
|  |             ) | ||||||
|             .route( |             .route( | ||||||
|                 "/api/sysinfo/status", |                 "/api/sysinfo/status", | ||||||
|                 web::get().to(sys_info_controller::status), |                 web::get().to(sys_info_controller::status), | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| use std::future::{Ready, ready}; | use std::future::{ready, Ready}; | ||||||
| use std::rc::Rc; | use std::rc::Rc; | ||||||
|  |  | ||||||
| use crate::app_config::AppConfig; | use crate::app_config::AppConfig; | ||||||
| @@ -7,8 +7,8 @@ use crate::extractors::auth_extractor::AuthExtractor; | |||||||
| use actix_web::body::EitherBody; | use actix_web::body::EitherBody; | ||||||
| use actix_web::dev::Payload; | use actix_web::dev::Payload; | ||||||
| use actix_web::{ | use actix_web::{ | ||||||
|  |     dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, | ||||||
|     Error, FromRequest, HttpResponse, |     Error, FromRequest, HttpResponse, | ||||||
|     dev::{Service, ServiceRequest, ServiceResponse, Transform, forward_ready}, |  | ||||||
| }; | }; | ||||||
| use futures_util::future::LocalBoxFuture; | use futures_util::future::LocalBoxFuture; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,7 +1,6 @@ | |||||||
| use crate::app_config::AppConfig; | use crate::app_config::AppConfig; | ||||||
| use crate::utils::time; | use crate::utils::time; | ||||||
| use lazy_regex::regex; | use lazy_regex::regex; | ||||||
| use std::collections::HashMap; |  | ||||||
| use std::fmt::Display; | use std::fmt::Display; | ||||||
| use std::str::FromStr; | use std::str::FromStr; | ||||||
| use thiserror::Error; | use thiserror::Error; | ||||||
| @@ -13,105 +12,9 @@ pub enum VirtWebClientError { | |||||||
|     InvalidStatusCode(u16), |     InvalidStatusCode(u16), | ||||||
| } | } | ||||||
|  |  | ||||||
| #[derive(Eq, PartialEq, Debug, Clone, serde::Serialize, serde::Deserialize)] | #[derive(Eq, PartialEq, Debug, Copy, 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); | pub struct VMUuid(Uuid); | ||||||
|  |  | ||||||
| #[derive(Default, serde::Deserialize, serde::Serialize)] |  | ||||||
| pub struct TreatmentResult { |  | ||||||
|     ok: usize, |  | ||||||
|     failed: usize, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| impl VMUuid { | impl VMUuid { | ||||||
|     pub fn route_info(&self) -> String { |     pub fn route_info(&self) -> String { | ||||||
|         format!("/api/vm/{}", self.0) |         format!("/api/vm/{}", self.0) | ||||||
| @@ -166,7 +69,7 @@ pub struct TokenClaims { | |||||||
|     pub nonce: String, |     pub nonce: String, | ||||||
| } | } | ||||||
|  |  | ||||||
| #[derive(serde::Deserialize, serde::Serialize, Debug)] | #[derive(serde::Deserialize, Debug)] | ||||||
| pub struct VMInfo { | pub struct VMInfo { | ||||||
|     pub uuid: VMUuid, |     pub uuid: VMUuid, | ||||||
|     pub name: String, |     pub name: String, | ||||||
| @@ -176,18 +79,6 @@ pub struct VMInfo { | |||||||
|     pub number_vcpu: usize, |     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)] | #[derive(serde::Deserialize, serde::Serialize, Debug)] | ||||||
| pub struct VMState { | pub struct VMState { | ||||||
|     pub state: String, |     pub state: String, | ||||||
| @@ -256,16 +147,6 @@ impl TokenInfo { | |||||||
|         false |         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 |     /// List the virtual machines with access | ||||||
|     pub fn list_vm(&self) -> Vec<VMUuid> { |     pub fn list_vm(&self) -> Vec<VMUuid> { | ||||||
|         self.rights |         self.rights | ||||||
| @@ -287,13 +168,12 @@ async fn request<D: Display>(uri: D) -> anyhow::Result<reqwest::Response> { | |||||||
|     let url = format!("{}{}", AppConfig::get().virtweb_base_url, uri); |     let url = format!("{}{}", AppConfig::get().virtweb_base_url, uri); | ||||||
|     log::debug!("Will query {uri}..."); |     log::debug!("Will query {uri}..."); | ||||||
|  |  | ||||||
|     let uri = uri.to_string(); |  | ||||||
|     let jwt = TokenClaims { |     let jwt = TokenClaims { | ||||||
|         sub: AppConfig::get().virtweb_token_id.to_string(), |         sub: AppConfig::get().virtweb_token_id.to_string(), | ||||||
|         iat: time() - 60 * 2, |         iat: time() - 60 * 2, | ||||||
|         exp: time() + 60 * 3, |         exp: time() + 60 * 3, | ||||||
|         verb: "GET".to_string(), |         verb: "GET".to_string(), | ||||||
|         path: uri.split_once('?').map(|s| s.0).unwrap_or(&uri).to_string(), |         path: uri.to_string(), | ||||||
|         nonce: Uuid::new_v4().to_string(), |         nonce: Uuid::new_v4().to_string(), | ||||||
|     }; |     }; | ||||||
|     let jwt = AppConfig::get().token_private_key().sign_jwt(&jwt)?; |     let jwt = AppConfig::get().token_private_key().sign_jwt(&jwt)?; | ||||||
| @@ -380,73 +260,6 @@ pub async fn vm_screenshot(id: VMUuid) -> anyhow::Result<Vec<u8>> { | |||||||
|         .to_vec()) |         .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 | /// Get current server information | ||||||
| pub async fn get_server_info() -> anyhow::Result<SystemInfo> { | pub async fn get_server_info() -> anyhow::Result<SystemInfo> { | ||||||
|     json_request("/api/server/info").await |     json_request("/api/server/info").await | ||||||
|   | |||||||
							
								
								
									
										18
									
								
								remote_frontend/.eslintrc.cjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								remote_frontend/.eslintrc.cjs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | |||||||
|  | module.exports = { | ||||||
|  |   root: true, | ||||||
|  |   env: { browser: true, es2020: true }, | ||||||
|  |   extends: [ | ||||||
|  |     'eslint:recommended', | ||||||
|  |     'plugin:@typescript-eslint/recommended', | ||||||
|  |     'plugin:react-hooks/recommended', | ||||||
|  |   ], | ||||||
|  |   ignorePatterns: ['dist', '.eslintrc.cjs'], | ||||||
|  |   parser: '@typescript-eslint/parser', | ||||||
|  |   plugins: ['react-refresh'], | ||||||
|  |   rules: { | ||||||
|  |     'react-refresh/only-export-components': [ | ||||||
|  |       'warn', | ||||||
|  |       { allowConstantExport: true }, | ||||||
|  |     ], | ||||||
|  |   }, | ||||||
|  | } | ||||||
| @@ -1,28 +0,0 @@ | |||||||
| import js from '@eslint/js' |  | ||||||
| import globals from 'globals' |  | ||||||
| import reactHooks from 'eslint-plugin-react-hooks' |  | ||||||
| import reactRefresh from 'eslint-plugin-react-refresh' |  | ||||||
| import tseslint from 'typescript-eslint' |  | ||||||
|  |  | ||||||
| export default tseslint.config( |  | ||||||
|   { ignores: ['dist'] }, |  | ||||||
|   { |  | ||||||
|     extends: [js.configs.recommended, ...tseslint.configs.recommended], |  | ||||||
|     files: ['**/*.{ts,tsx}'], |  | ||||||
|     languageOptions: { |  | ||||||
|       ecmaVersion: 2020, |  | ||||||
|       globals: globals.browser, |  | ||||||
|     }, |  | ||||||
|     plugins: { |  | ||||||
|       'react-hooks': reactHooks, |  | ||||||
|       'react-refresh': reactRefresh, |  | ||||||
|     }, |  | ||||||
|     rules: { |  | ||||||
|       ...reactHooks.configs.recommended.rules, |  | ||||||
|       'react-refresh/only-export-components': [ |  | ||||||
|         'warn', |  | ||||||
|         { allowConstantExport: true }, |  | ||||||
|       ], |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
| ) |  | ||||||
							
								
								
									
										5197
									
								
								remote_frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5197
									
								
								remote_frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -10,25 +10,22 @@ | |||||||
|     "preview": "vite preview" |     "preview": "vite preview" | ||||||
|   }, |   }, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@fluentui/react-components": "^9.72.3", |     "@fluentui/react-components": "^9.49.2", | ||||||
|     "@fluentui/react-icons": "^2.0.313", |     "@fluentui/react-icons": "^2.0.239", | ||||||
|     "filesize": "^11.0.13", |     "filesize": "^10.1.1", | ||||||
|     "react": "^19.2.0", |     "react": "^18.2.0", | ||||||
|     "react-dom": "^19.2.0" |     "react-dom": "^18.2.0" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@eslint/js": "^9.38.0", |     "@types/react": "^18.2.66", | ||||||
|     "@types/react": "^19.2.2", |     "@types/react-dom": "^18.2.22", | ||||||
|     "@types/react-dom": "^19.2.2", |     "@typescript-eslint/eslint-plugin": "^7.2.0", | ||||||
|     "@typescript-eslint/eslint-plugin": "^8.46.2", |     "@typescript-eslint/parser": "^7.2.0", | ||||||
|     "@typescript-eslint/parser": "^8.46.2", |     "@vitejs/plugin-react": "^4.2.1", | ||||||
|     "@vitejs/plugin-react": "^5.1.0", |     "eslint": "^8.57.0", | ||||||
|     "eslint": "^9.38.0", |     "eslint-plugin-react-hooks": "^4.6.0", | ||||||
|     "eslint-plugin-react-hooks": "^5.2.0", |     "eslint-plugin-react-refresh": "^0.4.6", | ||||||
|     "eslint-plugin-react-refresh": "^0.4.24", |     "typescript": "^5.2.2", | ||||||
|     "globals": "^16.4.0", |     "vite": "^5.2.11" | ||||||
|     "typescript": "^5.9.3", |  | ||||||
|     "typescript-eslint": "^8.43.0", |  | ||||||
|     "vite": "^7.1.12" |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -5,8 +5,6 @@ import { | |||||||
|   typographyStyles, |   typographyStyles, | ||||||
| } from "@fluentui/react-components"; | } from "@fluentui/react-components"; | ||||||
| import { | import { | ||||||
|   AppsListDetailFilled, |  | ||||||
|   AppsListDetailRegular, |  | ||||||
|   DesktopFilled, |   DesktopFilled, | ||||||
|   DesktopRegular, |   DesktopRegular, | ||||||
|   InfoFilled, |   InfoFilled, | ||||||
| @@ -14,13 +12,12 @@ import { | |||||||
|   bundleIcon, |   bundleIcon, | ||||||
| } from "@fluentui/react-icons"; | } from "@fluentui/react-icons"; | ||||||
| import React from "react"; | import React from "react"; | ||||||
| import { Rights, ServerApi } from "./api/ServerApi"; | import { ServerApi } from "./api/ServerApi"; | ||||||
| import { AuthRouteWidget } from "./routes/AuthRouteWidget"; | import { AuthRouteWidget } from "./routes/AuthRouteWidget"; | ||||||
| import { AsyncWidget } from "./widgets/AsyncWidget"; | import { AsyncWidget } from "./widgets/AsyncWidget"; | ||||||
| import { MainMenu } from "./widgets/MainMenu"; | import { MainMenu } from "./widgets/MainMenu"; | ||||||
| import { SystemInfoWidget } from "./widgets/SystemInfoWidget"; | import { SystemInfoWidget } from "./widgets/SystemInfoWidget"; | ||||||
| import { VirtualMachinesWidget } from "./widgets/VirtualMachinesWidget"; | import { VirtualMachinesWidget } from "./widgets/VirtualMachinesWidget"; | ||||||
| import { GroupsWidget } from "./widgets/GroupsWidget"; |  | ||||||
|  |  | ||||||
| const useStyles = makeStyles({ | const useStyles = makeStyles({ | ||||||
|   title: typographyStyles.title2, |   title: typographyStyles.title2, | ||||||
| @@ -30,8 +27,6 @@ const InfoIcon = bundleIcon(InfoFilled, InfoRegular); | |||||||
|  |  | ||||||
| const DesktopIcon = bundleIcon(DesktopFilled, DesktopRegular); | const DesktopIcon = bundleIcon(DesktopFilled, DesktopRegular); | ||||||
|  |  | ||||||
| const AppListIcon = bundleIcon(AppsListDetailFilled, AppsListDetailRegular); |  | ||||||
|  |  | ||||||
| export function App() { | export function App() { | ||||||
|   return ( |   return ( | ||||||
|     <AsyncWidget |     <AsyncWidget | ||||||
| @@ -45,79 +40,45 @@ export function App() { | |||||||
| } | } | ||||||
|  |  | ||||||
| function AppInner(): React.ReactElement { | function AppInner(): React.ReactElement { | ||||||
|  |   const styles = useStyles(); | ||||||
|  |   const [tab, setTab] = React.useState<"vm" | "info">("vm"); | ||||||
|  |  | ||||||
|   if (!ServerApi.Config.authenticated && !ServerApi.Config.disable_auth) |   if (!ServerApi.Config.authenticated && !ServerApi.Config.disable_auth) | ||||||
|     return <AuthRouteWidget />; |     return <AuthRouteWidget />; | ||||||
|  |  | ||||||
|   return <AuthenticatedApp />; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function AuthenticatedApp(): React.ReactElement { |  | ||||||
|   const styles = useStyles(); |  | ||||||
|   const [tab, setTab] = React.useState<"group" | "vm" | "info">("group"); |  | ||||||
|  |  | ||||||
|   const [rights, setRights] = React.useState<Rights | undefined>(); |  | ||||||
|  |  | ||||||
|   const load = async () => { |  | ||||||
|     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 ( |   return ( | ||||||
|     <AsyncWidget |     <div | ||||||
|       loadKey={1} |       style={{ | ||||||
|       load={load} |         width: "95%", | ||||||
|       errMsg="Failed to retrieve application rights!" |         maxWidth: "1000px", | ||||||
|       build={() => { |         margin: "50px auto", | ||||||
|         return ( |  | ||||||
|           <div |  | ||||||
|             style={{ |  | ||||||
|               width: "95%", |  | ||||||
|               maxWidth: "1000px", |  | ||||||
|               margin: "50px auto", |  | ||||||
|             }} |  | ||||||
|           > |  | ||||||
|             <span className={styles.title}>VirtWebRemote</span> |  | ||||||
|             <div |  | ||||||
|               style={{ |  | ||||||
|                 display: "flex", |  | ||||||
|                 justifyContent: "space-between", |  | ||||||
|                 marginTop: "30px", |  | ||||||
|               }} |  | ||||||
|             > |  | ||||||
|               <TabList |  | ||||||
|                 selectedValue={tab} |  | ||||||
|                 onTabSelect={(_, d) => setTab(d.value as any)} |  | ||||||
|               > |  | ||||||
|                 {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> |  | ||||||
|         ); |  | ||||||
|       }} |       }} | ||||||
|     /> |     > | ||||||
|  |       <span className={styles.title}>VirtWebRemote</span> | ||||||
|  |       <div | ||||||
|  |         style={{ | ||||||
|  |           display: "flex", | ||||||
|  |           justifyContent: "space-between", | ||||||
|  |           marginTop: "30px", | ||||||
|  |         }} | ||||||
|  |       > | ||||||
|  |         <TabList | ||||||
|  |           selectedValue={tab} | ||||||
|  |           onTabSelect={(_, d) => setTab(d.value as any)} | ||||||
|  |         > | ||||||
|  |           <Tab value="vm" icon={<DesktopIcon />}> | ||||||
|  |             Virtual machines | ||||||
|  |           </Tab> | ||||||
|  |           <Tab value="info" icon={<InfoIcon />}> | ||||||
|  |             System info | ||||||
|  |           </Tab> | ||||||
|  |         </TabList> | ||||||
|  |         <div> | ||||||
|  |           <MainMenu /> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |       {tab === "vm" && <VirtualMachinesWidget />} | ||||||
|  |       {tab === "info" && <SystemInfoWidget />} | ||||||
|  |     </div> | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,107 +0,0 @@ | |||||||
| 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,24 +1,10 @@ | |||||||
| import { APIClient } from "./ApiClient"; | import { APIClient } from "./ApiClient"; | ||||||
| import { VMCaps, VMInfo, VMInfoAndCaps } from "./VMApi"; |  | ||||||
|  |  | ||||||
| export interface ServerConfig { | export interface ServerConfig { | ||||||
|   authenticated: boolean; |   authenticated: boolean; | ||||||
|   disable_auth: boolean; |   disable_auth: boolean; | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface Rights { |  | ||||||
|   groups: VMGroup[]; |  | ||||||
|   vms: VMInfoAndCaps[]; |  | ||||||
|   sys_info: boolean; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export type VMGroup = VMGroupInfo & VMCaps; |  | ||||||
|  |  | ||||||
| export interface VMGroupInfo { |  | ||||||
|   id: string; |  | ||||||
|   vms: VMInfo[]; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| let config: ServerConfig | null = null; | let config: ServerConfig | null = null; | ||||||
|  |  | ||||||
| export class ServerApi { | export class ServerApi { | ||||||
| @@ -41,16 +27,4 @@ export class ServerApi { | |||||||
|     if (config === null) throw new Error("Missing configuration!"); |     if (config === null) throw new Error("Missing configuration!"); | ||||||
|     return config; |     return config; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * Get application rights |  | ||||||
|    */ |  | ||||||
|   static async GetRights(): Promise<Rights> { |  | ||||||
|     return ( |  | ||||||
|       await APIClient.exec({ |  | ||||||
|         uri: "/server/rights", |  | ||||||
|         method: "GET", |  | ||||||
|       }) |  | ||||||
|     ).data; |  | ||||||
|   } |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,5 +1,9 @@ | |||||||
| import { APIClient } from "./ApiClient"; | import { APIClient } from "./ApiClient"; | ||||||
|  |  | ||||||
|  | export interface SysInfoConfig { | ||||||
|  |   allowed: boolean; | ||||||
|  | } | ||||||
|  |  | ||||||
| export interface LoadAverage { | export interface LoadAverage { | ||||||
|   one: number; |   one: number; | ||||||
|   five: number; |   five: number; | ||||||
| @@ -20,6 +24,14 @@ export interface SysInfoStatus { | |||||||
| } | } | ||||||
|  |  | ||||||
| export class SysInfoApi { | export class SysInfoApi { | ||||||
|  |   /** | ||||||
|  |    * Get system info configuration (ie. check if it allowed) | ||||||
|  |    */ | ||||||
|  |   static async GetConfig(): Promise<SysInfoConfig> { | ||||||
|  |     return (await APIClient.exec({ method: "GET", uri: "/sysinfo/config" })) | ||||||
|  |       .data; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Get system status |    * Get system status | ||||||
|    */ |    */ | ||||||
|   | |||||||
| @@ -1,15 +1,12 @@ | |||||||
| import { APIClient } from "./ApiClient"; | import { APIClient } from "./ApiClient"; | ||||||
|  |  | ||||||
| export interface VMInfo { | export interface VMInfo { | ||||||
|   uuid: string; |   uiid: string; | ||||||
|   name: string; |   name: string; | ||||||
|   description?: string; |   description?: string; | ||||||
|   architecture: string; |   architecture: string; | ||||||
|   memory: number; |   memory: number; | ||||||
|   number_vcpu: number; |   number_vcpu: number; | ||||||
| } |  | ||||||
|  |  | ||||||
| export interface VMCaps { |  | ||||||
|   can_get_state: boolean; |   can_get_state: boolean; | ||||||
|   can_start: boolean; |   can_start: boolean; | ||||||
|   can_shutdown: boolean; |   can_shutdown: boolean; | ||||||
| @@ -20,8 +17,6 @@ export interface VMCaps { | |||||||
|   can_screenshot: boolean; |   can_screenshot: boolean; | ||||||
| } | } | ||||||
|  |  | ||||||
| export type VMInfoAndCaps = VMInfo & VMCaps; |  | ||||||
|  |  | ||||||
| export type VMState = | export type VMState = | ||||||
|   | "NoState" |   | "NoState" | ||||||
|   | "Running" |   | "Running" | ||||||
| @@ -34,12 +29,19 @@ export type VMState = | |||||||
|   | "Other"; |   | "Other"; | ||||||
|  |  | ||||||
| export class VMApi { | export class VMApi { | ||||||
|  |   /** | ||||||
|  |    * Get the list of VM that can be managed by this console | ||||||
|  |    */ | ||||||
|  |   static async GetList(): Promise<VMInfo[]> { | ||||||
|  |     return (await APIClient.exec({ method: "GET", uri: "/vm/list" })).data; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Get the state of a VM |    * Get the state of a VM | ||||||
|    */ |    */ | ||||||
|   static async State(vm: VMInfo): Promise<VMState> { |   static async State(vm: VMInfo): Promise<VMState> { | ||||||
|     return ( |     return ( | ||||||
|       await APIClient.exec({ method: "GET", uri: `/vm/${vm.uuid}/state` }) |       await APIClient.exec({ method: "GET", uri: `/vm/${vm.uiid}/state` }) | ||||||
|     ).data.state; |     ).data.state; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -47,42 +49,42 @@ export class VMApi { | |||||||
|    * Request to start VM |    * Request to start VM | ||||||
|    */ |    */ | ||||||
|   static async StartVM(vm: VMInfo): Promise<void> { |   static async StartVM(vm: VMInfo): Promise<void> { | ||||||
|     await APIClient.exec({ method: "GET", uri: `/vm/${vm.uuid}/start` }); |     await APIClient.exec({ method: "GET", uri: `/vm/${vm.uiid}/start` }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Request to suspend VM |    * Request to suspend VM | ||||||
|    */ |    */ | ||||||
|   static async SuspendVM(vm: VMInfo): Promise<void> { |   static async SuspendVM(vm: VMInfo): Promise<void> { | ||||||
|     await APIClient.exec({ method: "GET", uri: `/vm/${vm.uuid}/suspend` }); |     await APIClient.exec({ method: "GET", uri: `/vm/${vm.uiid}/suspend` }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Request to resume VM |    * Request to resume VM | ||||||
|    */ |    */ | ||||||
|   static async ResumeVM(vm: VMInfo): Promise<void> { |   static async ResumeVM(vm: VMInfo): Promise<void> { | ||||||
|     await APIClient.exec({ method: "GET", uri: `/vm/${vm.uuid}/resume` }); |     await APIClient.exec({ method: "GET", uri: `/vm/${vm.uiid}/resume` }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Request to shutdown VM |    * Request to shutdown VM | ||||||
|    */ |    */ | ||||||
|   static async ShutdownVM(vm: VMInfo): Promise<void> { |   static async ShutdownVM(vm: VMInfo): Promise<void> { | ||||||
|     await APIClient.exec({ method: "GET", uri: `/vm/${vm.uuid}/shutdown` }); |     await APIClient.exec({ method: "GET", uri: `/vm/${vm.uiid}/shutdown` }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Request to kill VM |    * Request to kill VM | ||||||
|    */ |    */ | ||||||
|   static async KillVM(vm: VMInfo): Promise<void> { |   static async KillVM(vm: VMInfo): Promise<void> { | ||||||
|     await APIClient.exec({ method: "GET", uri: `/vm/${vm.uuid}/kill` }); |     await APIClient.exec({ method: "GET", uri: `/vm/${vm.uiid}/kill` }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Request to reset VM |    * Request to reset VM | ||||||
|    */ |    */ | ||||||
|   static async ResetVM(vm: VMInfo): Promise<void> { |   static async ResetVM(vm: VMInfo): Promise<void> { | ||||||
|     await APIClient.exec({ method: "GET", uri: `/vm/${vm.uuid}/reset` }); |     await APIClient.exec({ method: "GET", uri: `/vm/${vm.uiid}/reset` }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
| @@ -91,7 +93,7 @@ export class VMApi { | |||||||
|   static async Screenshot(vm: VMInfo): Promise<Blob> { |   static async Screenshot(vm: VMInfo): Promise<Blob> { | ||||||
|     return ( |     return ( | ||||||
|       await APIClient.exec({ |       await APIClient.exec({ | ||||||
|         uri: `/vm/${vm.uuid}/screenshot`, |         uri: `/vm/${vm.uiid}/screenshot`, | ||||||
|         method: "GET", |         method: "GET", | ||||||
|       }) |       }) | ||||||
|     ).data; |     ).data; | ||||||
|   | |||||||
| @@ -20,7 +20,7 @@ type ThemeContext = { theme: Theme; set: (theme: Theme) => void }; | |||||||
| const ThemeContextK = React.createContext<ThemeContext | null>(null); | const ThemeContextK = React.createContext<ThemeContext | null>(null); | ||||||
|  |  | ||||||
| export function ThemeProvider(p: React.PropsWithChildren): React.ReactElement { | export function ThemeProvider(p: React.PropsWithChildren): React.ReactElement { | ||||||
|   const [theme, setTheme] = React.useState<Theme>("teamsdark"); |   const [theme, setTheme] = React.useState<Theme>("highcontrast"); | ||||||
|  |  | ||||||
|   let fluentTheme = teamsHighContrastTheme; |   let fluentTheme = teamsHighContrastTheme; | ||||||
|   switch (theme) { |   switch (theme) { | ||||||
|   | |||||||
| @@ -1,177 +0,0 @@ | |||||||
| 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> |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
| @@ -1,171 +0,0 @@ | |||||||
| 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)} •{" "} |  | ||||||
|                   {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,13 +1,47 @@ | |||||||
| import { Field, ProgressBar } from "@fluentui/react-components"; |  | ||||||
| import { filesize } from "filesize"; |  | ||||||
| import React from "react"; | import React from "react"; | ||||||
| import { SysInfoApi, SysInfoStatus } from "../api/SysInfoApi"; | import { SysInfoApi, SysInfoConfig, SysInfoStatus } from "../api/SysInfoApi"; | ||||||
| import { useToast } from "../hooks/providers/ToastProvider"; |  | ||||||
| import { format_duration } from "../utils/time_utils"; |  | ||||||
| import { AsyncWidget } from "./AsyncWidget"; | import { AsyncWidget } from "./AsyncWidget"; | ||||||
| import { SectionContainer } from "./SectionContainer"; | import { SectionContainer } from "./SectionContainer"; | ||||||
|  | import { Field, ProgressBar } from "@fluentui/react-components"; | ||||||
|  | import { filesize } from "filesize"; | ||||||
|  | import { format_duration } from "../utils/time_utils"; | ||||||
|  | import { useToast } from "../hooks/providers/ToastProvider"; | ||||||
|  |  | ||||||
| export function SystemInfoWidget(): React.ReactElement { | export function SystemInfoWidget(): React.ReactElement { | ||||||
|  |   const [config, setConfig] = React.useState<SysInfoConfig | undefined>(); | ||||||
|  |  | ||||||
|  |   const load = async () => { | ||||||
|  |     setConfig(await SysInfoApi.GetConfig()); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <SectionContainer> | ||||||
|  |       <AsyncWidget | ||||||
|  |         loadKey={1} | ||||||
|  |         load={load} | ||||||
|  |         errMsg="Failed to check system configuration!" | ||||||
|  |         loadingMessage="Checking server configuration..." | ||||||
|  |         build={() => | ||||||
|  |           config?.allowed ? ( | ||||||
|  |             <SystemInfoWidgetInner /> | ||||||
|  |           ) : ( | ||||||
|  |             <SystemInfoWidgetUnavailable /> | ||||||
|  |           ) | ||||||
|  |         } | ||||||
|  |       /> | ||||||
|  |     </SectionContainer> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function SystemInfoWidgetUnavailable(): React.ReactElement { | ||||||
|  |   return ( | ||||||
|  |     <p style={{ textAlign: "center" }}> | ||||||
|  |       Unfortunatley, system information is available. (not enough privileges) | ||||||
|  |     </p> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function SystemInfoWidgetInner(): React.ReactElement { | ||||||
|   const toast = useToast(); |   const toast = useToast(); | ||||||
|  |  | ||||||
|   const [status, setStatus] = React.useState<SysInfoStatus | undefined>(); |   const [status, setStatus] = React.useState<SysInfoStatus | undefined>(); | ||||||
| @@ -29,51 +63,49 @@ export function SystemInfoWidget(): React.ReactElement { | |||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <SectionContainer> |     <AsyncWidget | ||||||
|       <AsyncWidget |       loadKey={1} | ||||||
|         loadKey={1} |       load={load} | ||||||
|         load={load} |       loadingMessage="Loading system status..." | ||||||
|         loadingMessage="Loading system status..." |       errMsg="Failed to load system status!" | ||||||
|         errMsg="Failed to load system status!" |       build={() => ( | ||||||
|         build={() => ( |         <div | ||||||
|           <div |           style={{ | ||||||
|             style={{ |             display: "flex", | ||||||
|               display: "flex", |             flexDirection: "row", | ||||||
|               flexDirection: "row", |             alignItems: "center", | ||||||
|               alignItems: "center", |             justifyContent: "center", | ||||||
|               justifyContent: "center", |           }} | ||||||
|             }} |         > | ||||||
|  |           <Field | ||||||
|  |             validationMessage={`${filesize( | ||||||
|  |               status!.system.used_memory | ||||||
|  |             )} of memory used out of ${filesize( | ||||||
|  |               status!.system.available_memory + status!.system.used_memory | ||||||
|  |             )}`} | ||||||
|  |             validationState="none" | ||||||
|  |             style={{ flex: 2 }} | ||||||
|           > |           > | ||||||
|             <Field |             <ProgressBar | ||||||
|               validationMessage={`${filesize( |               value={ | ||||||
|                 status!.system.used_memory |                 status!.system.used_memory / | ||||||
|               )} of memory used out of ${filesize( |                 (status!.system.available_memory + status!.system.used_memory) | ||||||
|                 status!.system.available_memory + status!.system.used_memory |               } | ||||||
|               )}`} |             /> | ||||||
|               validationState="none" |           </Field> | ||||||
|               style={{ flex: 2 }} |           <div style={{ width: "20px" }}></div> | ||||||
|             > |           <div style={{ flex: 1 }}> | ||||||
|               <ProgressBar |             <p> | ||||||
|                 value={ |               Load average: {status!.system.load_average.one}{" "} | ||||||
|                   status!.system.used_memory / |               {status!.system.load_average.five}{" "} | ||||||
|                   (status!.system.available_memory + status!.system.used_memory) |               {status!.system.load_average.fifteen} | ||||||
|                 } |             </p> | ||||||
|               /> |             <UptimeWidget uptime={status!.system.uptime} /> | ||||||
|             </Field> |             Number physical cores: {status!.system.physical_core_count} | ||||||
|             <div style={{ width: "20px" }}></div> |  | ||||||
|             <div style={{ flex: 1 }}> |  | ||||||
|               <p> |  | ||||||
|                 Load average: {status!.system.load_average.one}{" "} |  | ||||||
|                 {status!.system.load_average.five}{" "} |  | ||||||
|                 {status!.system.load_average.fifteen} |  | ||||||
|               </p> |  | ||||||
|               <UptimeWidget uptime={status!.system.uptime} /> |  | ||||||
|               Number physical cores: {status!.system.physical_core_count} |  | ||||||
|             </div> |  | ||||||
|           </div> |           </div> | ||||||
|         )} |         </div> | ||||||
|       /> |       )} | ||||||
|     </SectionContainer> |     /> | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,13 +1,8 @@ | |||||||
| import React from "react"; | import React from "react"; | ||||||
| import { GroupApi } from "../api/GroupApi"; |  | ||||||
| import { VMGroup } from "../api/ServerApi"; |  | ||||||
| import { VMApi, VMInfo } from "../api/VMApi"; | import { VMApi, VMInfo } from "../api/VMApi"; | ||||||
| import { useToast } from "../hooks/providers/ToastProvider"; | import { useToast } from "../hooks/providers/ToastProvider"; | ||||||
|  |  | ||||||
| export function VMLiveScreenshot(p: { | export function VMLiveScreenshot(p: { vm: VMInfo }): React.ReactElement { | ||||||
|   vm: VMInfo; |  | ||||||
|   group?: VMGroup; |  | ||||||
| }): React.ReactElement { |  | ||||||
|   const toast = useToast(); |   const toast = useToast(); | ||||||
|  |  | ||||||
|   const [screenshotURL, setScreenshotURL] = React.useState< |   const [screenshotURL, setScreenshotURL] = React.useState< | ||||||
| @@ -19,9 +14,7 @@ export function VMLiveScreenshot(p: { | |||||||
|   React.useEffect(() => { |   React.useEffect(() => { | ||||||
|     const refresh = async () => { |     const refresh = async () => { | ||||||
|       try { |       try { | ||||||
|         const screenshot = p.group |         const screenshot = await VMApi.Screenshot(p.vm); | ||||||
|           ? await GroupApi.ScreenshotVM(p.group, p.vm) |  | ||||||
|           : await VMApi.Screenshot(p.vm); |  | ||||||
|         const u = URL.createObjectURL(screenshot); |         const u = URL.createObjectURL(screenshot); | ||||||
|         setScreenshotURL(u); |         setScreenshotURL(u); | ||||||
|       } catch (e) { |       } catch (e) { | ||||||
|   | |||||||
| @@ -21,10 +21,10 @@ import { | |||||||
| } from "@fluentui/react-icons"; | } from "@fluentui/react-icons"; | ||||||
| import { filesize } from "filesize"; | import { filesize } from "filesize"; | ||||||
| import React from "react"; | 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 { useConfirm } from "../hooks/providers/ConfirmDialogProvider"; | ||||||
| import { useToast } from "../hooks/providers/ToastProvider"; | import { useToast } from "../hooks/providers/ToastProvider"; | ||||||
|  | import { AsyncWidget } from "./AsyncWidget"; | ||||||
| import { SectionContainer } from "./SectionContainer"; | import { SectionContainer } from "./SectionContainer"; | ||||||
| import { VMLiveScreenshot } from "./VMLiveScreenshot"; | import { VMLiveScreenshot } from "./VMLiveScreenshot"; | ||||||
|  |  | ||||||
| @@ -33,28 +33,43 @@ const useStyles = makeStyles({ | |||||||
|   caption1: typographyStyles.caption1, |   caption1: typographyStyles.caption1, | ||||||
| }); | }); | ||||||
|  |  | ||||||
| export function VirtualMachinesWidget(p: { | export function VirtualMachinesWidget(): React.ReactElement { | ||||||
|   rights: Rights; |   const [list, setList] = React.useState<VMInfo[] | undefined>(); | ||||||
| }): React.ReactElement { |   const load = async () => { | ||||||
|  |     setList(await VMApi.GetList()); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <SectionContainer> |     <SectionContainer> | ||||||
|       <div |       <AsyncWidget | ||||||
|         style={{ |         loadKey={1} | ||||||
|           display: "flex", |         load={load} | ||||||
|           flexDirection: "row", |         loadingMessage="Loading the list virtual machines..." | ||||||
|           flexWrap: "wrap", |         errMsg="Failed to load the list of virtual machines!" | ||||||
|           justifyContent: "center", |         build={() => <VirtualMachinesWidgetInner list={list!} />} | ||||||
|         }} |       /> | ||||||
|       > |  | ||||||
|         {p.rights.vms.map((v, n) => ( |  | ||||||
|           <VMWidget key={n} vm={v} /> |  | ||||||
|         ))} |  | ||||||
|       </div> |  | ||||||
|     </SectionContainer> |     </SectionContainer> | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|  |  | ||||||
| function VMWidget(p: { vm: VMInfoAndCaps }): React.ReactElement { | function VirtualMachinesWidgetInner(p: { list: VMInfo[] }): React.ReactElement { | ||||||
|  |   return ( | ||||||
|  |     <div | ||||||
|  |       style={{ | ||||||
|  |         display: "flex", | ||||||
|  |         flexDirection: "row", | ||||||
|  |         flexWrap: "wrap", | ||||||
|  |         justifyContent: "center", | ||||||
|  |       }} | ||||||
|  |     > | ||||||
|  |       {p.list.map((v, n) => ( | ||||||
|  |         <VMWidget key={n} vm={v} /> | ||||||
|  |       ))}{" "} | ||||||
|  |     </div> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function VMWidget(p: { vm: VMInfo }): React.ReactElement { | ||||||
|   const toast = useToast(); |   const toast = useToast(); | ||||||
|  |  | ||||||
|   const [state, setState] = React.useState<VMState | undefined>(); |   const [state, setState] = React.useState<VMState | undefined>(); | ||||||
| @@ -107,7 +122,7 @@ function VMWidget(p: { vm: VMInfoAndCaps }): React.ReactElement { | |||||||
|         } |         } | ||||||
|       /> |       /> | ||||||
|       <p className={styles.caption1} style={{ margin: "0px auto" }}> |       <p className={styles.caption1} style={{ margin: "0px auto" }}> | ||||||
|         {p.vm.architecture} • RAM : {filesize(p.vm.memory)}{" "} |         {p.vm.architecture} • RAM : {filesize(p.vm.memory * 1000 * 1000)}{" "} | ||||||
|         • {p.vm.number_vcpu} vCPU |         • {p.vm.number_vcpu} vCPU | ||||||
|       </p> |       </p> | ||||||
|  |  | ||||||
| @@ -189,10 +204,7 @@ function VMWidget(p: { vm: VMInfoAndCaps }): React.ReactElement { | |||||||
|   ); |   ); | ||||||
| } | } | ||||||
|  |  | ||||||
| function VMPreview(p: { | function VMPreview(p: { vm: VMInfo; state?: VMState }): React.ReactElement { | ||||||
|   vm: VMInfoAndCaps; |  | ||||||
|   state?: VMState; |  | ||||||
| }): React.ReactElement { |  | ||||||
|   const styles = useStyles(); |   const styles = useStyles(); | ||||||
|   if (!p.vm.can_screenshot || p.state !== "Running") { |   if (!p.vm.can_screenshot || p.state !== "Running") { | ||||||
|     return ( |     return ( | ||||||
|   | |||||||
| @@ -1,26 +0,0 @@ | |||||||
| { |  | ||||||
|   "compilerOptions": { |  | ||||||
|     "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", |  | ||||||
|     "target": "ES2020", |  | ||||||
|     "useDefineForClassFields": true, |  | ||||||
|     "lib": ["ES2020", "DOM", "DOM.Iterable"], |  | ||||||
|     "module": "ESNext", |  | ||||||
|     "skipLibCheck": true, |  | ||||||
|  |  | ||||||
|     /* Bundler mode */ |  | ||||||
|     "moduleResolution": "bundler", |  | ||||||
|     "allowImportingTsExtensions": true, |  | ||||||
|     "isolatedModules": true, |  | ||||||
|     "moduleDetection": "force", |  | ||||||
|     "noEmit": true, |  | ||||||
|     "jsx": "react-jsx", |  | ||||||
|  |  | ||||||
|     /* Linting */ |  | ||||||
|     "strict": true, |  | ||||||
|     "noUnusedLocals": true, |  | ||||||
|     "noUnusedParameters": true, |  | ||||||
|     "noFallthroughCasesInSwitch": true, |  | ||||||
|     "noUncheckedSideEffectImports": true |  | ||||||
|   }, |  | ||||||
|   "include": ["src"] |  | ||||||
| } |  | ||||||
| @@ -1,7 +1,25 @@ | |||||||
| { | { | ||||||
|   "files": [], |   "compilerOptions": { | ||||||
|   "references": [ |     "target": "ES2020", | ||||||
|     { "path": "./tsconfig.app.json" }, |     "useDefineForClassFields": true, | ||||||
|     { "path": "./tsconfig.node.json" } |     "lib": ["ES2020", "DOM", "DOM.Iterable"], | ||||||
|   ] |     "module": "ESNext", | ||||||
|  |     "skipLibCheck": true, | ||||||
|  |  | ||||||
|  |     /* Bundler mode */ | ||||||
|  |     "moduleResolution": "bundler", | ||||||
|  |     "allowImportingTsExtensions": true, | ||||||
|  |     "resolveJsonModule": true, | ||||||
|  |     "isolatedModules": true, | ||||||
|  |     "noEmit": true, | ||||||
|  |     "jsx": "react-jsx", | ||||||
|  |  | ||||||
|  |     /* Linting */ | ||||||
|  |     "strict": true, | ||||||
|  |     "noUnusedLocals": true, | ||||||
|  |     "noUnusedParameters": true, | ||||||
|  |     "noFallthroughCasesInSwitch": true | ||||||
|  |   }, | ||||||
|  |   "include": ["src"], | ||||||
|  |   "references": [{ "path": "./tsconfig.node.json" }] | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,24 +1,11 @@ | |||||||
| { | { | ||||||
|   "compilerOptions": { |   "compilerOptions": { | ||||||
|     "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", |     "composite": true, | ||||||
|     "target": "ES2022", |  | ||||||
|     "lib": ["ES2023"], |  | ||||||
|     "module": "ESNext", |  | ||||||
|     "skipLibCheck": true, |     "skipLibCheck": true, | ||||||
|  |     "module": "ESNext", | ||||||
|     /* Bundler mode */ |  | ||||||
|     "moduleResolution": "bundler", |     "moduleResolution": "bundler", | ||||||
|     "allowImportingTsExtensions": true, |     "allowSyntheticDefaultImports": true, | ||||||
|     "isolatedModules": true, |     "strict": true | ||||||
|     "moduleDetection": "force", |  | ||||||
|     "noEmit": true, |  | ||||||
|  |  | ||||||
|     /* Linting */ |  | ||||||
|     "strict": true, |  | ||||||
|     "noUnusedLocals": true, |  | ||||||
|     "noUnusedParameters": true, |  | ||||||
|     "noFallthroughCasesInSwitch": true, |  | ||||||
|     "noUncheckedSideEffectImports": true |  | ||||||
|   }, |   }, | ||||||
|   "include": ["vite.config.ts"] |   "include": ["vite.config.ts"] | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| import { defineConfig } from 'vite' | import { defineConfig } from 'vite' | ||||||
| import react from '@vitejs/plugin-react' | import react from '@vitejs/plugin-react' | ||||||
|  |  | ||||||
| // https://vite.dev/config/ | // https://vitejs.dev/config/ | ||||||
| export default defineConfig({ | export default defineConfig({ | ||||||
|   plugins: [react()], |   plugins: [react()], | ||||||
| }) | }) | ||||||
|   | |||||||
| @@ -1,3 +1,9 @@ | |||||||
| { | { | ||||||
|   "extends": ["local>renovate/presets"] |   "$schema": "https://docs.renovatebot.com/renovate-schema.json", | ||||||
|  |   "packageRules": [ | ||||||
|  |     { | ||||||
|  |       "matchUpdateTypes": ["major", "minor", "patch"], | ||||||
|  |       "automerge": true | ||||||
|  |     } | ||||||
|  |   ] | ||||||
| } | } | ||||||
		Reference in New Issue
	
	Block a user