Compare commits

..

1 Commits

Author SHA1 Message Date
af37979f32 Update dependency web-vitals to v4
Some checks failed
continuous-integration/drone/push Build is failing
2024-05-17 00:26:43 +00:00
29 changed files with 7498 additions and 7120 deletions

View File

@ -5,7 +5,7 @@ name: default
steps: steps:
- name: web_build - name: web_build
image: node:23 image: node:22
volumes: volumes:
- name: web_app - name: web_app
path: /tmp/web_build path: /tmp/web_build

File diff suppressed because it is too large Load Diff

View File

@ -8,41 +8,41 @@ edition = "2021"
[dependencies] [dependencies]
log = "0.4.21" log = "0.4.21"
env_logger = "0.11.3" env_logger = "0.11.3"
clap = { version = "4.5.20", features = ["derive", "env"] } clap = { version = "4.5.4", features = ["derive", "env"] }
light-openid = { version = "1.0.2", features = ["crypto-wrapper"] } light-openid = { version = "1.0.2", features = ["crypto-wrapper"] }
lazy_static = "1.5.0" lazy_static = "1.4.0"
actix = "0.13.3" actix = "0.13.3"
actix-web = "4.9.0" actix-web = "4.5.1"
actix-remote-ip = "0.1.0" actix-remote-ip = "0.1.0"
actix-session = { version = "0.10.0", features = ["cookie-session"] } actix-session = { version = "0.9.0", features = ["cookie-session"] }
actix-identity = "0.8.0" actix-identity = "0.7.1"
actix-cors = "0.7.0" actix-cors = "0.7.0"
actix-files = "0.6.5" actix-files = "0.6.5"
actix-web-actors = "4.3.0" actix-web-actors = "4.3.0"
actix-http = "3.9.0" actix-http = "3.6.0"
serde = { version = "1.0.214", features = ["derive"] } serde = { version = "1.0.199", features = ["derive"] }
serde_json = "1.0.132" serde_json = "1.0.116"
quick-xml = { version = "0.37.0", features = ["serialize", "overlapped-lists"] } quick-xml = { version = "0.31.0", features = ["serialize", "overlapped-lists"] }
futures-util = "0.3.31" futures-util = "0.3.30"
anyhow = "1.0.91" anyhow = "1.0.82"
actix-multipart = "0.7.0" actix-multipart = "0.6.1"
tempfile = "3.13.0" tempfile = "3.10.1"
reqwest = { version = "0.12.9", features = ["stream"] } reqwest = { version = "0.12.4", features = ["stream"] }
url = "2.5.0" url = "2.5.0"
virt = "0.4.1" virt = "0.3.1"
sysinfo = { version = "0.32.0", features = ["serde"] } sysinfo = { version = "0.30.11", features = ["serde"] }
uuid = { version = "1.11.0", features = ["v4", "serde"] } uuid = { version = "1.8.0", features = ["v4", "serde"] }
lazy-regex = "3.3.0" lazy-regex = "3.1.0"
thiserror = "2.0.0" thiserror = "1.0.59"
image = "0.25.4" image = "0.25.1"
rand = "0.8.5" rand = "0.8.5"
bytes = "1.8.0" bytes = "1.6.0"
tokio = "1.41.0" tokio = "1.37.0"
futures = "0.3.31" futures = "0.3.30"
ipnetwork = "0.20.0" ipnetwork = "0.20.0"
num = "0.4.2" num = "0.4.2"
rust-embed = { version = "8.5.0" } rust-embed = { version = "8.3.0" }
mime_guess = "2.0.4" mime_guess = "2.0.4"
dotenvy = "0.15.7" dotenvy = "0.15.7"
nix = { version = "0.29.0", features = ["net"] } nix = { version = "0.28.0", features = ["net"] }
basic-jwt = "0.2.0" basic-jwt = "0.2.0"

View File

@ -31,7 +31,7 @@ impl LibVirtActor {
"Will connect to hypvervisor at address '{}'", "Will connect to hypvervisor at address '{}'",
hypervisor_uri hypervisor_uri
); );
let conn = Connect::open(Some(hypervisor_uri))?; let conn = Connect::open(hypervisor_uri)?;
Ok(Self { m: conn }) Ok(Self { m: conn })
} }

View File

@ -1,16 +0,0 @@
use crate::controllers::{HttpResult, LibVirtReq};
use actix_web::HttpResponse;
/// Get the list of groups
pub async fn list(client: LibVirtReq) -> HttpResult {
let groups = match client.get_full_groups_list().await {
Err(e) => {
log::error!("Failed to get the list of groups! {e}");
return Ok(HttpResponse::InternalServerError()
.json(format!("Failed to get the list of groups! {e}")));
}
Ok(l) => l,
};
Ok(HttpResponse::Ok().json(groups))
}

View File

@ -8,7 +8,6 @@ use std::io::ErrorKind;
pub mod api_tokens_controller; pub mod api_tokens_controller;
pub mod auth_controller; pub mod auth_controller;
pub mod groups_controller;
pub mod iso_controller; pub mod iso_controller;
pub mod network_controller; pub mod network_controller;
pub mod nwfilter_controller; pub mod nwfilter_controller;

View File

@ -40,7 +40,6 @@ struct ServerConstraints {
vnc_token_duration: u64, vnc_token_duration: u64,
vm_name_size: LenConstraints, vm_name_size: LenConstraints,
vm_title_size: LenConstraints, vm_title_size: LenConstraints,
group_id_size: LenConstraints,
memory_size: LenConstraints, memory_size: LenConstraints,
disk_name_size: LenConstraints, disk_name_size: LenConstraints,
disk_size: LenConstraints, disk_size: LenConstraints,
@ -73,7 +72,6 @@ pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder {
vm_name_size: LenConstraints { min: 2, max: 50 }, vm_name_size: LenConstraints { min: 2, max: 50 },
vm_title_size: LenConstraints { min: 0, max: 50 }, vm_title_size: LenConstraints { min: 0, max: 50 },
group_id_size: LenConstraints { min: 3, max: 50 },
memory_size: LenConstraints { memory_size: LenConstraints {
min: constants::MIN_VM_MEMORY, min: constants::MIN_VM_MEMORY,
max: constants::MAX_VM_MEMORY, max: constants::MAX_VM_MEMORY,
@ -173,7 +171,7 @@ pub async fn network_hook_status() -> HttpResult {
pub async fn number_vcpus() -> HttpResult { pub async fn number_vcpus() -> HttpResult {
let mut system = System::new(); let mut system = System::new();
system.refresh_cpu_all(); system.refresh_cpu();
let number_cpus = system.cpus().len(); let number_cpus = system.cpus().len();
assert_ne!(number_cpus, 0, "Got invlid number of CPU!"); assert_ne!(number_cpus, 0, "Got invlid number of CPU!");

View File

@ -21,7 +21,7 @@ struct VMUuid {
/// Create a new VM /// Create a new VM
pub async fn create(client: LibVirtReq, req: web::Json<VMInfo>) -> HttpResult { pub async fn create(client: LibVirtReq, req: web::Json<VMInfo>) -> HttpResult {
let domain = match req.0.as_domain() { let domain = match req.0.as_tomain() {
Ok(d) => d, Ok(d) => d,
Err(e) => { Err(e) => {
log::error!("Failed to extract domain info! {e}"); log::error!("Failed to extract domain info! {e}");
@ -83,8 +83,6 @@ pub async fn get_single(client: LibVirtReq, id: web::Path<SingleVMUUidReq>) -> H
} }
}; };
log::debug!("INFO={info:#?}");
let state = client.get_domain_state(id.uid).await?; let state = client.get_domain_state(id.uid).await?;
Ok(HttpResponse::Ok().json(VMInfoAndState { Ok(HttpResponse::Ok().json(VMInfoAndState {
@ -114,7 +112,7 @@ pub async fn update(
id: web::Path<SingleVMUUidReq>, id: web::Path<SingleVMUUidReq>,
req: web::Json<VMInfo>, req: web::Json<VMInfo>,
) -> HttpResult { ) -> HttpResult {
let mut domain = match req.0.as_domain() { let mut domain = match req.0.as_tomain() {
Ok(d) => d, Ok(d) => d,
Err(e) => { Err(e) => {
log::error!("Failed to extract domain info! {e}"); log::error!("Failed to extract domain info! {e}");

View File

@ -7,9 +7,8 @@ use crate::libvirt_lib_structures::XMLUuid;
use crate::libvirt_rest_structures::hypervisor::HypervisorInfo; use crate::libvirt_rest_structures::hypervisor::HypervisorInfo;
use crate::libvirt_rest_structures::net::NetworkInfo; use crate::libvirt_rest_structures::net::NetworkInfo;
use crate::libvirt_rest_structures::nw_filter::NetworkFilter; use crate::libvirt_rest_structures::nw_filter::NetworkFilter;
use crate::libvirt_rest_structures::vm::{VMGroupId, VMInfo}; use crate::libvirt_rest_structures::vm::VMInfo;
use actix::Addr; use actix::Addr;
use std::collections::HashSet;
#[derive(Clone)] #[derive(Clone)]
pub struct LibVirtClient(pub Addr<LibVirtActor>); pub struct LibVirtClient(pub Addr<LibVirtActor>);
@ -108,20 +107,6 @@ impl LibVirtClient {
.await? .await?
} }
/// Get the full list of groups
pub async fn get_full_groups_list(&self) -> anyhow::Result<Vec<VMGroupId>> {
let domains = self.get_full_domains_list().await?;
let mut out = HashSet::new();
for d in domains {
if let Some(g) = VMInfo::from_domain(d)?.group {
out.insert(g);
}
}
let mut out: Vec<_> = out.into_iter().collect();
out.sort();
Ok(out)
}
/// Update a network configuration /// Update a network configuration
pub async fn update_network( pub async fn update_network(
&self, &self,

View File

@ -1,25 +1,7 @@
use crate::libvirt_lib_structures::XMLUuid; use crate::libvirt_lib_structures::XMLUuid;
/// VirtWeb specific metadata
#[derive(serde::Serialize, serde::Deserialize, Default, Debug, Clone)]
#[serde(rename = "virtweb", default)]
pub struct DomainMetadataVirtWebXML {
#[serde(rename = "@xmlns:virtweb", default)]
pub ns: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub group: Option<String>,
}
/// Domain metadata
#[derive(serde::Serialize, serde::Deserialize, Default, Debug, Clone)]
#[serde(rename = "metadata")]
pub struct DomainMetadataXML {
#[serde(rename = "virtweb:metadata", default)]
pub virtweb: DomainMetadataVirtWebXML,
}
/// OS information /// OS information
#[derive(Debug, serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "os")] #[serde(rename = "os")]
pub struct OSXML { pub struct OSXML {
#[serde(rename = "@firmware", default)] #[serde(rename = "@firmware", default)]
@ -29,7 +11,7 @@ pub struct OSXML {
} }
/// OS Type information /// OS Type information
#[derive(Debug, serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "os")] #[serde(rename = "os")]
pub struct OSTypeXML { pub struct OSTypeXML {
#[serde(rename = "@arch")] #[serde(rename = "@arch")]
@ -41,7 +23,7 @@ pub struct OSTypeXML {
} }
/// OS Loader information /// OS Loader information
#[derive(Debug, serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "loader")] #[serde(rename = "loader")]
pub struct OSLoaderXML { pub struct OSLoaderXML {
#[serde(rename = "@secure")] #[serde(rename = "@secure")]
@ -49,39 +31,39 @@ pub struct OSLoaderXML {
} }
/// Hypervisor features /// Hypervisor features
#[derive(serde::Serialize, serde::Deserialize, Default, Debug)] #[derive(serde::Serialize, serde::Deserialize, Default)]
#[serde(rename = "features")] #[serde(rename = "features")]
pub struct FeaturesXML { pub struct FeaturesXML {
pub acpi: ACPIXML, pub acpi: ACPIXML,
} }
/// ACPI feature /// ACPI feature
#[derive(serde::Serialize, serde::Deserialize, Default, Debug)] #[derive(serde::Serialize, serde::Deserialize, Default)]
#[serde(rename = "acpi")] #[serde(rename = "acpi")]
pub struct ACPIXML {} pub struct ACPIXML {}
#[derive(Debug, serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "mac")] #[serde(rename = "mac")]
pub struct NetMacAddress { pub struct NetMacAddress {
#[serde(rename = "@address")] #[serde(rename = "@address")]
pub address: String, pub address: String,
} }
#[derive(Debug, serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "source")] #[serde(rename = "source")]
pub struct NetIntSourceXML { pub struct NetIntSourceXML {
#[serde(rename = "@network")] #[serde(rename = "@network")]
pub network: String, pub network: String,
} }
#[derive(Debug, serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "model")] #[serde(rename = "model")]
pub struct NetIntModelXML { pub struct NetIntModelXML {
#[serde(rename = "@type")] #[serde(rename = "@type")]
pub r#type: String, pub r#type: String,
} }
#[derive(Debug, serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "filterref")] #[serde(rename = "filterref")]
pub struct NetIntFilterParameterXML { pub struct NetIntFilterParameterXML {
#[serde(rename = "@name")] #[serde(rename = "@name")]
@ -90,7 +72,7 @@ pub struct NetIntFilterParameterXML {
pub value: String, pub value: String,
} }
#[derive(Debug, serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "filterref")] #[serde(rename = "filterref")]
pub struct NetIntfilterRefXML { pub struct NetIntfilterRefXML {
#[serde(rename = "@filter")] #[serde(rename = "@filter")]
@ -99,7 +81,7 @@ pub struct NetIntfilterRefXML {
pub parameters: Vec<NetIntFilterParameterXML>, pub parameters: Vec<NetIntFilterParameterXML>,
} }
#[derive(Debug, serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "interface")] #[serde(rename = "interface")]
pub struct DomainNetInterfaceXML { pub struct DomainNetInterfaceXML {
#[serde(rename = "@type")] #[serde(rename = "@type")]
@ -113,14 +95,14 @@ pub struct DomainNetInterfaceXML {
pub filterref: Option<NetIntfilterRefXML>, pub filterref: Option<NetIntfilterRefXML>,
} }
#[derive(Debug, serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "input")] #[serde(rename = "input")]
pub struct DomainInputXML { pub struct DomainInputXML {
#[serde(rename = "@type")] #[serde(rename = "@type")]
pub r#type: String, pub r#type: String,
} }
#[derive(Debug, serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "backend")] #[serde(rename = "backend")]
pub struct TPMBackendXML { pub struct TPMBackendXML {
#[serde(rename = "@type")] #[serde(rename = "@type")]
@ -130,7 +112,7 @@ pub struct TPMBackendXML {
pub r#version: String, pub r#version: String,
} }
#[derive(Debug, serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "tpm")] #[serde(rename = "tpm")]
pub struct TPMDeviceXML { pub struct TPMDeviceXML {
#[serde(rename = "@model")] #[serde(rename = "@model")]
@ -139,7 +121,7 @@ pub struct TPMDeviceXML {
} }
/// Devices information /// Devices information
#[derive(Debug, serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "devices")] #[serde(rename = "devices")]
pub struct DevicesXML { pub struct DevicesXML {
/// Graphics (used for VNC) /// Graphics (used for VNC)
@ -168,7 +150,7 @@ pub struct DevicesXML {
} }
/// Graphics information /// Graphics information
#[derive(Debug, serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "graphics")] #[serde(rename = "graphics")]
pub struct GraphicsXML { pub struct GraphicsXML {
#[serde(rename = "@type")] #[serde(rename = "@type")]
@ -178,14 +160,14 @@ pub struct GraphicsXML {
} }
/// Video device information /// Video device information
#[derive(Debug, serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "video")] #[serde(rename = "video")]
pub struct VideoXML { pub struct VideoXML {
pub model: VideoModelXML, pub model: VideoModelXML,
} }
/// Video model device information /// Video model device information
#[derive(Debug, serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "model")] #[serde(rename = "model")]
pub struct VideoModelXML { pub struct VideoModelXML {
#[serde(rename = "@type")] #[serde(rename = "@type")]
@ -193,7 +175,7 @@ pub struct VideoModelXML {
} }
/// Disk information /// Disk information
#[derive(Debug, serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "disk")] #[serde(rename = "disk")]
pub struct DiskXML { pub struct DiskXML {
#[serde(rename = "@type")] #[serde(rename = "@type")]
@ -211,7 +193,7 @@ pub struct DiskXML {
pub address: Option<DiskAddressXML>, pub address: Option<DiskAddressXML>,
} }
#[derive(Debug, serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "driver")] #[serde(rename = "driver")]
pub struct DiskDriverXML { pub struct DiskDriverXML {
#[serde(rename = "@name")] #[serde(rename = "@name")]
@ -222,14 +204,14 @@ pub struct DiskDriverXML {
pub r#cache: String, pub r#cache: String,
} }
#[derive(Debug, serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "source")] #[serde(rename = "source")]
pub struct DiskSourceXML { pub struct DiskSourceXML {
#[serde(rename = "@file")] #[serde(rename = "@file")]
pub file: String, pub file: String,
} }
#[derive(Debug, serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "target")] #[serde(rename = "target")]
pub struct DiskTargetXML { pub struct DiskTargetXML {
#[serde(rename = "@dev")] #[serde(rename = "@dev")]
@ -238,18 +220,18 @@ pub struct DiskTargetXML {
pub bus: String, pub bus: String,
} }
#[derive(Debug, serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "readonly")] #[serde(rename = "readonly")]
pub struct DiskReadOnlyXML {} pub struct DiskReadOnlyXML {}
#[derive(Debug, serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "boot")] #[serde(rename = "boot")]
pub struct DiskBootXML { pub struct DiskBootXML {
#[serde(rename = "@order")] #[serde(rename = "@order")]
pub order: String, pub order: String,
} }
#[derive(Debug, serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "address")] #[serde(rename = "address")]
pub struct DiskAddressXML { pub struct DiskAddressXML {
#[serde(rename = "@type")] #[serde(rename = "@type")]
@ -269,7 +251,7 @@ pub struct DiskAddressXML {
} }
/// Domain RAM information /// Domain RAM information
#[derive(Debug, serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "memory")] #[serde(rename = "memory")]
pub struct DomainMemoryXML { pub struct DomainMemoryXML {
#[serde(rename = "@unit")] #[serde(rename = "@unit")]
@ -279,7 +261,7 @@ pub struct DomainMemoryXML {
pub memory: usize, pub memory: usize,
} }
#[derive(Debug, serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "topology")] #[serde(rename = "topology")]
pub struct DomainCPUTopology { pub struct DomainCPUTopology {
#[serde(rename = "@sockets")] #[serde(rename = "@sockets")]
@ -290,14 +272,14 @@ pub struct DomainCPUTopology {
pub threads: usize, pub threads: usize,
} }
#[derive(Debug, serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "cpu")] #[serde(rename = "cpu")]
pub struct DomainVCPUXML { pub struct DomainVCPUXML {
#[serde(rename = "$value")] #[serde(rename = "$value")]
pub body: usize, pub body: usize,
} }
#[derive(Debug, serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "cpu")] #[serde(rename = "cpu")]
pub struct DomainCPUXML { pub struct DomainCPUXML {
#[serde(rename = "@mode")] #[serde(rename = "@mode")]
@ -306,7 +288,7 @@ pub struct DomainCPUXML {
} }
/// Domain information, see https://libvirt.org/formatdomain.html /// Domain information, see https://libvirt.org/formatdomain.html
#[derive(Debug, serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "domain")] #[serde(rename = "domain")]
pub struct DomainXML { pub struct DomainXML {
/// Domain type (kvm) /// Domain type (kvm)
@ -318,9 +300,6 @@ pub struct DomainXML {
pub genid: Option<uuid::Uuid>, pub genid: Option<uuid::Uuid>,
pub title: Option<String>, pub title: Option<String>,
pub description: Option<String>, pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<DomainMetadataXML>,
pub os: OSXML, pub os: OSXML,
#[serde(default)] #[serde(default)]
pub features: FeaturesXML, pub features: FeaturesXML,
@ -340,32 +319,10 @@ pub struct DomainXML {
pub on_crash: String, pub on_crash: String,
} }
const METADATA_START_MARKER: &str =
"<virtweb:metadata xmlns:virtweb=\"https://virtweb.communiquons.org\">";
const METADATA_END_MARKER: &str = "</virtweb:metadata>";
impl DomainXML { impl DomainXML {
/// Decode Domain structure from XML definition /// Decode Domain structure from XML definition
pub fn parse_xml(xml: &str) -> anyhow::Result<Self> { pub fn parse_xml(xml: &str) -> anyhow::Result<Self> {
let mut res: Self = quick_xml::de::from_str(xml)?; Ok(quick_xml::de::from_str(xml)?)
// Handle custom metadata parsing issue
//
// https://github.com/tafia/quick-xml/pull/797
if xml.contains(METADATA_START_MARKER) && xml.contains(METADATA_END_MARKER) {
let s = xml
.split_once(METADATA_START_MARKER)
.unwrap()
.1
.split_once(METADATA_END_MARKER)
.unwrap()
.0;
let s = format!("<virtweb>{s}</virtweb>");
let metadata: DomainMetadataVirtWebXML = quick_xml::de::from_str(&s)?;
res.metadata = Some(DomainMetadataXML { virtweb: metadata });
}
Ok(res)
} }
/// Turn this domain into its XML definition /// Turn this domain into its XML definition

View File

@ -10,11 +10,6 @@ use crate::utils::files_utils::convert_size_unit_to_mb;
use lazy_regex::regex; use lazy_regex::regex;
use num::Integer; use num::Integer;
#[derive(
Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, Hash, Ord, PartialOrd,
)]
pub struct VMGroupId(pub String);
#[derive(serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
pub enum BootType { pub enum BootType {
UEFI, UEFI,
@ -64,9 +59,6 @@ pub struct VMInfo {
pub genid: Option<XMLUuid>, pub genid: Option<XMLUuid>,
pub title: Option<String>, pub title: Option<String>,
pub description: Option<String>, pub description: Option<String>,
/// Group associated with the VM (VirtWeb specific field)
#[serde(skip_serializing_if = "Option::is_none")]
pub group: Option<VMGroupId>,
pub boot_type: BootType, pub boot_type: BootType,
pub architecture: VMArchitecture, pub architecture: VMArchitecture,
/// VM allocated memory, in megabytes /// VM allocated memory, in megabytes
@ -87,7 +79,7 @@ pub struct VMInfo {
impl VMInfo { impl VMInfo {
/// Turn this VM into a domain /// Turn this VM into a domain
pub fn as_domain(&self) -> anyhow::Result<DomainXML> { pub fn as_tomain(&self) -> anyhow::Result<DomainXML> {
if !regex!("^[a-zA-Z0-9]+$").is_match(&self.name) { if !regex!("^[a-zA-Z0-9]+$").is_match(&self.name) {
return Err(StructureExtraction("VM name is invalid!").into()); return Err(StructureExtraction("VM name is invalid!").into());
} }
@ -113,12 +105,6 @@ impl VMInfo {
} }
} }
if let Some(group) = &self.group {
if !regex!("^[a-zA-Z0-9]+$").is_match(&group.0) {
return Err(StructureExtraction("VM group name is invalid!").into());
}
}
if self.memory < constants::MIN_VM_MEMORY || self.memory > constants::MAX_VM_MEMORY { if self.memory < constants::MIN_VM_MEMORY || self.memory > constants::MAX_VM_MEMORY {
return Err(StructureExtraction("VM memory is invalid!").into()); return Err(StructureExtraction("VM memory is invalid!").into());
} }
@ -296,12 +282,6 @@ impl VMInfo {
title: self.title.clone(), title: self.title.clone(),
description: self.description.clone(), description: self.description.clone(),
metadata: Some(DomainMetadataXML {
virtweb: DomainMetadataVirtWebXML {
ns: "https://virtweb.communiquons.org".to_string(),
group: self.group.clone().map(|g| g.0),
},
}),
os: OSXML { os: OSXML {
r#type: OSTypeXML { r#type: OSTypeXML {
arch: match self.architecture { arch: match self.architecture {
@ -389,13 +369,6 @@ impl VMInfo {
genid: domain.genid.map(XMLUuid), genid: domain.genid.map(XMLUuid),
title: domain.title, title: domain.title,
description: domain.description, description: domain.description,
group: domain
.metadata
.clone()
.unwrap_or_default()
.virtweb
.group
.map(VMGroupId),
boot_type: match domain.os.loader { boot_type: match domain.os.loader {
None => BootType::UEFI, None => BootType::UEFI,
Some(l) => match l.secure.as_str() { Some(l) => match l.secure.as_str() {

View File

@ -22,7 +22,7 @@ use virtweb_backend::constants::{
MAX_INACTIVITY_DURATION, MAX_SESSION_DURATION, SESSION_COOKIE_NAME, MAX_INACTIVITY_DURATION, MAX_SESSION_DURATION, SESSION_COOKIE_NAME,
}; };
use virtweb_backend::controllers::{ use virtweb_backend::controllers::{
api_tokens_controller, auth_controller, groups_controller, iso_controller, network_controller, api_tokens_controller, auth_controller, iso_controller, network_controller,
nwfilter_controller, server_controller, static_controller, vm_controller, nwfilter_controller, server_controller, static_controller, vm_controller,
}; };
use virtweb_backend::libvirt_client::LibVirtClient; use virtweb_backend::libvirt_client::LibVirtClient;
@ -210,8 +210,6 @@ async fn main() -> std::io::Result<()> {
web::get().to(vm_controller::vnc_token), web::get().to(vm_controller::vnc_token),
) )
.route("/api/vnc", web::get().to(vm_controller::vnc)) .route("/api/vnc", web::get().to(vm_controller::vnc))
// Groups controller
.route("/api/group/list", web::get().to(groups_controller::list))
// Network controller // Network controller
.route( .route(
"/api/network/create", "/api/network/create",

View File

@ -9,7 +9,7 @@ make
The release file will be available in `virtweb_backend/target/release/virtweb_backend`. The release file will be available in `virtweb_backend/target/release/virtweb_backend`.
This is the only artifact that must be copied to the server. It is recommended to copy it to the `/usr/local/bin` directory. This is the only artifcat that must be copied to the server. It is recommended to copy it to the `/usr/local/bin` directory.
## Install requirements ## Install requirements
In order to work properly, VirtWeb relies on `libvirt`, `qemu` and `kvm`: In order to work properly, VirtWeb relies on `libvirt`, `qemu` and `kvm`:

File diff suppressed because it is too large Load Diff

View File

@ -6,37 +6,37 @@
"dependencies": { "dependencies": {
"@emotion/react": "^11.11.1", "@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0", "@emotion/styled": "^11.11.0",
"@fontsource/roboto": "^5.1.0", "@fontsource/roboto": "^5.0.13",
"@mdi/js": "^7.2.96", "@mdi/js": "^7.2.96",
"@mdi/react": "^1.6.1", "@mdi/react": "^1.6.1",
"@mui/icons-material": "^6.1.6", "@mui/icons-material": "^5.14.7",
"@mui/material": "^6.1.6", "@mui/material": "^5.14.7",
"@mui/x-charts": "^7.22.1", "@mui/x-charts": "^7.3.0",
"@mui/x-data-grid": "^7.22.1", "@mui/x-data-grid": "^7.3.0",
"@testing-library/jest-dom": "^6.6.3", "@testing-library/jest-dom": "^6.4.2",
"@testing-library/react": "^16.0.0", "@testing-library/react": "^15.0.4",
"@testing-library/user-event": "^14.5.2", "@testing-library/user-event": "^14.5.2",
"@types/humanize-duration": "^3.27.1", "@types/humanize-duration": "^3.27.1",
"@types/jest": "^29.5.14", "@types/jest": "^29.5.12",
"@types/react": "^18.3.12", "@types/react": "^18.2.79",
"@types/react-dom": "^18.3.1", "@types/react-dom": "^18.2.25",
"@types/react-syntax-highlighter": "^15.5.13", "@types/react-syntax-highlighter": "^15.5.11",
"@types/uuid": "^10.0.0", "@types/uuid": "^9.0.5",
"@vitejs/plugin-react": "^4.3.3", "@vitejs/plugin-react": "^4.2.1",
"date-and-time": "^3.6.0", "date-and-time": "^3.1.1",
"filesize": "^10.1.6", "filesize": "^10.0.12",
"humanize-duration": "^3.29.0", "humanize-duration": "^3.29.0",
"mui-file-input": "^6.0.0", "mui-file-input": "^4.0.4",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-router-dom": "^6.23.0", "react-router-dom": "^6.23.0",
"react-syntax-highlighter": "^15.6.1", "react-syntax-highlighter": "^15.5.0",
"react-vnc": "^2.0.2", "react-vnc": "^1.0.0",
"typescript": "^4.9.5", "typescript": "^4.0.0",
"uuid": "^11.0.2", "uuid": "^9.0.1",
"vite": "^5.4.10", "vite": "^5.2.10",
"vite-tsconfig-paths": "^5.0.1", "vite-tsconfig-paths": "^4.2.2",
"web-vitals": "^3.5.2", "web-vitals": "^4.0.0",
"xml-formatter": "^3.6.0" "xml-formatter": "^3.6.0"
}, },
"scripts": { "scripts": {

View File

@ -1,15 +0,0 @@
import { APIClient } from "./ApiClient";
export class GroupApi {
/**
* Get the entire list of networks
*/
static async GetList(): Promise<string[]> {
return (
await APIClient.exec({
method: "GET",
uri: "/group/list",
})
).data;
}
}

View File

@ -16,7 +16,6 @@ export interface ServerConstraints {
vnc_token_duration: number; vnc_token_duration: number;
vm_name_size: LenConstraint; vm_name_size: LenConstraint;
vm_title_size: LenConstraint; vm_title_size: LenConstraint;
group_id_size: LenConstraint;
memory_size: LenConstraint; memory_size: LenConstraint;
disk_name_size: LenConstraint; disk_name_size: LenConstraint;
disk_size: LenConstraint; disk_size: LenConstraint;
@ -74,7 +73,7 @@ interface SystemInfo {
secs: number; secs: number;
nanos: number; nanos: number;
}; };
global_cpu_usage: number; global_cpu_info: GlobalCPUInfo;
cpus: CpuCore[]; cpus: CpuCore[];
physical_core_count: number; physical_core_count: number;
total_memory: number; total_memory: number;
@ -95,6 +94,14 @@ interface SystemInfo {
host_name: string; host_name: string;
} }
interface GlobalCPUInfo {
cpu_usage: number;
name: string;
vendor_id: string;
brand: string;
frequency: number;
}
interface CpuCore { interface CpuCore {
cpu_usage: number; cpu_usage: number;
name: string; name: string;

View File

@ -63,7 +63,6 @@ interface VMInfoInterface {
genid?: string; genid?: string;
title?: string; title?: string;
description?: string; description?: string;
group?: string;
boot_type: "UEFI" | "UEFISecureBoot"; boot_type: "UEFI" | "UEFISecureBoot";
architecture: "i686" | "x86_64"; architecture: "i686" | "x86_64";
memory: number; memory: number;
@ -81,7 +80,6 @@ export class VMInfo implements VMInfoInterface {
genid?: string; genid?: string;
title?: string; title?: string;
description?: string; description?: string;
group?: string;
boot_type: "UEFI" | "UEFISecureBoot"; boot_type: "UEFI" | "UEFISecureBoot";
architecture: "i686" | "x86_64"; architecture: "i686" | "x86_64";
number_vcpu: number; number_vcpu: number;
@ -98,7 +96,6 @@ export class VMInfo implements VMInfoInterface {
this.genid = int.genid; this.genid = int.genid;
this.title = int.title; this.title = int.title;
this.description = int.description; this.description = int.description;
this.group = int.group;
this.boot_type = int.boot_type; this.boot_type = int.boot_type;
this.architecture = int.architecture; this.architecture = int.architecture;
this.number_vcpu = int.number_vcpu; this.number_vcpu = int.number_vcpu;

View File

@ -8,6 +8,7 @@ import {
import Icon from "@mdi/react"; import Icon from "@mdi/react";
import { import {
Box, Box,
Grid,
LinearProgress, LinearProgress,
Table, Table,
TableBody, TableBody,
@ -16,10 +17,7 @@ import {
TableRow, TableRow,
Typography, Typography,
} from "@mui/material"; } from "@mui/material";
import Grid from "@mui/material/Grid2";
import { PieChart } from "@mui/x-charts"; import { PieChart } from "@mui/x-charts";
import { filesize } from "filesize";
import humanizeDuration from "humanize-duration";
import React from "react"; import React from "react";
import { import {
DiskInfo, DiskInfo,
@ -30,6 +28,8 @@ import {
import { AsyncWidget } from "../widgets/AsyncWidget"; import { AsyncWidget } from "../widgets/AsyncWidget";
import { VirtWebPaper } from "../widgets/VirtWebPaper"; import { VirtWebPaper } from "../widgets/VirtWebPaper";
import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer";
import humanizeDuration from "humanize-duration";
import { filesize } from "filesize";
export function SysInfoRoute(): React.ReactElement { export function SysInfoRoute(): React.ReactElement {
const [info, setInfo] = React.useState<ServerSystemInfo>(); const [info, setInfo] = React.useState<ServerSystemInfo>();
@ -65,7 +65,7 @@ export function SysInfoRouteInner(p: {
<VirtWebRouteContainer label="Sysinfo"> <VirtWebRouteContainer label="Sysinfo">
<Grid container spacing={2}> <Grid container spacing={2}>
{/* Memory */} {/* Memory */}
<Grid size={{ xs: 4 }}> <Grid xs={4}>
<Box flexGrow={1}> <Box flexGrow={1}>
<Typography style={{ textAlign: "center" }}>Memory</Typography> <Typography style={{ textAlign: "center" }}>Memory</Typography>
<PieChart <PieChart
@ -97,7 +97,7 @@ export function SysInfoRouteInner(p: {
</Grid> </Grid>
{/* Disk usage */} {/* Disk usage */}
<Grid size={{ xs: 4 }}> <Grid xs={4}>
<Box flexGrow={1}> <Box flexGrow={1}>
<Typography style={{ textAlign: "center" }}>Disk usage</Typography> <Typography style={{ textAlign: "center" }}>Disk usage</Typography>
<PieChart <PieChart
@ -125,7 +125,7 @@ export function SysInfoRouteInner(p: {
</Grid> </Grid>
{/* CPU usage */} {/* CPU usage */}
<Grid size={{ xs: 4 }}> <Grid xs={4}>
<Box flexGrow={1}> <Box flexGrow={1}>
<Typography style={{ textAlign: "center" }}>CPU usage</Typography> <Typography style={{ textAlign: "center" }}>CPU usage</Typography>
<PieChart <PieChart
@ -134,13 +134,13 @@ export function SysInfoRouteInner(p: {
data: [ data: [
{ {
id: 1, id: 1,
value: 100 - p.info.system.global_cpu_usage, value: 100 - p.info.system.global_cpu_info.cpu_usage,
label: "Free", label: "Free",
}, },
{ {
id: 2, id: 2,
value: p.info.system.global_cpu_usage, value: p.info.system.global_cpu_info.cpu_usage,
label: "Used", label: "Used",
}, },
], ],
@ -180,18 +180,18 @@ export function SysInfoRouteInner(p: {
label="CPU info" label="CPU info"
icon={<Icon size={"1rem"} path={mdiMemory} />} icon={<Icon size={"1rem"} path={mdiMemory} />}
entries={[ entries={[
{ label: "Brand", value: p.info.system.cpus[0].brand }, { label: "Brand", value: p.info.system.global_cpu_info.brand },
{ {
label: "Vendor ID", label: "Vendor ID",
value: p.info.system.cpus[0].vendor_id, value: p.info.system.global_cpu_info.vendor_id,
}, },
{ {
label: "CPU usage", label: "CPU usage",
value: p.info.system.cpus[0].cpu_usage, value: p.info.system.global_cpu_info.cpu_usage,
}, },
{ {
label: "Name", label: "Name",
value: p.info.system.cpus[0].name, value: p.info.system.global_cpu_info.name,
}, },
{ {
label: "CPU model", label: "CPU model",

View File

@ -1,5 +1,3 @@
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
import VisibilityIcon from "@mui/icons-material/Visibility"; import VisibilityIcon from "@mui/icons-material/Visibility";
import { import {
Button, Button,
@ -9,7 +7,6 @@ import {
TableBody, TableBody,
TableCell, TableCell,
TableContainer, TableContainer,
TableFooter,
TableHead, TableHead,
TableRow, TableRow,
Tooltip, Tooltip,
@ -17,27 +14,19 @@ import {
import { filesize } from "filesize"; import { filesize } from "filesize";
import React from "react"; import React from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { GroupApi } from "../api/GroupApi"; import { VMApi, VMInfo } from "../api/VMApi";
import { VMApi, VMInfo, VMState } from "../api/VMApi";
import { AsyncWidget } from "../widgets/AsyncWidget"; import { AsyncWidget } from "../widgets/AsyncWidget";
import { RouterLink } from "../widgets/RouterLink"; import { RouterLink } from "../widgets/RouterLink";
import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer";
import { VMStatusWidget } from "../widgets/vms/VMStatusWidget"; import { VMStatusWidget } from "../widgets/vms/VMStatusWidget";
export function VMListRoute(): React.ReactElement { export function VMListRoute(): React.ReactElement {
const [groups, setGroups] = React.useState<Array<string | undefined>>();
const [list, setList] = React.useState<VMInfo[] | undefined>(); const [list, setList] = React.useState<VMInfo[] | undefined>();
const loadKey = React.useRef(1); const loadKey = React.useRef(1);
const load = async () => { const load = async () => {
const groups: Array<string | undefined> = await GroupApi.GetList(); setList(await VMApi.GetList());
const list = await VMApi.GetList();
if (list.find((v) => !v.group) !== undefined) groups.push(undefined);
setGroups(groups);
setList(list);
}; };
const reload = () => { const reload = () => {
@ -62,7 +51,7 @@ export function VMListRoute(): React.ReactElement {
</> </>
} }
> >
<VMListWidget list={list!} groups={groups!} onReload={reload} /> <VMListWidget list={list!} onReload={reload} />
</VirtWebRouteContainer> </VirtWebRouteContainer>
)} )}
/> />
@ -70,37 +59,11 @@ export function VMListRoute(): React.ReactElement {
} }
function VMListWidget(p: { function VMListWidget(p: {
groups: Array<string | undefined>;
list: VMInfo[]; list: VMInfo[];
onReload: () => void; onReload: () => void;
}): React.ReactElement { }): React.ReactElement {
const navigate = useNavigate(); const navigate = useNavigate();
const [hiddenGroups, setHiddenGroups] = React.useState<
Set<string | undefined>
>(new Set());
const [runningVMs, setRunningVMs] = React.useState<Set<string>>(new Set());
const toggleHiddenGroup = (g: string | undefined) => {
if (hiddenGroups.has(g)) hiddenGroups.delete(g);
else hiddenGroups.add(g);
setHiddenGroups(new Set([...hiddenGroups]));
};
const updateVMState = (v: VMInfo, s: VMState) => {
const running = s !== "Shutoff";
if (runningVMs.has(v.name) === running) {
return;
}
if (running) runningVMs.add(v.name);
else runningVMs.delete(v.name);
setRunningVMs(new Set([...runningVMs]));
};
return ( return (
<TableContainer component={Paper}> <TableContainer component={Paper}>
<Table> <Table>
@ -109,39 +72,12 @@ function VMListWidget(p: {
<TableCell>Name</TableCell> <TableCell>Name</TableCell>
<TableCell>Description</TableCell> <TableCell>Description</TableCell>
<TableCell>Memory</TableCell> <TableCell>Memory</TableCell>
<TableCell>vCPU</TableCell>
<TableCell>Status</TableCell> <TableCell>Status</TableCell>
<TableCell>Actions</TableCell> <TableCell>Actions</TableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{p.groups.map((g, num) => ( {p.list.map((row) => (
<React.Fragment key={num}>
{p.groups.length > 1 && (
<TableRow>
<TableCell
style={{ paddingBottom: 2, paddingTop: 2 }}
colSpan={6}
>
<IconButton
size="small"
onClick={() => toggleHiddenGroup(g)}
>
{!hiddenGroups?.has(g) ? (
<KeyboardArrowUpIcon />
) : (
<KeyboardArrowDownIcon />
)}
</IconButton>
{g ?? "default"}
</TableCell>
</TableRow>
)}
{!hiddenGroups.has(g) &&
p.list
.filter((row) => row.group === g)
.map((row) => (
<TableRow <TableRow
hover hover
key={row.name} key={row.name}
@ -152,13 +88,9 @@ function VMListWidget(p: {
{row.name} {row.name}
</TableCell> </TableCell>
<TableCell>{row.description ?? ""}</TableCell> <TableCell>{row.description ?? ""}</TableCell>
<TableCell>{vmMemoryToHuman(row.memory)}</TableCell> <TableCell>{filesize(row.memory * 1000 * 1000)}</TableCell>
<TableCell>{row.number_vcpu}</TableCell>
<TableCell> <TableCell>
<VMStatusWidget <VMStatusWidget vm={row} />
vm={row}
onChange={(s) => updateVMState(row, s)}
/>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Tooltip title="View this VM"> <Tooltip title="View this VM">
@ -171,38 +103,8 @@ function VMListWidget(p: {
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}
</React.Fragment>
))}
</TableBody> </TableBody>
<TableFooter>
<TableRow>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell>
{vmMemoryToHuman(
p.list
.filter((v) => runningVMs.has(v.name))
.reduce((s, v) => s + v.memory, 0)
)}
{" / "}
{vmMemoryToHuman(p.list.reduce((s, v) => s + v.memory, 0))}
</TableCell>
<TableCell>
{p.list
.filter((v) => runningVMs.has(v.name))
.reduce((s, v) => s + v.number_vcpu, 0)}
{" / "}
{p.list.reduce((s, v) => s + v.number_vcpu, 0)}
</TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
</TableRow>
</TableFooter>
</Table> </Table>
</TableContainer> </TableContainer>
); );
} }
function vmMemoryToHuman(size: number): string {
return filesize(size * 1000 * 1000);
}

View File

@ -3,7 +3,7 @@ import Icon from "@mdi/react";
import Avatar from "@mui/material/Avatar"; import Avatar from "@mui/material/Avatar";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import CssBaseline from "@mui/material/CssBaseline"; import CssBaseline from "@mui/material/CssBaseline";
import Grid from "@mui/material/Grid2"; import Grid from "@mui/material/Grid";
import Paper from "@mui/material/Paper"; import Paper from "@mui/material/Paper";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import { Link, Outlet } from "react-router-dom"; import { Link, Outlet } from "react-router-dom";
@ -38,7 +38,10 @@ export function BaseLoginPage() {
<Grid container component="main" sx={{ height: "100vh" }}> <Grid container component="main" sx={{ height: "100vh" }}>
<CssBaseline /> <CssBaseline />
<Grid <Grid
size={{ xs: false, sm: 4, md: 7 }} item
xs={false}
sm={4}
md={7}
sx={{ sx={{
backgroundImage: "url(/login_splash.jpg)", backgroundImage: "url(/login_splash.jpg)",
backgroundRepeat: "no-repeat", backgroundRepeat: "no-repeat",
@ -50,12 +53,7 @@ export function BaseLoginPage() {
backgroundPosition: "center", backgroundPosition: "center",
}} }}
/> />
<Grid <Grid item xs={12} sm={8} md={5} component={Paper} elevation={6} square>
size={{ xs: 12, sm: 8, md: 5 }}
component={Paper}
elevation={6}
square
>
<Box <Box
sx={{ sx={{
my: 8, my: 8,

View File

@ -1,6 +1,5 @@
import { Paper, Typography } from "@mui/material"; import { Grid, Paper, Typography } from "@mui/material";
import React, { PropsWithChildren } from "react"; import React, { PropsWithChildren } from "react";
import Grid from "@mui/material/Grid2";
export function EditSection( export function EditSection(
p: { p: {
@ -10,7 +9,7 @@ export function EditSection(
} & PropsWithChildren } & PropsWithChildren
): React.ReactElement { ): React.ReactElement {
return ( return (
<Grid size={{ sm: 12, md: p.fullWidth ? 12 : 6 }}> <Grid item sm={12} md={p.fullWidth ? 12 : 6}>
<Paper style={{ margin: "10px", padding: "10px" }}> <Paper style={{ margin: "10px", padding: "10px" }}>
{(p.title || p.actions) && ( {(p.title || p.actions) && (
<span <span

View File

@ -4,6 +4,7 @@ import DeleteIcon from "@mui/icons-material/Delete";
import { import {
Avatar, Avatar,
Button, Button,
Grid,
IconButton, IconButton,
ListItem, ListItem,
ListItemAvatar, ListItemAvatar,
@ -18,7 +19,6 @@ import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider";
import { IPInput } from "./IPInput"; import { IPInput } from "./IPInput";
import { MACInput } from "./MACInput"; import { MACInput } from "./MACInput";
import { TextInput } from "./TextInput"; import { TextInput } from "./TextInput";
import Grid from "@mui/material/Grid2";
export function NetDHCPHostReservations(p: { export function NetDHCPHostReservations(p: {
editable: boolean; editable: boolean;
@ -39,7 +39,7 @@ export function NetDHCPHostReservations(p: {
<> <>
<Grid container> <Grid container>
{p.dhcp.hosts.map((h, num) => ( {p.dhcp.hosts.map((h, num) => (
<Grid key={num} size={{ sm: 12, md: 6 }} style={{ padding: "10px" }}> <Grid key={num} sm={12} md={6} item style={{ padding: "10px" }}>
<HostReservationWidget <HostReservationWidget
key={num} key={num}
{...p} {...p}

View File

@ -5,11 +5,11 @@ import {
Card, Card,
CardActions, CardActions,
CardContent, CardContent,
Grid,
IconButton, IconButton,
Tooltip, Tooltip,
Typography, Typography,
} from "@mui/material"; } from "@mui/material";
import Grid from "@mui/material/Grid2";
import React, { PropsWithChildren } from "react"; import React, { PropsWithChildren } from "react";
import { NatEntry } from "../../api/NetworksApi"; import { NatEntry } from "../../api/NetworksApi";
import { ServerApi } from "../../api/ServerApi"; import { ServerApi } from "../../api/ServerApi";
@ -295,7 +295,7 @@ function NATEntryProp(
p: PropsWithChildren<{ label?: string }> p: PropsWithChildren<{ label?: string }>
): React.ReactElement { ): React.ReactElement {
return ( return (
<Grid size={{ sm: 12, md: 6 }} style={{ padding: "20px" }}> <Grid item sm={12} md={6} style={{ padding: "20px" }}>
{p.label && ( {p.label && (
<Typography variant="h6" style={{ marginBottom: "10px" }}> <Typography variant="h6" style={{ marginBottom: "10px" }}>
{p.label} {p.label}

View File

@ -4,13 +4,13 @@ import DeleteIcon from "@mui/icons-material/Delete";
import { import {
Avatar, Avatar,
Button, Button,
Grid,
IconButton, IconButton,
ListItem, ListItem,
ListItemAvatar, ListItemAvatar,
ListItemText, ListItemText,
Tooltip, Tooltip,
} from "@mui/material"; } from "@mui/material";
import Grid from "@mui/material/Grid2";
import { NWFilter } from "../../api/NWFilterApi"; import { NWFilter } from "../../api/NWFilterApi";
import { NetworkInfo } from "../../api/NetworksApi"; import { NetworkInfo } from "../../api/NetworksApi";
import { ServerApi } from "../../api/ServerApi"; import { ServerApi } from "../../api/ServerApi";

View File

@ -1,5 +1,4 @@
import { Button, Checkbox } from "@mui/material"; import { Button, Checkbox, Grid } from "@mui/material";
import Grid from "@mui/material/Grid2";
import React from "react"; import React from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { IpConfig, NetworkApi, NetworkInfo } from "../../api/NetworksApi"; import { IpConfig, NetworkApi, NetworkInfo } from "../../api/NetworksApi";

View File

@ -1,5 +1,4 @@
import { Button } from "@mui/material"; import { Button, Grid } from "@mui/material";
import Grid from "@mui/material/Grid2";
import React, { ReactElement } from "react"; import React, { ReactElement } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { import {
@ -7,7 +6,6 @@ import {
NWFilterApi, NWFilterApi,
NWFilterIsBuiltin, NWFilterIsBuiltin,
} from "../../api/NWFilterApi"; } from "../../api/NWFilterApi";
import { ServerApi } from "../../api/ServerApi";
import { useAlert } from "../../hooks/providers/AlertDialogProvider"; import { useAlert } from "../../hooks/providers/AlertDialogProvider";
import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider"; import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider";
import { useSnackbar } from "../../hooks/providers/SnackbarProvider"; import { useSnackbar } from "../../hooks/providers/SnackbarProvider";
@ -15,11 +13,12 @@ import { AsyncWidget } from "../AsyncWidget";
import { TabsWidget } from "../TabsWidget"; import { TabsWidget } from "../TabsWidget";
import { XMLAsyncWidget } from "../XMLWidget"; import { XMLAsyncWidget } from "../XMLWidget";
import { EditSection } from "../forms/EditSection"; import { EditSection } from "../forms/EditSection";
import { NWFSelectReferencedFilters } from "../forms/NWFSelectReferencedFilters";
import { NWFilterPriorityInput } from "../forms/NWFilterPriorityInput";
import { NWFilterRules } from "../forms/NWFilterRules";
import { SelectInput } from "../forms/SelectInput";
import { TextInput } from "../forms/TextInput"; import { TextInput } from "../forms/TextInput";
import { ServerApi } from "../../api/ServerApi";
import { SelectInput } from "../forms/SelectInput";
import { NWFSelectReferencedFilters } from "../forms/NWFSelectReferencedFilters";
import { NWFilterRules } from "../forms/NWFilterRules";
import { NWFilterPriorityInput } from "../forms/NWFilterPriorityInput";
interface DetailsProps { interface DetailsProps {
nwfilter: NWFilter; nwfilter: NWFilter;

View File

@ -1,5 +1,4 @@
import { Button } from "@mui/material"; import { Button, Grid } from "@mui/material";
import Grid from "@mui/material/Grid2";
import React from "react"; import React from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { NWFilter, NWFilterApi } from "../../api/NWFilterApi"; import { NWFilter, NWFilterApi } from "../../api/NWFilterApi";

View File

@ -1,11 +1,7 @@
import AddIcon from "@mui/icons-material/Add"; import { Button, Grid } from "@mui/material";
import ListIcon from "@mui/icons-material/List";
import { Button, IconButton, Tooltip } from "@mui/material";
import Grid from "@mui/material/Grid2";
import React from "react"; import React from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { validate as validateUUID } from "uuid"; import { validate as validateUUID } from "uuid";
import { GroupApi } from "../../api/GroupApi";
import { IsoFile, IsoFilesApi } from "../../api/IsoFilesApi"; import { IsoFile, IsoFilesApi } from "../../api/IsoFilesApi";
import { NWFilter, NWFilterApi } from "../../api/NWFilterApi"; import { NWFilter, NWFilterApi } from "../../api/NWFilterApi";
import { NetworkApi, NetworkInfo } from "../../api/NetworksApi"; import { NetworkApi, NetworkInfo } from "../../api/NetworksApi";
@ -16,7 +12,6 @@ import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider";
import { useSnackbar } from "../../hooks/providers/SnackbarProvider"; import { useSnackbar } from "../../hooks/providers/SnackbarProvider";
import { AsyncWidget } from "../AsyncWidget"; import { AsyncWidget } from "../AsyncWidget";
import { TabsWidget } from "../TabsWidget"; import { TabsWidget } from "../TabsWidget";
import { XMLAsyncWidget } from "../XMLWidget";
import { CheckboxInput } from "../forms/CheckboxInput"; import { CheckboxInput } from "../forms/CheckboxInput";
import { EditSection } from "../forms/EditSection"; import { EditSection } from "../forms/EditSection";
import { ResAutostartInput } from "../forms/ResAutostartInput"; import { ResAutostartInput } from "../forms/ResAutostartInput";
@ -26,6 +21,7 @@ import { VMDisksList } from "../forms/VMDisksList";
import { VMNetworksList } from "../forms/VMNetworksList"; import { VMNetworksList } from "../forms/VMNetworksList";
import { VMSelectIsoInput } from "../forms/VMSelectIsoInput"; import { VMSelectIsoInput } from "../forms/VMSelectIsoInput";
import { VMScreenshot } from "./VMScreenshot"; import { VMScreenshot } from "./VMScreenshot";
import { XMLAsyncWidget } from "../XMLWidget";
interface DetailsProps { interface DetailsProps {
vm: VMInfo; vm: VMInfo;
@ -35,7 +31,6 @@ interface DetailsProps {
} }
export function VMDetails(p: DetailsProps): React.ReactElement { export function VMDetails(p: DetailsProps): React.ReactElement {
const [groupsList, setGroupsList] = React.useState<string[] | any>();
const [isoList, setIsoList] = React.useState<IsoFile[] | any>(); const [isoList, setIsoList] = React.useState<IsoFile[] | any>();
const [vcpuCombinations, setVCPUCombinations] = React.useState< const [vcpuCombinations, setVCPUCombinations] = React.useState<
number[] | any number[] | any
@ -46,7 +41,6 @@ export function VMDetails(p: DetailsProps): React.ReactElement {
>(); >();
const load = async () => { const load = async () => {
setGroupsList(await GroupApi.GetList());
setIsoList(await IsoFilesApi.GetList()); setIsoList(await IsoFilesApi.GetList());
setVCPUCombinations(await ServerApi.NumberVCPUs()); setVCPUCombinations(await ServerApi.NumberVCPUs());
setNetworksList(await NetworkApi.GetList()); setNetworksList(await NetworkApi.GetList());
@ -60,7 +54,6 @@ export function VMDetails(p: DetailsProps): React.ReactElement {
errMsg="Failed to load the list of ISO files" errMsg="Failed to load the list of ISO files"
build={() => ( build={() => (
<VMDetailsInner <VMDetailsInner
groupsList={groupsList}
isoList={isoList} isoList={isoList}
vcpuCombinations={vcpuCombinations} vcpuCombinations={vcpuCombinations}
networksList={networksList} networksList={networksList}
@ -81,7 +74,6 @@ enum VMTab {
} }
type DetailsInnerProps = DetailsProps & { type DetailsInnerProps = DetailsProps & {
groupsList: string[];
isoList: IsoFile[]; isoList: IsoFile[];
vcpuCombinations: number[]; vcpuCombinations: number[];
networksList: NetworkInfo[]; networksList: NetworkInfo[];
@ -124,8 +116,6 @@ function VMDetailsInner(p: DetailsInnerProps): React.ReactElement {
} }
function VMDetailsTabGeneral(p: DetailsInnerProps): React.ReactElement { function VMDetailsTabGeneral(p: DetailsInnerProps): React.ReactElement {
const [addGroup, setAddGroup] = React.useState(false);
return ( return (
<Grid container spacing={2}> <Grid container spacing={2}>
{ {
@ -184,50 +174,6 @@ function VMDetailsTabGeneral(p: DetailsInnerProps): React.ReactElement {
}} }}
multiline={true} multiline={true}
/> />
<div style={{ display: "flex" }}>
{addGroup ? (
<TextInput
label="Group"
editable={p.editable}
value={p.vm.group}
onValueChange={(v) => {
p.vm.group = v;
p.onChange?.();
}}
size={ServerApi.Config.constraints.group_id_size}
/>
) : (
<SelectInput
editable={p.editable}
label="Group"
onValueChange={(v) => {
p.vm.group = v! as any;
p.onChange?.();
}}
value={p.vm.group}
options={[
{ label: "None" },
...p.groupsList.map((g) => {
return { value: g, label: g };
}),
]}
/>
)}
{p.editable && (
<Tooltip
title={
addGroup
? "Use an existing group"
: "Add a new group instead of using existing one"
}
>
<IconButton onClick={() => setAddGroup(!addGroup)}>
{addGroup ? <ListIcon /> : <AddIcon />}
</IconButton>
</Tooltip>
)}
</div>
</EditSection> </EditSection>
{/* General section */} {/* General section */}