Can query hypervisor information
This commit is contained in:
		
							
								
								
									
										23
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								README.md
									
									
									
									
									
								
							@@ -1,2 +1,25 @@
 | 
			
		||||
# VirtWEB
 | 
			
		||||
WIP project
 | 
			
		||||
 | 
			
		||||
## Development requirements
 | 
			
		||||
1. The `libvirt-dev` package must be installed:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
sudo apt install libvirt-dev
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
2. Libvirt must also be installed:
 | 
			
		||||
```bash
 | 
			
		||||
sudo apt install qemu-kvm libvirt-daemon-system
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
3. Allow the current user to manage VMs:
 | 
			
		||||
```
 | 
			
		||||
sudo adduser $USER libvirt
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
> Note: You will need to login again for this change to take effect.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## Production requirements
 | 
			
		||||
TODO
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										77
									
								
								virtweb_backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										77
									
								
								virtweb_backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							@@ -2,6 +2,31 @@
 | 
			
		||||
# It is not intended for manual editing.
 | 
			
		||||
version = 3
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "actix"
 | 
			
		||||
version = "0.13.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "cba56612922b907719d4a01cf11c8d5b458e7d3dba946d0435f20f58d6795ed2"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "actix-macros",
 | 
			
		||||
 "actix-rt",
 | 
			
		||||
 "actix_derive",
 | 
			
		||||
 "bitflags 2.4.0",
 | 
			
		||||
 "bytes",
 | 
			
		||||
 "crossbeam-channel",
 | 
			
		||||
 "futures-core",
 | 
			
		||||
 "futures-sink",
 | 
			
		||||
 "futures-task",
 | 
			
		||||
 "futures-util",
 | 
			
		||||
 "log",
 | 
			
		||||
 "once_cell",
 | 
			
		||||
 "parking_lot",
 | 
			
		||||
 "pin-project-lite",
 | 
			
		||||
 "smallvec",
 | 
			
		||||
 "tokio",
 | 
			
		||||
 "tokio-util",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "actix-codec"
 | 
			
		||||
version = "0.5.1"
 | 
			
		||||
@@ -301,6 +326,17 @@ dependencies = [
 | 
			
		||||
 "syn 2.0.29",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "actix_derive"
 | 
			
		||||
version = "0.6.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "6d44b8fee1ced9671ba043476deddef739dd0959bf77030b26b738cc591737a7"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "proc-macro2",
 | 
			
		||||
 "quote",
 | 
			
		||||
 "syn 1.0.109",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "addr2line"
 | 
			
		||||
version = "0.21.0"
 | 
			
		||||
@@ -703,6 +739,25 @@ dependencies = [
 | 
			
		||||
 "cfg-if",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "crossbeam-channel"
 | 
			
		||||
version = "0.5.8"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "cfg-if",
 | 
			
		||||
 "crossbeam-utils",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "crossbeam-utils"
 | 
			
		||||
version = "0.8.16"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "cfg-if",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "crypto-common"
 | 
			
		||||
version = "0.1.6"
 | 
			
		||||
@@ -2042,6 +2097,26 @@ version = "0.9.4"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "virt"
 | 
			
		||||
version = "0.3.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "9316a0df71f1ec209e7ef8ab07097e4181945b245d3348f57de07a3e811e53cf"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "libc",
 | 
			
		||||
 "virt-sys",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "virt-sys"
 | 
			
		||||
version = "0.2.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "cd39f6e0d0ab3fe7c371fac05c9b6ca72e186f06a2e666fb3b95441091eba2db"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "libc",
 | 
			
		||||
 "pkg-config",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "virtue"
 | 
			
		||||
version = "0.0.13"
 | 
			
		||||
@@ -2052,6 +2127,7 @@ checksum = "9dcc60c0624df774c82a0ef104151231d37da4962957d691c011c852b2473314"
 | 
			
		||||
name = "virtweb_backend"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "actix",
 | 
			
		||||
 "actix-cors",
 | 
			
		||||
 "actix-files",
 | 
			
		||||
 "actix-identity",
 | 
			
		||||
@@ -2071,6 +2147,7 @@ dependencies = [
 | 
			
		||||
 "serde_json",
 | 
			
		||||
 "tempfile",
 | 
			
		||||
 "url",
 | 
			
		||||
 "virt",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,7 @@ env_logger = "0.10.0"
 | 
			
		||||
clap = { version = "4.3.19", features = ["derive", "env"] }
 | 
			
		||||
light-openid = { version = "1.0.1", features = ["crypto-wrapper"] }
 | 
			
		||||
lazy_static = "1.4.0"
 | 
			
		||||
actix = "0.13.1"
 | 
			
		||||
actix-web = "4"
 | 
			
		||||
actix-remote-ip = "0.1.0"
 | 
			
		||||
actix-session = { version = "0.7.2", features = ["cookie-session"] }
 | 
			
		||||
@@ -25,3 +26,4 @@ actix-multipart = "0.6.1"
 | 
			
		||||
tempfile = "3.8.0"
 | 
			
		||||
reqwest = { version = "0.11.18", features = ["stream"] }
 | 
			
		||||
url = "2.4.0"
 | 
			
		||||
virt = "0.3.0"
 | 
			
		||||
							
								
								
									
										79
									
								
								virtweb_backend/src/actors/libvirt_actor.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								virtweb_backend/src/actors/libvirt_actor.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,79 @@
 | 
			
		||||
use crate::app_config::AppConfig;
 | 
			
		||||
use actix::{Actor, Context, Handler, Message};
 | 
			
		||||
use virt::connect::Connect;
 | 
			
		||||
 | 
			
		||||
pub struct LibVirtActor {
 | 
			
		||||
    m: Connect,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl LibVirtActor {
 | 
			
		||||
    /// Connect to hypervisor
 | 
			
		||||
    pub async fn connect() -> anyhow::Result<Self> {
 | 
			
		||||
        let hypervisor_uri = AppConfig::get().hypervisor_uri.as_deref().unwrap_or("");
 | 
			
		||||
        log::info!(
 | 
			
		||||
            "Will connect to hypvervisor at address '{}'",
 | 
			
		||||
            hypervisor_uri
 | 
			
		||||
        );
 | 
			
		||||
        let conn = Connect::open(hypervisor_uri)?;
 | 
			
		||||
 | 
			
		||||
        Ok(Self { m: conn })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Actor for LibVirtActor {
 | 
			
		||||
    type Context = Context<Self>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Message)]
 | 
			
		||||
#[rtype(result = "anyhow::Result<HypervisorInfo>")]
 | 
			
		||||
pub struct GetHypervisorInfo;
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize)]
 | 
			
		||||
pub struct HypervisorInfo {
 | 
			
		||||
    pub r#type: String,
 | 
			
		||||
    pub hyp_version: u32,
 | 
			
		||||
    pub lib_version: u32,
 | 
			
		||||
    pub capabilities: String,
 | 
			
		||||
    pub free_memory: u64,
 | 
			
		||||
    pub hostname: String,
 | 
			
		||||
    pub node: HypervisorNodeInfo,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize)]
 | 
			
		||||
pub struct HypervisorNodeInfo {
 | 
			
		||||
    pub cpu_model: String,
 | 
			
		||||
    /// Memory size in kilobytes
 | 
			
		||||
    pub memory_size: u64,
 | 
			
		||||
    pub number_of_active_cpus: u32,
 | 
			
		||||
    pub cpu_frequency_mhz: u32,
 | 
			
		||||
    pub number_of_numa_cell: u32,
 | 
			
		||||
    pub number_of_cpu_socket_per_node: u32,
 | 
			
		||||
    pub number_of_core_per_sockets: u32,
 | 
			
		||||
    pub number_of_threads_per_sockets: u32,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Handler<GetHypervisorInfo> for LibVirtActor {
 | 
			
		||||
    type Result = anyhow::Result<HypervisorInfo>;
 | 
			
		||||
 | 
			
		||||
    fn handle(&mut self, _msg: GetHypervisorInfo, _ctx: &mut Self::Context) -> Self::Result {
 | 
			
		||||
        let node = self.m.get_node_info()?;
 | 
			
		||||
        Ok(HypervisorInfo {
 | 
			
		||||
            r#type: self.m.get_type()?,
 | 
			
		||||
            hyp_version: self.m.get_hyp_version()?,
 | 
			
		||||
            lib_version: self.m.get_lib_version()?,
 | 
			
		||||
            capabilities: self.m.get_capabilities()?,
 | 
			
		||||
            free_memory: self.m.get_free_memory()?,
 | 
			
		||||
            hostname: self.m.get_hostname()?,
 | 
			
		||||
            node: HypervisorNodeInfo {
 | 
			
		||||
                cpu_model: node.model,
 | 
			
		||||
                memory_size: node.memory,
 | 
			
		||||
                number_of_active_cpus: node.cpus,
 | 
			
		||||
                cpu_frequency_mhz: node.mhz,
 | 
			
		||||
                number_of_numa_cell: node.nodes,
 | 
			
		||||
                number_of_cpu_socket_per_node: node.sockets,
 | 
			
		||||
                number_of_core_per_sockets: node.cores,
 | 
			
		||||
                number_of_threads_per_sockets: node.threads,
 | 
			
		||||
            },
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1
									
								
								virtweb_backend/src/actors/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								virtweb_backend/src/actors/mod.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
pub mod libvirt_actor;
 | 
			
		||||
@@ -72,6 +72,10 @@ pub struct AppConfig {
 | 
			
		||||
    /// Directory where temporary files are stored
 | 
			
		||||
    #[arg(long, env, default_value = "/tmp")]
 | 
			
		||||
    pub temp_dir: String,
 | 
			
		||||
 | 
			
		||||
    /// Hypervisor URI. If not specified, "" will be used instead
 | 
			
		||||
    #[arg(long, env)]
 | 
			
		||||
    pub hypervisor_uri: Option<String>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
lazy_static::lazy_static! {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
use crate::libvirt_client::LibVirtClient;
 | 
			
		||||
use actix_web::body::BoxBody;
 | 
			
		||||
use actix_web::HttpResponse;
 | 
			
		||||
use actix_web::{web, HttpResponse};
 | 
			
		||||
use std::error::Error;
 | 
			
		||||
use std::fmt::{Display, Formatter};
 | 
			
		||||
use std::io::ErrorKind;
 | 
			
		||||
@@ -78,3 +79,5 @@ impl From<reqwest::header::ToStrError> for HttpErr {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub type HttpResult = Result<HttpResponse, HttpErr>;
 | 
			
		||||
 | 
			
		||||
pub type LibVirtReq = web::Data<LibVirtClient>;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,7 @@
 | 
			
		||||
use crate::actors::libvirt_actor::HypervisorInfo;
 | 
			
		||||
use crate::app_config::AppConfig;
 | 
			
		||||
use crate::constants;
 | 
			
		||||
use crate::controllers::{HttpResult, LibVirtReq};
 | 
			
		||||
use crate::extractors::local_auth_extractor::LocalAuthEnabled;
 | 
			
		||||
use actix_web::{HttpResponse, Responder};
 | 
			
		||||
 | 
			
		||||
@@ -23,3 +25,14 @@ pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder {
 | 
			
		||||
        iso_max_size: constants::ISO_MAX_SIZE,
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize)]
 | 
			
		||||
struct ServerInfo {
 | 
			
		||||
    hypervisor: HypervisorInfo,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub async fn server_info(client: LibVirtReq) -> HttpResult {
 | 
			
		||||
    Ok(HttpResponse::Ok().json(ServerInfo {
 | 
			
		||||
        hypervisor: client.get_info().await?,
 | 
			
		||||
    }))
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,8 @@
 | 
			
		||||
pub mod actors;
 | 
			
		||||
pub mod app_config;
 | 
			
		||||
pub mod constants;
 | 
			
		||||
pub mod controllers;
 | 
			
		||||
pub mod extractors;
 | 
			
		||||
pub mod libvirt_client;
 | 
			
		||||
pub mod middlewares;
 | 
			
		||||
pub mod utils;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										13
									
								
								virtweb_backend/src/libvirt_client.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								virtweb_backend/src/libvirt_client.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
			
		||||
use crate::actors::libvirt_actor;
 | 
			
		||||
use crate::actors::libvirt_actor::{HypervisorInfo, LibVirtActor};
 | 
			
		||||
use actix::Addr;
 | 
			
		||||
 | 
			
		||||
#[derive(Clone)]
 | 
			
		||||
pub struct LibVirtClient(pub Addr<LibVirtActor>);
 | 
			
		||||
 | 
			
		||||
impl LibVirtClient {
 | 
			
		||||
    /// Get hypervisor info
 | 
			
		||||
    pub async fn get_info(&self) -> anyhow::Result<HypervisorInfo> {
 | 
			
		||||
        self.0.send(libvirt_actor::GetHypervisorInfo).await?
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,3 +1,4 @@
 | 
			
		||||
use actix::Actor;
 | 
			
		||||
use actix_cors::Cors;
 | 
			
		||||
use actix_identity::config::LogoutBehaviour;
 | 
			
		||||
use actix_identity::IdentityMiddleware;
 | 
			
		||||
@@ -13,12 +14,14 @@ use actix_web::web::Data;
 | 
			
		||||
use actix_web::{web, App, HttpServer};
 | 
			
		||||
use light_openid::basic_state_manager::BasicStateManager;
 | 
			
		||||
use std::time::Duration;
 | 
			
		||||
use virtweb_backend::actors::libvirt_actor::LibVirtActor;
 | 
			
		||||
use virtweb_backend::app_config::AppConfig;
 | 
			
		||||
use virtweb_backend::constants;
 | 
			
		||||
use virtweb_backend::constants::{
 | 
			
		||||
    MAX_INACTIVITY_DURATION, MAX_SESSION_DURATION, SESSION_COOKIE_NAME,
 | 
			
		||||
};
 | 
			
		||||
use virtweb_backend::controllers::{auth_controller, iso_controller, server_controller};
 | 
			
		||||
use virtweb_backend::libvirt_client::LibVirtClient;
 | 
			
		||||
use virtweb_backend::middlewares::auth_middleware::AuthChecker;
 | 
			
		||||
use virtweb_backend::utils::files_utils;
 | 
			
		||||
 | 
			
		||||
@@ -29,6 +32,13 @@ async fn main() -> std::io::Result<()> {
 | 
			
		||||
    log::debug!("Create required directory, if missing");
 | 
			
		||||
    files_utils::create_directory_if_missing(&AppConfig::get().iso_storage_path()).unwrap();
 | 
			
		||||
 | 
			
		||||
    let conn = Data::new(LibVirtClient(
 | 
			
		||||
        LibVirtActor::connect()
 | 
			
		||||
            .await
 | 
			
		||||
            .expect("Failed to connect to hypervisor!")
 | 
			
		||||
            .start(),
 | 
			
		||||
    ));
 | 
			
		||||
 | 
			
		||||
    log::info!("Start to listen on {}", AppConfig::get().listen_address);
 | 
			
		||||
 | 
			
		||||
    let state_manager = Data::new(BasicStateManager::new());
 | 
			
		||||
@@ -69,6 +79,7 @@ async fn main() -> std::io::Result<()> {
 | 
			
		||||
            .app_data(Data::new(RemoteIPConfig {
 | 
			
		||||
                proxy: AppConfig::get().proxy_ip.clone(),
 | 
			
		||||
            }))
 | 
			
		||||
            .app_data(conn.clone())
 | 
			
		||||
            // Uploaded files
 | 
			
		||||
            .app_data(MultipartFormConfig::default().total_limit(constants::ISO_MAX_SIZE))
 | 
			
		||||
            .app_data(TempFileConfig::default().directory(&AppConfig::get().temp_dir))
 | 
			
		||||
@@ -78,6 +89,10 @@ async fn main() -> std::io::Result<()> {
 | 
			
		||||
                "/api/server/static_config",
 | 
			
		||||
                web::get().to(server_controller::static_config),
 | 
			
		||||
            )
 | 
			
		||||
            .route(
 | 
			
		||||
                "/api/server/info",
 | 
			
		||||
                web::get().to(server_controller::server_info),
 | 
			
		||||
            )
 | 
			
		||||
            // Auth controller
 | 
			
		||||
            .route(
 | 
			
		||||
                "/api/auth/local",
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,8 @@ export interface ServerConfig {
 | 
			
		||||
 | 
			
		||||
let config: ServerConfig | null = null;
 | 
			
		||||
 | 
			
		||||
export interface ServerInfo {}
 | 
			
		||||
 | 
			
		||||
export class ServerApi {
 | 
			
		||||
  /**
 | 
			
		||||
   * Get server configuration
 | 
			
		||||
@@ -29,4 +31,16 @@ export class ServerApi {
 | 
			
		||||
    if (config === null) throw new Error("Missing configuration!");
 | 
			
		||||
    return config;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get server information
 | 
			
		||||
   */
 | 
			
		||||
  static async SystemInfo(): Promise<ServerInfo> {
 | 
			
		||||
    return (
 | 
			
		||||
      await APIClient.exec({
 | 
			
		||||
        method: "GET",
 | 
			
		||||
        uri: "/server/info",
 | 
			
		||||
      })
 | 
			
		||||
    ).data;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -11,11 +11,10 @@ import {
 | 
			
		||||
} from "@mui/material";
 | 
			
		||||
import Box from "@mui/material/Box";
 | 
			
		||||
import Button from "@mui/material/Button";
 | 
			
		||||
import Grid from "@mui/material/Grid";
 | 
			
		||||
import TextField from "@mui/material/TextField";
 | 
			
		||||
import Typography from "@mui/material/Typography";
 | 
			
		||||
import * as React from "react";
 | 
			
		||||
import { Link, useNavigate } from "react-router-dom";
 | 
			
		||||
import { useNavigate } from "react-router-dom";
 | 
			
		||||
import { useAuth } from "../../App";
 | 
			
		||||
import { AuthApi } from "../../api/AuthApi";
 | 
			
		||||
import { ServerApi } from "../../api/ServerApi";
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,5 @@
 | 
			
		||||
import { mdiDisc, mdiHome, mdiLanPending } from "@mdi/js";
 | 
			
		||||
import Icon from "@mdi/react";
 | 
			
		||||
import {
 | 
			
		||||
  Box,
 | 
			
		||||
  List,
 | 
			
		||||
@@ -5,13 +7,10 @@ import {
 | 
			
		||||
  ListItemIcon,
 | 
			
		||||
  ListItemSecondaryAction,
 | 
			
		||||
  ListItemText,
 | 
			
		||||
  ListSubheader,
 | 
			
		||||
} from "@mui/material";
 | 
			
		||||
import { VirtWebAppBar } from "./VirtWebAppBar";
 | 
			
		||||
import { RouterLink } from "./RouterLink";
 | 
			
		||||
import { Outlet, useLocation } from "react-router-dom";
 | 
			
		||||
import Icon from "@mdi/react";
 | 
			
		||||
import { mdiDisc, mdiHome, mdiLanPending } from "@mdi/js";
 | 
			
		||||
import { RouterLink } from "./RouterLink";
 | 
			
		||||
import { VirtWebAppBar } from "./VirtWebAppBar";
 | 
			
		||||
 | 
			
		||||
export function BaseAuthenticatedPage(): React.ReactElement {
 | 
			
		||||
  return (
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user