Can query hypervisor information
This commit is contained in:
parent
fbe11af121
commit
57c023b45b
23
README.md
23
README.md
@ -1,2 +1,25 @@
|
|||||||
# VirtWEB
|
# VirtWEB
|
||||||
WIP project
|
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.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
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]]
|
[[package]]
|
||||||
name = "actix-codec"
|
name = "actix-codec"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
@ -301,6 +326,17 @@ dependencies = [
|
|||||||
"syn 2.0.29",
|
"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]]
|
[[package]]
|
||||||
name = "addr2line"
|
name = "addr2line"
|
||||||
version = "0.21.0"
|
version = "0.21.0"
|
||||||
@ -703,6 +739,25 @@ dependencies = [
|
|||||||
"cfg-if",
|
"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]]
|
[[package]]
|
||||||
name = "crypto-common"
|
name = "crypto-common"
|
||||||
version = "0.1.6"
|
version = "0.1.6"
|
||||||
@ -2042,6 +2097,26 @@ version = "0.9.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
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]]
|
[[package]]
|
||||||
name = "virtue"
|
name = "virtue"
|
||||||
version = "0.0.13"
|
version = "0.0.13"
|
||||||
@ -2052,6 +2127,7 @@ checksum = "9dcc60c0624df774c82a0ef104151231d37da4962957d691c011c852b2473314"
|
|||||||
name = "virtweb_backend"
|
name = "virtweb_backend"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"actix",
|
||||||
"actix-cors",
|
"actix-cors",
|
||||||
"actix-files",
|
"actix-files",
|
||||||
"actix-identity",
|
"actix-identity",
|
||||||
@ -2071,6 +2147,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"url",
|
"url",
|
||||||
|
"virt",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -11,6 +11,7 @@ env_logger = "0.10.0"
|
|||||||
clap = { version = "4.3.19", features = ["derive", "env"] }
|
clap = { version = "4.3.19", features = ["derive", "env"] }
|
||||||
light-openid = { version = "1.0.1", features = ["crypto-wrapper"] }
|
light-openid = { version = "1.0.1", features = ["crypto-wrapper"] }
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
|
actix = "0.13.1"
|
||||||
actix-web = "4"
|
actix-web = "4"
|
||||||
actix-remote-ip = "0.1.0"
|
actix-remote-ip = "0.1.0"
|
||||||
actix-session = { version = "0.7.2", features = ["cookie-session"] }
|
actix-session = { version = "0.7.2", features = ["cookie-session"] }
|
||||||
@ -25,3 +26,4 @@ actix-multipart = "0.6.1"
|
|||||||
tempfile = "3.8.0"
|
tempfile = "3.8.0"
|
||||||
reqwest = { version = "0.11.18", features = ["stream"] }
|
reqwest = { version = "0.11.18", features = ["stream"] }
|
||||||
url = "2.4.0"
|
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
|
/// Directory where temporary files are stored
|
||||||
#[arg(long, env, default_value = "/tmp")]
|
#[arg(long, env, default_value = "/tmp")]
|
||||||
pub temp_dir: String,
|
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! {
|
lazy_static::lazy_static! {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
|
use crate::libvirt_client::LibVirtClient;
|
||||||
use actix_web::body::BoxBody;
|
use actix_web::body::BoxBody;
|
||||||
use actix_web::HttpResponse;
|
use actix_web::{web, HttpResponse};
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::fmt::{Display, Formatter};
|
use std::fmt::{Display, Formatter};
|
||||||
use std::io::ErrorKind;
|
use std::io::ErrorKind;
|
||||||
@ -78,3 +79,5 @@ impl From<reqwest::header::ToStrError> for HttpErr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub type HttpResult = Result<HttpResponse, 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::app_config::AppConfig;
|
||||||
use crate::constants;
|
use crate::constants;
|
||||||
|
use crate::controllers::{HttpResult, LibVirtReq};
|
||||||
use crate::extractors::local_auth_extractor::LocalAuthEnabled;
|
use crate::extractors::local_auth_extractor::LocalAuthEnabled;
|
||||||
use actix_web::{HttpResponse, Responder};
|
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,
|
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 app_config;
|
||||||
pub mod constants;
|
pub mod constants;
|
||||||
pub mod controllers;
|
pub mod controllers;
|
||||||
pub mod extractors;
|
pub mod extractors;
|
||||||
|
pub mod libvirt_client;
|
||||||
pub mod middlewares;
|
pub mod middlewares;
|
||||||
pub mod utils;
|
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_cors::Cors;
|
||||||
use actix_identity::config::LogoutBehaviour;
|
use actix_identity::config::LogoutBehaviour;
|
||||||
use actix_identity::IdentityMiddleware;
|
use actix_identity::IdentityMiddleware;
|
||||||
@ -13,12 +14,14 @@ use actix_web::web::Data;
|
|||||||
use actix_web::{web, App, HttpServer};
|
use actix_web::{web, App, HttpServer};
|
||||||
use light_openid::basic_state_manager::BasicStateManager;
|
use light_openid::basic_state_manager::BasicStateManager;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use virtweb_backend::actors::libvirt_actor::LibVirtActor;
|
||||||
use virtweb_backend::app_config::AppConfig;
|
use virtweb_backend::app_config::AppConfig;
|
||||||
use virtweb_backend::constants;
|
use virtweb_backend::constants;
|
||||||
use virtweb_backend::constants::{
|
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::{auth_controller, iso_controller, server_controller};
|
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::middlewares::auth_middleware::AuthChecker;
|
||||||
use virtweb_backend::utils::files_utils;
|
use virtweb_backend::utils::files_utils;
|
||||||
|
|
||||||
@ -29,6 +32,13 @@ async fn main() -> std::io::Result<()> {
|
|||||||
log::debug!("Create required directory, if missing");
|
log::debug!("Create required directory, if missing");
|
||||||
files_utils::create_directory_if_missing(&AppConfig::get().iso_storage_path()).unwrap();
|
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);
|
log::info!("Start to listen on {}", AppConfig::get().listen_address);
|
||||||
|
|
||||||
let state_manager = Data::new(BasicStateManager::new());
|
let state_manager = Data::new(BasicStateManager::new());
|
||||||
@ -69,6 +79,7 @@ async fn main() -> std::io::Result<()> {
|
|||||||
.app_data(Data::new(RemoteIPConfig {
|
.app_data(Data::new(RemoteIPConfig {
|
||||||
proxy: AppConfig::get().proxy_ip.clone(),
|
proxy: AppConfig::get().proxy_ip.clone(),
|
||||||
}))
|
}))
|
||||||
|
.app_data(conn.clone())
|
||||||
// Uploaded files
|
// Uploaded files
|
||||||
.app_data(MultipartFormConfig::default().total_limit(constants::ISO_MAX_SIZE))
|
.app_data(MultipartFormConfig::default().total_limit(constants::ISO_MAX_SIZE))
|
||||||
.app_data(TempFileConfig::default().directory(&AppConfig::get().temp_dir))
|
.app_data(TempFileConfig::default().directory(&AppConfig::get().temp_dir))
|
||||||
@ -78,6 +89,10 @@ async fn main() -> std::io::Result<()> {
|
|||||||
"/api/server/static_config",
|
"/api/server/static_config",
|
||||||
web::get().to(server_controller::static_config),
|
web::get().to(server_controller::static_config),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/api/server/info",
|
||||||
|
web::get().to(server_controller::server_info),
|
||||||
|
)
|
||||||
// Auth controller
|
// Auth controller
|
||||||
.route(
|
.route(
|
||||||
"/api/auth/local",
|
"/api/auth/local",
|
||||||
|
@ -9,6 +9,8 @@ export interface ServerConfig {
|
|||||||
|
|
||||||
let config: ServerConfig | null = null;
|
let config: ServerConfig | null = null;
|
||||||
|
|
||||||
|
export interface ServerInfo {}
|
||||||
|
|
||||||
export class ServerApi {
|
export class ServerApi {
|
||||||
/**
|
/**
|
||||||
* Get server configuration
|
* Get server configuration
|
||||||
@ -29,4 +31,16 @@ export class ServerApi {
|
|||||||
if (config === null) throw new Error("Missing configuration!");
|
if (config === null) throw new Error("Missing configuration!");
|
||||||
return config;
|
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";
|
} from "@mui/material";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import Grid from "@mui/material/Grid";
|
|
||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useAuth } from "../../App";
|
import { useAuth } from "../../App";
|
||||||
import { AuthApi } from "../../api/AuthApi";
|
import { AuthApi } from "../../api/AuthApi";
|
||||||
import { ServerApi } from "../../api/ServerApi";
|
import { ServerApi } from "../../api/ServerApi";
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { mdiDisc, mdiHome, mdiLanPending } from "@mdi/js";
|
||||||
|
import Icon from "@mdi/react";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
List,
|
List,
|
||||||
@ -5,13 +7,10 @@ import {
|
|||||||
ListItemIcon,
|
ListItemIcon,
|
||||||
ListItemSecondaryAction,
|
ListItemSecondaryAction,
|
||||||
ListItemText,
|
ListItemText,
|
||||||
ListSubheader,
|
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { VirtWebAppBar } from "./VirtWebAppBar";
|
|
||||||
import { RouterLink } from "./RouterLink";
|
|
||||||
import { Outlet, useLocation } from "react-router-dom";
|
import { Outlet, useLocation } from "react-router-dom";
|
||||||
import Icon from "@mdi/react";
|
import { RouterLink } from "./RouterLink";
|
||||||
import { mdiDisc, mdiHome, mdiLanPending } from "@mdi/js";
|
import { VirtWebAppBar } from "./VirtWebAppBar";
|
||||||
|
|
||||||
export function BaseAuthenticatedPage(): React.ReactElement {
|
export function BaseAuthenticatedPage(): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
|
Loading…
Reference in New Issue
Block a user