Compare commits
1 Commits
master
...
renovate/l
Author | SHA1 | Date | |
---|---|---|---|
4bed002a36 |
@ -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
|
||||||
|
1501
virtweb_backend/Cargo.lock
generated
1501
virtweb_backend/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
@ -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 })
|
||||||
}
|
}
|
||||||
|
@ -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))
|
|
||||||
}
|
|
@ -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;
|
||||||
|
@ -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!");
|
||||||
|
|
||||||
|
@ -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}");
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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() {
|
||||||
|
@ -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",
|
||||||
|
@ -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`:
|
||||||
|
12127
virtweb_frontend/package-lock.json
generated
12127
virtweb_frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -6,36 +6,36 @@
|
|||||||
"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": "^16.0.0",
|
||||||
"@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": "^10.0.0",
|
||||||
"@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": "^10.0.0",
|
||||||
"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": "^3.5.2",
|
||||||
"xml-formatter": "^3.6.0"
|
"xml-formatter": "^3.6.0"
|
||||||
},
|
},
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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",
|
||||||
|
@ -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,100 +72,39 @@ 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}>
|
<TableRow
|
||||||
{p.groups.length > 1 && (
|
hover
|
||||||
<TableRow>
|
key={row.name}
|
||||||
<TableCell
|
sx={{ "&:last-child td, &:last-child th": { border: 0 } }}
|
||||||
style={{ paddingBottom: 2, paddingTop: 2 }}
|
onDoubleClick={() => navigate(row.ViewURL)}
|
||||||
colSpan={6}
|
>
|
||||||
>
|
<TableCell component="th" scope="row">
|
||||||
<IconButton
|
{row.name}
|
||||||
size="small"
|
</TableCell>
|
||||||
onClick={() => toggleHiddenGroup(g)}
|
<TableCell>{row.description ?? ""}</TableCell>
|
||||||
>
|
<TableCell>{filesize(row.memory * 1000 * 1000)}</TableCell>
|
||||||
{!hiddenGroups?.has(g) ? (
|
<TableCell>
|
||||||
<KeyboardArrowUpIcon />
|
<VMStatusWidget vm={row} />
|
||||||
) : (
|
</TableCell>
|
||||||
<KeyboardArrowDownIcon />
|
<TableCell>
|
||||||
)}
|
<Tooltip title="View this VM">
|
||||||
|
<RouterLink to={row.ViewURL}>
|
||||||
|
<IconButton>
|
||||||
|
<VisibilityIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
{g ?? "default"}
|
</RouterLink>
|
||||||
</TableCell>
|
</Tooltip>
|
||||||
</TableRow>
|
</TableCell>
|
||||||
)}
|
</TableRow>
|
||||||
|
|
||||||
{!hiddenGroups.has(g) &&
|
|
||||||
p.list
|
|
||||||
.filter((row) => row.group === g)
|
|
||||||
.map((row) => (
|
|
||||||
<TableRow
|
|
||||||
hover
|
|
||||||
key={row.name}
|
|
||||||
sx={{ "&:last-child td, &:last-child th": { border: 0 } }}
|
|
||||||
onDoubleClick={() => navigate(row.ViewURL)}
|
|
||||||
>
|
|
||||||
<TableCell component="th" scope="row">
|
|
||||||
{row.name}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>{row.description ?? ""}</TableCell>
|
|
||||||
<TableCell>{vmMemoryToHuman(row.memory)}</TableCell>
|
|
||||||
<TableCell>{row.number_vcpu}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<VMStatusWidget
|
|
||||||
vm={row}
|
|
||||||
onChange={(s) => updateVMState(row, s)}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Tooltip title="View this VM">
|
|
||||||
<RouterLink to={row.ViewURL}>
|
|
||||||
<IconButton>
|
|
||||||
<VisibilityIcon />
|
|
||||||
</IconButton>
|
|
||||||
</RouterLink>
|
|
||||||
</Tooltip>
|
|
||||||
</TableCell>
|
|
||||||
</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);
|
|
||||||
}
|
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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}
|
||||||
|
@ -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}
|
||||||
|
@ -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";
|
||||||
|
@ -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";
|
||||||
|
@ -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;
|
||||||
|
@ -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";
|
||||||
|
@ -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 */}
|
||||||
|
Loading…
Reference in New Issue
Block a user