3 Commits

Author SHA1 Message Date
26fee59c5d Can determine the rights of the token over the group
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-11-30 15:07:22 +01:00
a8a75328a9 Get information about the VM of a group
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-11-30 11:22:32 +01:00
09f54bf3c1 Refactorize VM information management
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-11-30 10:59:38 +01:00
33 changed files with 2801 additions and 3497 deletions

View File

@@ -5,7 +5,7 @@ name: default
steps: steps:
- name: frontend_build - name: frontend_build
image: node:24 image: node:23
volumes: volumes:
- name: frontend_app - name: frontend_app
path: /tmp/frontend_build path: /tmp/frontend_build

View File

@@ -1,11 +1,4 @@
# VirtWeb Remote # VirtWeb Remote
Web UI that allows to start and stop VMs managed by VirtWEB without having to expose the VirtWEB directly on the Internet. WIP project
VirtWebRemote rely on OpenID to authenticate users. This project aims to use the VirtWeb API to start and stop VM without directly exposing the VirtWEB API to the Internet.
VirtWebRemote authenticates against VirtWEB API using an API token. Both the token ID and private key are required to be able to authenticate against the VirtWEB API.
## Docker image options
```bash
docker run --rm -it pierre42100/virtweb_remote --help
```

1042
remote_backend/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,28 +1,28 @@
[package] [package]
name = "remote_backend" name = "remote_backend"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
log = "0.4.28" log = "0.4.21"
env_logger = "0.11.8" env_logger = "0.11.3"
clap = { version = "4.5.53", features = ["derive", "env"] } clap = { version = "4.5.21", features = ["derive", "env"] }
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.215", features = ["derive"] }
light-openid = { version = "1.0.4", features = ["crypto-wrapper"] } light-openid = { version = "1.0.2", features = ["crypto-wrapper"] }
basic-jwt = "0.3.0" basic-jwt = "0.2.0"
actix-web = "4.12.1" actix-web = "4.5.1"
actix-remote-ip = "0.1.0" actix-remote-ip = "0.1.0"
actix-session = { version = "0.11.0", features = ["cookie-session"] } actix-session = { version = "0.10.1", features = ["cookie-session"] }
actix-identity = "0.9.0" actix-identity = "0.8.0"
actix-cors = "0.7.1" actix-cors = "0.7.0"
lazy_static = "1.5.0" lazy_static = "1.4.0"
anyhow = "1.0.100" anyhow = "1.0.93"
reqwest = { version = "0.12.24", features = ["json"] } reqwest = { version = "0.12.9", features = ["json"] }
thiserror = "2.0.17" thiserror = "2.0.3"
uuid = { version = "1.18.1", features = ["v4", "serde"] } uuid = { version = "1.8.0", features = ["v4", "serde"] }
futures-util = "0.3.31" futures-util = "0.3.30"
lazy-regex = "3.4.2" lazy-regex = "3.1.0"
mime_guess = "2.0.5" mime_guess = "2.0.4"
rust-embed = { version = "8.9.0" } rust-embed = { version = "8.3.0" }

View File

@@ -6,4 +6,4 @@ RUN apt-get update \
COPY remote_backend /usr/local/bin/remote_backend COPY remote_backend /usr/local/bin/remote_backend
ENTRYPOINT ["/usr/local/bin/remote_backend"] ENTRYPOINT /usr/local/bin/remote_backend

View File

@@ -1,6 +1,6 @@
use actix_remote_ip::RemoteIP; use actix_remote_ip::RemoteIP;
use actix_web::web::Data; use actix_web::web::Data;
use actix_web::{HttpResponse, Responder, web}; use actix_web::{web, HttpResponse, Responder};
use light_openid::basic_state_manager::BasicStateManager; use light_openid::basic_state_manager::BasicStateManager;
use crate::app_config::AppConfig; use crate::app_config::AppConfig;

View File

@@ -1,79 +0,0 @@
use crate::controllers::HttpResult;
use crate::virtweb_client;
use crate::virtweb_client::{GroupID, VMUuid};
use actix_web::{HttpResponse, web};
#[derive(serde::Deserialize)]
pub struct GroupIDInPath {
gid: GroupID,
}
#[derive(serde::Deserialize)]
pub struct VMIDInQuery {
vm_id: Option<VMUuid>,
}
/// Get the state of one or all VM
pub async fn vm_state(
path: web::Path<GroupIDInPath>,
query: web::Query<VMIDInQuery>,
) -> HttpResult {
Ok(HttpResponse::Ok().json(virtweb_client::group_vm_state(&path.gid, query.vm_id).await?))
}
/// Start one or all VM
pub async fn vm_start(
path: web::Path<GroupIDInPath>,
query: web::Query<VMIDInQuery>,
) -> HttpResult {
Ok(HttpResponse::Ok().json(virtweb_client::group_vm_start(&path.gid, query.vm_id).await?))
}
/// Shutdown one or all VM
pub async fn vm_shutdown(
path: web::Path<GroupIDInPath>,
query: web::Query<VMIDInQuery>,
) -> HttpResult {
Ok(HttpResponse::Ok().json(virtweb_client::group_vm_shutdown(&path.gid, query.vm_id).await?))
}
/// Kill one or all VM
pub async fn vm_kill(path: web::Path<GroupIDInPath>, query: web::Query<VMIDInQuery>) -> HttpResult {
Ok(HttpResponse::Ok().json(virtweb_client::group_vm_kill(&path.gid, query.vm_id).await?))
}
/// Reset one or all VM
pub async fn vm_reset(
path: web::Path<GroupIDInPath>,
query: web::Query<VMIDInQuery>,
) -> HttpResult {
Ok(HttpResponse::Ok().json(virtweb_client::group_vm_reset(&path.gid, query.vm_id).await?))
}
/// Suspend one or all VM
pub async fn vm_suspend(
path: web::Path<GroupIDInPath>,
query: web::Query<VMIDInQuery>,
) -> HttpResult {
Ok(HttpResponse::Ok().json(virtweb_client::group_vm_suspend(&path.gid, query.vm_id).await?))
}
/// Resume one or all VM
pub async fn vm_resume(
path: web::Path<GroupIDInPath>,
query: web::Query<VMIDInQuery>,
) -> HttpResult {
Ok(HttpResponse::Ok().json(virtweb_client::group_vm_resume(&path.gid, query.vm_id).await?))
}
/// Screenshot one or all VM
pub async fn vm_screenshot(
path: web::Path<GroupIDInPath>,
query: web::Query<VMIDInQuery>,
) -> HttpResult {
let screenshot = virtweb_client::group_vm_screenshot(&path.gid, query.vm_id).await?;
Ok(HttpResponse::Ok()
.insert_header(("content-type", "image/png"))
.body(screenshot))
}

View File

@@ -1,11 +1,11 @@
use actix_web::HttpResponse;
use actix_web::body::BoxBody; use actix_web::body::BoxBody;
use actix_web::http::StatusCode; use actix_web::http::StatusCode;
use actix_web::HttpResponse;
use std::error::Error; use std::error::Error;
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use std::io::ErrorKind;
pub mod auth_controller; pub mod auth_controller;
pub mod group_controller;
pub mod server_controller; pub mod server_controller;
pub mod static_controller; pub mod static_controller;
pub mod sys_info_controller; pub mod sys_info_controller;
@@ -37,7 +37,7 @@ impl actix_web::error::ResponseError for HttpErr {
} }
} }
fn error_response(&self) -> HttpResponse<BoxBody> { fn error_response(&self) -> HttpResponse<BoxBody> {
log::error!("Error while processing request! {self}"); log::error!("Error while processing request! {}", self);
HttpResponse::InternalServerError().body("Failed to execute request!") HttpResponse::InternalServerError().body("Failed to execute request!")
} }
@@ -51,7 +51,7 @@ impl From<anyhow::Error> for HttpErr {
impl From<Box<dyn Error>> for HttpErr { impl From<Box<dyn Error>> for HttpErr {
fn from(value: Box<dyn Error>) -> Self { fn from(value: Box<dyn Error>) -> Self {
HttpErr::Err(std::io::Error::other(value.to_string()).into()) HttpErr::Err(std::io::Error::new(ErrorKind::Other, value.to_string()).into())
} }
} }
@@ -81,7 +81,7 @@ impl From<reqwest::header::ToStrError> for HttpErr {
impl From<actix_web::Error> for HttpErr { impl From<actix_web::Error> for HttpErr {
fn from(value: actix_web::Error) -> Self { fn from(value: actix_web::Error) -> Self {
HttpErr::Err(std::io::Error::other(value.to_string()).into()) HttpErr::Err(std::io::Error::new(ErrorKind::Other, value.to_string()).into())
} }
} }

View File

@@ -2,7 +2,7 @@ use crate::app_config::AppConfig;
use crate::controllers::HttpResult; use crate::controllers::HttpResult;
use crate::extractors::auth_extractor::AuthExtractor; use crate::extractors::auth_extractor::AuthExtractor;
use crate::virtweb_client; use crate::virtweb_client;
use crate::virtweb_client::{GroupID, VMCaps, VMInfo}; use crate::virtweb_client::{GroupID, VMInfo};
use actix_web::HttpResponse; use actix_web::HttpResponse;
#[derive(serde::Serialize)] #[derive(serde::Serialize)]
@@ -29,16 +29,28 @@ pub struct Rights {
pub struct GroupInfo { pub struct GroupInfo {
id: GroupID, id: GroupID,
vms: Vec<VMInfo>, vms: Vec<VMInfo>,
#[serde(flatten)] can_get_state: bool,
caps: VMCaps, can_start: bool,
can_shutdown: bool,
can_kill: bool,
can_reset: bool,
can_suspend: bool,
can_resume: bool,
can_screenshot: bool,
} }
#[derive(Debug, serde::Serialize)] #[derive(Debug, serde::Serialize)]
pub struct VMInfoAndCaps { pub struct VMInfoAndCaps {
#[serde(flatten)] #[serde(flatten)]
info: VMInfo, info: VMInfo,
#[serde(flatten)] can_get_state: bool,
caps: VMCaps, can_start: bool,
can_shutdown: bool,
can_kill: bool,
can_reset: bool,
can_suspend: bool,
can_resume: bool,
can_screenshot: bool,
} }
pub async fn rights() -> HttpResult { pub async fn rights() -> HttpResult {
@@ -56,16 +68,14 @@ pub async fn rights() -> HttpResult {
res.groups.push(GroupInfo { res.groups.push(GroupInfo {
id: g.clone(), id: g.clone(),
vms: group_vms, vms: group_vms,
caps: VMCaps { can_get_state: rights.is_route_allowed("GET", &g.route_vm_state(None)),
can_get_state: rights.is_route_allowed("GET", &g.route_vm_state(None)), can_start: rights.is_route_allowed("GET", &g.route_vm_start(None)),
can_start: rights.is_route_allowed("GET", &g.route_vm_start(None)), can_shutdown: rights.is_route_allowed("GET", &g.route_vm_shutdown(None)),
can_shutdown: rights.is_route_allowed("GET", &g.route_vm_shutdown(None)), can_kill: rights.is_route_allowed("GET", &g.route_vm_kill(None)),
can_kill: rights.is_route_allowed("GET", &g.route_vm_kill(None)), can_reset: rights.is_route_allowed("GET", &g.route_vm_reset(None)),
can_reset: rights.is_route_allowed("GET", &g.route_vm_reset(None)), can_suspend: rights.is_route_allowed("GET", &g.route_vm_suspend(None)),
can_suspend: rights.is_route_allowed("GET", &g.route_vm_suspend(None)), can_resume: rights.is_route_allowed("GET", &g.route_vm_resume(None)),
can_resume: rights.is_route_allowed("GET", &g.route_vm_resume(None)), can_screenshot: rights.is_route_allowed("GET", &g.route_vm_screenshot(None)),
can_screenshot: rights.is_route_allowed("GET", &g.route_vm_screenshot(None)),
},
}) })
} }
@@ -74,16 +84,14 @@ pub async fn rights() -> HttpResult {
res.vms.push(VMInfoAndCaps { res.vms.push(VMInfoAndCaps {
info: vm_info, info: vm_info,
caps: VMCaps { can_get_state: rights.is_route_allowed("GET", &v.route_state()),
can_get_state: rights.is_route_allowed("GET", &v.route_state()), can_start: rights.is_route_allowed("GET", &v.route_start()),
can_start: rights.is_route_allowed("GET", &v.route_start()), can_shutdown: rights.is_route_allowed("GET", &v.route_shutdown()),
can_shutdown: rights.is_route_allowed("GET", &v.route_shutdown()), can_kill: rights.is_route_allowed("GET", &v.route_kill()),
can_kill: rights.is_route_allowed("GET", &v.route_kill()), can_reset: rights.is_route_allowed("GET", &v.route_reset()),
can_reset: rights.is_route_allowed("GET", &v.route_reset()), can_suspend: rights.is_route_allowed("GET", &v.route_suspend()),
can_suspend: rights.is_route_allowed("GET", &v.route_suspend()), can_resume: rights.is_route_allowed("GET", &v.route_resume()),
can_resume: rights.is_route_allowed("GET", &v.route_resume()), can_screenshot: rights.is_route_allowed("GET", &v.route_screenshot()),
can_screenshot: rights.is_route_allowed("GET", &v.route_screenshot()),
},
}) })
} }

View File

@@ -18,7 +18,7 @@ mod serve_static_debug {
#[cfg(not(debug_assertions))] #[cfg(not(debug_assertions))]
mod serve_static_release { mod serve_static_release {
use actix_web::{HttpResponse, Responder, web}; use actix_web::{web, HttpResponse, Responder};
use rust_embed::RustEmbed; use rust_embed::RustEmbed;
#[derive(RustEmbed)] #[derive(RustEmbed)]

View File

@@ -3,7 +3,7 @@
use crate::controllers::HttpResult; use crate::controllers::HttpResult;
use crate::virtweb_client; use crate::virtweb_client;
use crate::virtweb_client::VMUuid; use crate::virtweb_client::VMUuid;
use actix_web::{HttpResponse, web}; use actix_web::{web, HttpResponse};
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
pub struct ReqPath { pub struct ReqPath {

View File

@@ -1,7 +1,7 @@
use actix_identity::Identity; use actix_identity::Identity;
use actix_web::dev::Payload; use actix_web::dev::Payload;
use actix_web::{Error, FromRequest, HttpMessage, HttpRequest}; use actix_web::{Error, FromRequest, HttpMessage, HttpRequest};
use futures_util::future::{Ready, ready}; use futures_util::future::{ready, Ready};
use std::fmt::Display; use std::fmt::Display;
pub struct AuthExtractor { pub struct AuthExtractor {

View File

@@ -1,19 +1,18 @@
use actix_cors::Cors; use actix_cors::Cors;
use actix_identity::config::LogoutBehaviour;
use actix_identity::IdentityMiddleware; use actix_identity::IdentityMiddleware;
use actix_identity::config::LogoutBehavior;
use actix_remote_ip::RemoteIPConfig; use actix_remote_ip::RemoteIPConfig;
use actix_session::SessionMiddleware;
use actix_session::storage::CookieSessionStore; use actix_session::storage::CookieSessionStore;
use actix_session::SessionMiddleware;
use actix_web::cookie::{Key, SameSite}; use actix_web::cookie::{Key, SameSite};
use actix_web::middleware::Logger; use actix_web::middleware::Logger;
use actix_web::web::Data; use actix_web::web::Data;
use actix_web::{App, HttpServer, web}; use actix_web::{web, App, HttpServer};
use light_openid::basic_state_manager::BasicStateManager; use light_openid::basic_state_manager::BasicStateManager;
use remote_backend::app_config::AppConfig; use remote_backend::app_config::AppConfig;
use remote_backend::constants; use remote_backend::constants;
use remote_backend::controllers::{ use remote_backend::controllers::{
auth_controller, group_controller, server_controller, static_controller, sys_info_controller, auth_controller, server_controller, static_controller, sys_info_controller, vm_controller,
vm_controller,
}; };
use remote_backend::middlewares::auth_middleware::AuthChecker; use remote_backend::middlewares::auth_middleware::AuthChecker;
use std::time::Duration; use std::time::Duration;
@@ -37,7 +36,7 @@ async fn main() -> std::io::Result<()> {
.build(); .build();
let identity_middleware = IdentityMiddleware::builder() let identity_middleware = IdentityMiddleware::builder()
.logout_behavior(LogoutBehavior::PurgeSession) .logout_behaviour(LogoutBehaviour::PurgeSession)
.visit_deadline(Some(Duration::from_secs( .visit_deadline(Some(Duration::from_secs(
constants::MAX_INACTIVITY_DURATION, constants::MAX_INACTIVITY_DURATION,
))) )))
@@ -87,39 +86,6 @@ async fn main() -> std::io::Result<()> {
"/api/server/rights", "/api/server/rights",
web::get().to(server_controller::rights), web::get().to(server_controller::rights),
) )
// Groups routes
.route(
"/api/group/{gid}/vm/state",
web::get().to(group_controller::vm_state),
)
.route(
"/api/group/{gid}/vm/start",
web::get().to(group_controller::vm_start),
)
.route(
"/api/group/{gid}/vm/shutdown",
web::get().to(group_controller::vm_shutdown),
)
.route(
"/api/group/{gid}/vm/kill",
web::get().to(group_controller::vm_kill),
)
.route(
"/api/group/{gid}/vm/reset",
web::get().to(group_controller::vm_reset),
)
.route(
"/api/group/{gid}/vm/suspend",
web::get().to(group_controller::vm_suspend),
)
.route(
"/api/group/{gid}/vm/resume",
web::get().to(group_controller::vm_resume),
)
.route(
"/api/group/{gid}/vm/screenshot",
web::get().to(group_controller::vm_screenshot),
)
// VM routes // VM routes
.route("/api/vm/{uid}/state", web::get().to(vm_controller::state)) .route("/api/vm/{uid}/state", web::get().to(vm_controller::state))
.route("/api/vm/{uid}/start", web::get().to(vm_controller::start)) .route("/api/vm/{uid}/start", web::get().to(vm_controller::start))

View File

@@ -1,4 +1,4 @@
use std::future::{Ready, ready}; use std::future::{ready, Ready};
use std::rc::Rc; use std::rc::Rc;
use crate::app_config::AppConfig; use crate::app_config::AppConfig;
@@ -7,8 +7,8 @@ use crate::extractors::auth_extractor::AuthExtractor;
use actix_web::body::EitherBody; use actix_web::body::EitherBody;
use actix_web::dev::Payload; use actix_web::dev::Payload;
use actix_web::{ use actix_web::{
dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
Error, FromRequest, HttpResponse, Error, FromRequest, HttpResponse,
dev::{Service, ServiceRequest, ServiceResponse, Transform, forward_ready},
}; };
use futures_util::future::LocalBoxFuture; use futures_util::future::LocalBoxFuture;

View File

@@ -1,7 +1,6 @@
use crate::app_config::AppConfig; use crate::app_config::AppConfig;
use crate::utils::time; use crate::utils::time;
use lazy_regex::regex; use lazy_regex::regex;
use std::collections::HashMap;
use std::fmt::Display; use std::fmt::Display;
use std::str::FromStr; use std::str::FromStr;
use thiserror::Error; use thiserror::Error;
@@ -103,15 +102,9 @@ impl GroupID {
} }
} }
#[derive(Eq, PartialEq, Debug, Copy, Clone, serde::Serialize, serde::Deserialize, Hash)] #[derive(Eq, PartialEq, Debug, Copy, Clone, serde::Serialize, serde::Deserialize)]
pub struct VMUuid(Uuid); pub struct VMUuid(Uuid);
#[derive(Default, serde::Deserialize, serde::Serialize)]
pub struct TreatmentResult {
ok: usize,
failed: usize,
}
impl VMUuid { impl VMUuid {
pub fn route_info(&self) -> String { pub fn route_info(&self) -> String {
format!("/api/vm/{}", self.0) format!("/api/vm/{}", self.0)
@@ -176,18 +169,6 @@ pub struct VMInfo {
pub number_vcpu: usize, pub number_vcpu: usize,
} }
#[derive(serde::Deserialize, serde::Serialize, Debug)]
pub struct VMCaps {
pub can_get_state: bool,
pub can_start: bool,
pub can_shutdown: bool,
pub can_kill: bool,
pub can_reset: bool,
pub can_suspend: bool,
pub can_resume: bool,
pub can_screenshot: bool,
}
#[derive(serde::Deserialize, serde::Serialize, Debug)] #[derive(serde::Deserialize, serde::Serialize, Debug)]
pub struct VMState { pub struct VMState {
pub state: String, pub state: String,
@@ -287,13 +268,12 @@ async fn request<D: Display>(uri: D) -> anyhow::Result<reqwest::Response> {
let url = format!("{}{}", AppConfig::get().virtweb_base_url, uri); let url = format!("{}{}", AppConfig::get().virtweb_base_url, uri);
log::debug!("Will query {uri}..."); log::debug!("Will query {uri}...");
let uri = uri.to_string();
let jwt = TokenClaims { let jwt = TokenClaims {
sub: AppConfig::get().virtweb_token_id.to_string(), sub: AppConfig::get().virtweb_token_id.to_string(),
iat: time() - 60 * 2, iat: time() - 60 * 2,
exp: time() + 60 * 3, exp: time() + 60 * 3,
verb: "GET".to_string(), verb: "GET".to_string(),
path: uri.split_once('?').map(|s| s.0).unwrap_or(&uri).to_string(), path: uri.to_string(),
nonce: Uuid::new_v4().to_string(), nonce: Uuid::new_v4().to_string(),
}; };
let jwt = AppConfig::get().token_private_key().sign_jwt(&jwt)?; let jwt = AppConfig::get().token_private_key().sign_jwt(&jwt)?;
@@ -385,68 +365,6 @@ pub async fn group_vm_info(id: &GroupID) -> anyhow::Result<Vec<VMInfo>> {
json_request(id.route_vm_info()).await json_request(id.route_vm_info()).await
} }
/// Get the state of one or all VMs of a group
pub async fn group_vm_state(
id: &GroupID,
vm_id: Option<VMUuid>,
) -> anyhow::Result<HashMap<VMUuid, String>> {
json_request(id.route_vm_state(vm_id)).await
}
/// Start one or all VMs of a group
pub async fn group_vm_start(
id: &GroupID,
vm_id: Option<VMUuid>,
) -> anyhow::Result<TreatmentResult> {
json_request(id.route_vm_start(vm_id)).await
}
/// Shutdown one or all VMs of a group
pub async fn group_vm_shutdown(
id: &GroupID,
vm_id: Option<VMUuid>,
) -> anyhow::Result<TreatmentResult> {
json_request(id.route_vm_shutdown(vm_id)).await
}
/// Kill one or all VMs of a group
pub async fn group_vm_kill(id: &GroupID, vm_id: Option<VMUuid>) -> anyhow::Result<TreatmentResult> {
json_request(id.route_vm_kill(vm_id)).await
}
/// Reset one or all VMs of a group
pub async fn group_vm_reset(
id: &GroupID,
vm_id: Option<VMUuid>,
) -> anyhow::Result<TreatmentResult> {
json_request(id.route_vm_reset(vm_id)).await
}
/// Suspend one or all VMs of a group
pub async fn group_vm_suspend(
id: &GroupID,
vm_id: Option<VMUuid>,
) -> anyhow::Result<TreatmentResult> {
json_request(id.route_vm_suspend(vm_id)).await
}
/// Resume one or all VMs of a group
pub async fn group_vm_resume(
id: &GroupID,
vm_id: Option<VMUuid>,
) -> anyhow::Result<TreatmentResult> {
json_request(id.route_vm_resume(vm_id)).await
}
/// Get the screenshot of one or all VMs of a group
pub async fn group_vm_screenshot(id: &GroupID, vm_id: Option<VMUuid>) -> anyhow::Result<Vec<u8>> {
Ok(request(id.route_vm_screenshot(vm_id))
.await?
.bytes()
.await?
.to_vec())
}
/// Get current server information /// Get current server information
pub async fn get_server_info() -> anyhow::Result<SystemInfo> { pub async fn get_server_info() -> anyhow::Result<SystemInfo> {
json_request("/api/server/info").await json_request("/api/server/info").await

View File

@@ -0,0 +1,18 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}

View File

@@ -1,28 +0,0 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
)

File diff suppressed because it is too large Load Diff

View File

@@ -10,25 +10,22 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@fluentui/react-components": "^9.72.7", "@fluentui/react-components": "^9.56.3",
"@fluentui/react-icons": "^2.0.315", "@fluentui/react-icons": "^2.0.266",
"filesize": "^11.0.13", "filesize": "^10.1.6",
"react": "^19.2.0", "react": "^18.2.0",
"react-dom": "^19.2.0" "react-dom": "^18.2.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.1", "@types/react": "^18.3.12",
"@types/react": "^19.2.7", "@types/react-dom": "^18.3.1",
"@types/react-dom": "^19.2.3", "@typescript-eslint/eslint-plugin": "^8.16.0",
"@typescript-eslint/eslint-plugin": "^8.48.0", "@typescript-eslint/parser": "^8.16.0",
"@typescript-eslint/parser": "^8.48.0", "@vitejs/plugin-react": "^4.3.4",
"@vitejs/plugin-react": "^5.1.1", "eslint": "^8.57.0",
"eslint": "^9.39.1", "eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.14",
"eslint-plugin-react-refresh": "^0.4.24", "typescript": "^5.7.2",
"globals": "^16.5.0", "vite": "^6.0.1"
"typescript": "^5.9.3",
"typescript-eslint": "^8.48.0",
"vite": "^7.2.4"
} }
} }

View File

@@ -5,8 +5,6 @@ import {
typographyStyles, typographyStyles,
} from "@fluentui/react-components"; } from "@fluentui/react-components";
import { import {
AppsListDetailFilled,
AppsListDetailRegular,
DesktopFilled, DesktopFilled,
DesktopRegular, DesktopRegular,
InfoFilled, InfoFilled,
@@ -20,7 +18,6 @@ import { AsyncWidget } from "./widgets/AsyncWidget";
import { MainMenu } from "./widgets/MainMenu"; import { MainMenu } from "./widgets/MainMenu";
import { SystemInfoWidget } from "./widgets/SystemInfoWidget"; import { SystemInfoWidget } from "./widgets/SystemInfoWidget";
import { VirtualMachinesWidget } from "./widgets/VirtualMachinesWidget"; import { VirtualMachinesWidget } from "./widgets/VirtualMachinesWidget";
import { GroupsWidget } from "./widgets/GroupsWidget";
const useStyles = makeStyles({ const useStyles = makeStyles({
title: typographyStyles.title2, title: typographyStyles.title2,
@@ -30,8 +27,6 @@ const InfoIcon = bundleIcon(InfoFilled, InfoRegular);
const DesktopIcon = bundleIcon(DesktopFilled, DesktopRegular); const DesktopIcon = bundleIcon(DesktopFilled, DesktopRegular);
const AppListIcon = bundleIcon(AppsListDetailFilled, AppsListDetailRegular);
export function App() { export function App() {
return ( return (
<AsyncWidget <AsyncWidget
@@ -53,17 +48,12 @@ function AppInner(): React.ReactElement {
function AuthenticatedApp(): React.ReactElement { function AuthenticatedApp(): React.ReactElement {
const styles = useStyles(); const styles = useStyles();
const [tab, setTab] = React.useState<"group" | "vm" | "info">("group"); const [tab, setTab] = React.useState<"vm" | "info">("vm");
const [rights, setRights] = React.useState<Rights | undefined>(); const [rights, setRights] = React.useState<Rights | undefined>();
const load = async () => { const load = async () => {
const rights = await ServerApi.GetRights(); setRights(await ServerApi.GetRights());
setRights(rights);
if (rights!.groups.length > 0) setTab("group");
else if (rights!.vms.length > 0) setTab("vm");
else setTab("info");
}; };
return ( return (
@@ -92,27 +82,25 @@ function AuthenticatedApp(): React.ReactElement {
selectedValue={tab} selectedValue={tab}
onTabSelect={(_, d) => setTab(d.value as any)} onTabSelect={(_, d) => setTab(d.value as any)}
> >
{rights!.groups.length > 0 && ( <Tab
<Tab value="group" icon={<AppListIcon />}> value="vm"
Groups icon={<DesktopIcon />}
</Tab> disabled={rights!.vms.length === 0}
)} >
{rights!.vms.length > 0 && ( Virtual machines
<Tab value="vm" icon={<DesktopIcon />}> </Tab>
Virtual machines <Tab
</Tab> value="info"
)} icon={<InfoIcon />}
{rights!.sys_info && ( disabled={!rights!.sys_info}
<Tab value="info" icon={<InfoIcon />}> >
System info System info
</Tab> </Tab>
)}
</TabList> </TabList>
<div> <div>
<MainMenu /> <MainMenu />
</div> </div>
</div> </div>
{tab === "group" && <GroupsWidget rights={rights!} />}
{tab === "vm" && <VirtualMachinesWidget rights={rights!} />} {tab === "vm" && <VirtualMachinesWidget rights={rights!} />}
{tab === "info" && <SystemInfoWidget />} {tab === "info" && <SystemInfoWidget />}
</div> </div>

View File

@@ -1,107 +0,0 @@
import { APIClient } from "./ApiClient";
import { VMGroup } from "./ServerApi";
import { VMInfo, VMState } from "./VMApi";
export interface GroupVMState {
[key: string]: VMState;
}
export interface TreatmentResult {
ok: number;
failed: number;
}
export class GroupApi {
/**
* Get the state of the VMs of a group
*/
static async State(g: VMGroup): Promise<GroupVMState> {
return (
await APIClient.exec({ method: "GET", uri: `/group/${g.id}/vm/state` })
).data;
}
/**
* Request to start the VM of a group
*/
static async StartVM(g: VMGroup, vm?: VMInfo): Promise<TreatmentResult> {
return (
await APIClient.exec({
method: "GET",
uri: `/group/${g.id}/vm/start` + (vm ? `?vm_id=${vm.uuid}` : ""),
})
).data;
}
/**
* Request to suspend the VM of a group
*/
static async SuspendVM(g: VMGroup, vm?: VMInfo): Promise<TreatmentResult> {
return (
await APIClient.exec({
method: "GET",
uri: `/group/${g.id}/vm/suspend` + (vm ? `?vm_id=${vm.uuid}` : ""),
})
).data;
}
/**
* Request to resume the VM of a group
*/
static async ResumeVM(g: VMGroup, vm?: VMInfo): Promise<TreatmentResult> {
return (
await APIClient.exec({
method: "GET",
uri: `/group/${g.id}/vm/resume` + (vm ? `?vm_id=${vm.uuid}` : ""),
})
).data;
}
/**
* Request to shutdown the VM of a group
*/
static async ShutdownVM(g: VMGroup, vm?: VMInfo): Promise<TreatmentResult> {
return (
await APIClient.exec({
method: "GET",
uri: `/group/${g.id}/vm/shutdown` + (vm ? `?vm_id=${vm.uuid}` : ""),
})
).data;
}
/**
* Request to kill the VM of a group
*/
static async KillVM(g: VMGroup, vm?: VMInfo): Promise<TreatmentResult> {
return (
await APIClient.exec({
method: "GET",
uri: `/group/${g.id}/vm/kill` + (vm ? `?vm_id=${vm.uuid}` : ""),
})
).data;
}
/**
* Request to reset the VM of a group
*/
static async ResetVM(g: VMGroup, vm?: VMInfo): Promise<TreatmentResult> {
return (
await APIClient.exec({
method: "GET",
uri: `/group/${g.id}/vm/reset` + (vm ? `?vm_id=${vm.uuid}` : ""),
})
).data;
}
/**
* Request a screenshot of the VM of group
*/
static async ScreenshotVM(g: VMGroup, vm?: VMInfo): Promise<Blob> {
return (
await APIClient.exec({
method: "GET",
uri: `/group/${g.id}/vm/screenshot` + (vm ? `?vm_id=${vm.uuid}` : ""),
})
).data;
}
}

View File

@@ -1,5 +1,5 @@
import { APIClient } from "./ApiClient"; import { APIClient } from "./ApiClient";
import { VMCaps, VMInfo, VMInfoAndCaps } from "./VMApi"; import { VMInfo } from "./VMApi";
export interface ServerConfig { export interface ServerConfig {
authenticated: boolean; authenticated: boolean;
@@ -7,16 +7,8 @@ export interface ServerConfig {
} }
export interface Rights { export interface Rights {
groups: VMGroup[];
vms: VMInfoAndCaps[];
sys_info: boolean;
}
export type VMGroup = VMGroupInfo & VMCaps;
export interface VMGroupInfo {
id: string;
vms: VMInfo[]; vms: VMInfo[];
sys_info: boolean;
} }
let config: ServerConfig | null = null; let config: ServerConfig | null = null;

View File

@@ -7,9 +7,6 @@ export interface VMInfo {
architecture: string; architecture: string;
memory: number; memory: number;
number_vcpu: number; number_vcpu: number;
}
export interface VMCaps {
can_get_state: boolean; can_get_state: boolean;
can_start: boolean; can_start: boolean;
can_shutdown: boolean; can_shutdown: boolean;
@@ -20,8 +17,6 @@ export interface VMCaps {
can_screenshot: boolean; can_screenshot: boolean;
} }
export type VMInfoAndCaps = VMInfo & VMCaps;
export type VMState = export type VMState =
| "NoState" | "NoState"
| "Running" | "Running"

View File

@@ -20,7 +20,7 @@ type ThemeContext = { theme: Theme; set: (theme: Theme) => void };
const ThemeContextK = React.createContext<ThemeContext | null>(null); const ThemeContextK = React.createContext<ThemeContext | null>(null);
export function ThemeProvider(p: React.PropsWithChildren): React.ReactElement { export function ThemeProvider(p: React.PropsWithChildren): React.ReactElement {
const [theme, setTheme] = React.useState<Theme>("teamsdark"); const [theme, setTheme] = React.useState<Theme>("highcontrast");
let fluentTheme = teamsHighContrastTheme; let fluentTheme = teamsHighContrastTheme;
switch (theme) { switch (theme) {

View File

@@ -1,177 +0,0 @@
import { Button, Spinner, Toolbar, Tooltip } from "@fluentui/react-components";
import {
ArrowResetRegular,
PauseRegular,
PlayCircleRegular,
PlayFilled,
PowerRegular,
StopRegular,
} from "@fluentui/react-icons";
import React from "react";
import { GroupApi, TreatmentResult } from "../api/GroupApi";
import { VMGroup } from "../api/ServerApi";
import { VMInfo, VMState } from "../api/VMApi";
import { useAlert } from "../hooks/providers/AlertDialogProvider";
import { useConfirm } from "../hooks/providers/ConfirmDialogProvider";
import { useToast } from "../hooks/providers/ToastProvider";
export function GroupVMAction(p: {
group: VMGroup;
state?: VMState;
vm?: VMInfo;
}): React.ReactElement {
return (
<Toolbar>
<GroupVMButton
enabled={p.group.can_start}
icon={<PlayFilled />}
tooltip="Start"
group={p.group}
vm={p.vm}
allowedStates={["Shutdown", "Shutoff", "Crashed"]}
currState={p.state}
needConfirm={false}
action={GroupApi.StartVM}
/>
<GroupVMButton
enabled={p.group.can_suspend}
icon={<PauseRegular />}
tooltip="Suspend"
group={p.group}
vm={p.vm}
allowedStates={["Running"]}
currState={p.state}
needConfirm={true}
action={GroupApi.SuspendVM}
/>
<GroupVMButton
enabled={p.group.can_resume}
icon={<PlayCircleRegular />}
tooltip="Resume"
group={p.group}
vm={p.vm}
allowedStates={["Paused", "PowerManagementSuspended"]}
currState={p.state}
needConfirm={false}
action={GroupApi.ResumeVM}
/>
<GroupVMButton
enabled={p.group.can_shutdown}
icon={<PowerRegular />}
tooltip="Shutdown"
group={p.group}
vm={p.vm}
allowedStates={["Running"]}
currState={p.state}
needConfirm={true}
action={GroupApi.ShutdownVM}
/>
<GroupVMButton
enabled={p.group.can_kill}
icon={<StopRegular />}
tooltip="Kill"
group={p.group}
vm={p.vm}
allowedStates={[
"Running",
"Paused",
"PowerManagementSuspended",
"Blocked",
]}
currState={p.state}
needConfirm={true}
action={GroupApi.KillVM}
/>
<GroupVMButton
enabled={p.group.can_reset}
icon={<ArrowResetRegular />}
tooltip="Reset"
group={p.group}
vm={p.vm}
allowedStates={[
"Running",
"Paused",
"PowerManagementSuspended",
"Blocked",
]}
currState={p.state}
needConfirm={true}
action={GroupApi.ResetVM}
/>
</Toolbar>
);
}
function GroupVMButton(p: {
enabled: boolean;
icon: React.ReactElement;
action: (group: VMGroup, vm?: VMInfo) => Promise<TreatmentResult>;
tooltip: string;
currState?: VMState;
allowedStates: VMState[];
group: VMGroup;
vm?: VMInfo;
needConfirm: boolean;
}): React.ReactElement {
const toast = useToast();
const confirm = useConfirm();
const alert = useAlert();
const [running, setRunning] = React.useState(false);
const target = p.vm
? `the VM ${p.vm.name}`
: `all the VM of the group ${p.group.id}`;
const allowed =
!p.vm || (p.currState && p.allowedStates.includes(p.currState));
const perform = async () => {
if (running || !allowed) return;
try {
if (
(!p.vm || p.needConfirm) &&
!(await confirm(
`Do you want to perform ${p.tooltip} action on ${target}?`,
`Confirmation`,
p.tooltip
))
) {
return;
}
setRunning(true);
const result = await p.action(p.group, p.vm);
toast(
p.tooltip,
`${p.tooltip} action on ${target}: ${result.ok} OK / ${result.failed} Failed`,
"success"
);
} catch (e) {
console.error("Failed to perform group action!", e);
alert(`Failed to perform ${p.tooltip} action on ${target}: ${e}`);
} finally {
setRunning(false);
}
};
if (!p.enabled) return <></>;
return (
<Tooltip
content={`${p.tooltip} ${target}`}
relationship="description"
withArrow
>
<Button
icon={running ? <Spinner size="tiny" /> : p.icon}
onClick={allowed ? perform : undefined}
disabled={!allowed}
appearance="subtle"
/>
</Tooltip>
);
}

View File

@@ -1,171 +0,0 @@
import {
Button,
Card,
Dialog,
DialogActions,
DialogBody,
DialogContent,
DialogSurface,
DialogTitle,
DialogTrigger,
Table,
TableBody,
TableCell,
TableCellActions,
TableCellLayout,
TableHeader,
TableHeaderCell,
TableRow,
Title3,
Tooltip,
} from "@fluentui/react-components";
import { Desktop24Regular, ScreenshotRegular } from "@fluentui/react-icons";
import { filesize } from "filesize";
import React from "react";
import { GroupApi, GroupVMState } from "../api/GroupApi";
import { Rights, VMGroup } from "../api/ServerApi";
import { VMInfo } from "../api/VMApi";
import { useToast } from "../hooks/providers/ToastProvider";
import { GroupVMAction } from "./GroupVMAction";
import { VMLiveScreenshot } from "./VMLiveScreenshot";
export function GroupsWidget(p: { rights: Rights }): React.ReactElement {
return (
<>
{p.rights.groups.map((g) => (
<GroupInfo group={g} />
))}
</>
);
}
function GroupInfo(p: { group: VMGroup }): React.ReactElement {
const toast = useToast();
const [state, setState] = React.useState<GroupVMState | undefined>();
const [screenshotVM, setScreenshotVM] = React.useState<VMInfo | undefined>();
const load = async () => {
const newState = await GroupApi.State(p.group);
if (state !== newState) setState(newState);
};
const screenshot = (vm: VMInfo) => {
setScreenshotVM(vm);
};
React.useEffect(() => {
const interval = setInterval(async () => {
try {
if (p.group.can_get_state) await load();
} catch (e) {
console.error(e);
toast(
"Error",
`Failed to refresh group ${p.group.id} VMs status!`,
"error"
);
}
}, 1000);
return () => clearInterval(interval);
});
return (
<>
<Card
style={{
margin: "50px 10px",
display: "flex",
flexDirection: "column",
}}
>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<Title3 style={{ marginLeft: "10px" }}>{p.group.id}</Title3>
<GroupVMAction group={p.group} />
</div>
<Table sortable>
<TableHeader>
<TableRow>
<TableHeaderCell>VM</TableHeaderCell>
<TableHeaderCell>Resources</TableHeaderCell>
<TableHeaderCell>State</TableHeaderCell>
<TableHeaderCell>Actions</TableHeaderCell>
</TableRow>
</TableHeader>
<TableBody>
{p.group.vms.map((item) => (
<TableRow key={item.uuid}>
<TableCell>
<TableCellLayout
media={<Desktop24Regular />}
appearance="primary"
description={item.description}
>
{item.name}
</TableCellLayout>
<TableCellActions>
{state?.[item.uuid] === "Running" && (
<Tooltip
relationship="description"
content={"Take a screenshot of the VM screen"}
withArrow
>
<Button
icon={<ScreenshotRegular />}
appearance="subtle"
aria-label="Edit"
disabled={!p.group.can_screenshot}
onClick={() => screenshot(item)}
/>
</Tooltip>
)}
</TableCellActions>
</TableCell>
<TableCell>
{item.architecture} &bull; RAM :{" "}
{filesize(item.memory)} &bull;{" "}
{item.number_vcpu} vCPU
</TableCell>
<TableCell>{state?.[item.uuid] ?? ""}</TableCell>
<TableCell>
<GroupVMAction
group={p.group}
state={state?.[item.uuid]}
vm={item}
/>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Card>
<Dialog
open={!!screenshotVM}
onOpenChange={(_event, _data) => {
if (!screenshotVM) setScreenshotVM(undefined);
}}
>
<DialogSurface>
<DialogBody>
<DialogTitle>
<em>{screenshotVM?.name}</em> screen
</DialogTitle>
<DialogContent>
<VMLiveScreenshot vm={screenshotVM!} group={p.group} />
</DialogContent>
<DialogActions>
<DialogTrigger disableButtonEnhancement>
<Button
appearance="secondary"
onClick={() => setScreenshotVM(undefined)}
>
Close
</Button>
</DialogTrigger>
</DialogActions>
</DialogBody>
</DialogSurface>
</Dialog>
</>
);
}

View File

@@ -1,13 +1,8 @@
import React from "react"; import React from "react";
import { GroupApi } from "../api/GroupApi";
import { VMGroup } from "../api/ServerApi";
import { VMApi, VMInfo } from "../api/VMApi"; import { VMApi, VMInfo } from "../api/VMApi";
import { useToast } from "../hooks/providers/ToastProvider"; import { useToast } from "../hooks/providers/ToastProvider";
export function VMLiveScreenshot(p: { export function VMLiveScreenshot(p: { vm: VMInfo }): React.ReactElement {
vm: VMInfo;
group?: VMGroup;
}): React.ReactElement {
const toast = useToast(); const toast = useToast();
const [screenshotURL, setScreenshotURL] = React.useState< const [screenshotURL, setScreenshotURL] = React.useState<
@@ -19,9 +14,7 @@ export function VMLiveScreenshot(p: {
React.useEffect(() => { React.useEffect(() => {
const refresh = async () => { const refresh = async () => {
try { try {
const screenshot = p.group const screenshot = await VMApi.Screenshot(p.vm);
? await GroupApi.ScreenshotVM(p.group, p.vm)
: await VMApi.Screenshot(p.vm);
const u = URL.createObjectURL(screenshot); const u = URL.createObjectURL(screenshot);
setScreenshotURL(u); setScreenshotURL(u);
} catch (e) { } catch (e) {

View File

@@ -22,11 +22,11 @@ import {
import { filesize } from "filesize"; import { filesize } from "filesize";
import React from "react"; import React from "react";
import { Rights } from "../api/ServerApi"; import { Rights } from "../api/ServerApi";
import { VMApi, VMInfo, VMInfoAndCaps, VMState } from "../api/VMApi"; import { VMApi, VMInfo, VMState } from "../api/VMApi";
import { useConfirm } from "../hooks/providers/ConfirmDialogProvider"; import { useConfirm } from "../hooks/providers/ConfirmDialogProvider";
import { useToast } from "../hooks/providers/ToastProvider"; import { useToast } from "../hooks/providers/ToastProvider";
import { SectionContainer } from "./SectionContainer";
import { VMLiveScreenshot } from "./VMLiveScreenshot"; import { VMLiveScreenshot } from "./VMLiveScreenshot";
import { SectionContainer } from "./SectionContainer";
const useStyles = makeStyles({ const useStyles = makeStyles({
body1Stronger: typographyStyles.body1Stronger, body1Stronger: typographyStyles.body1Stronger,
@@ -54,7 +54,7 @@ export function VirtualMachinesWidget(p: {
); );
} }
function VMWidget(p: { vm: VMInfoAndCaps }): React.ReactElement { function VMWidget(p: { vm: VMInfo }): React.ReactElement {
const toast = useToast(); const toast = useToast();
const [state, setState] = React.useState<VMState | undefined>(); const [state, setState] = React.useState<VMState | undefined>();
@@ -107,7 +107,7 @@ function VMWidget(p: { vm: VMInfoAndCaps }): React.ReactElement {
} }
/> />
<p className={styles.caption1} style={{ margin: "0px auto" }}> <p className={styles.caption1} style={{ margin: "0px auto" }}>
{p.vm.architecture} &bull; RAM : {filesize(p.vm.memory)}{" "} {p.vm.architecture} &bull; RAM : {filesize(p.vm.memory * 1000 * 1000)}{" "}
&bull; {p.vm.number_vcpu} vCPU &bull; {p.vm.number_vcpu} vCPU
</p> </p>
@@ -189,10 +189,7 @@ function VMWidget(p: { vm: VMInfoAndCaps }): React.ReactElement {
); );
} }
function VMPreview(p: { function VMPreview(p: { vm: VMInfo; state?: VMState }): React.ReactElement {
vm: VMInfoAndCaps;
state?: VMState;
}): React.ReactElement {
const styles = useStyles(); const styles = useStyles();
if (!p.vm.can_screenshot || p.state !== "Running") { if (!p.vm.can_screenshot || p.state !== "Running") {
return ( return (

View File

@@ -1,26 +0,0 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}

View File

@@ -1,7 +1,25 @@
{ {
"files": [], "compilerOptions": {
"references": [ "target": "ES2020",
{ "path": "./tsconfig.app.json" }, "useDefineForClassFields": true,
{ "path": "./tsconfig.node.json" } "lib": ["ES2020", "DOM", "DOM.Iterable"],
] "module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
} }

View File

@@ -1,24 +1,11 @@
{ {
"compilerOptions": { "compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", "composite": true,
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true, "skipLibCheck": true,
"module": "ESNext",
/* Bundler mode */
"moduleResolution": "bundler", "moduleResolution": "bundler",
"allowImportingTsExtensions": true, "allowSyntheticDefaultImports": true,
"isolatedModules": true, "strict": true
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
}, },
"include": ["vite.config.ts"] "include": ["vite.config.ts"]
} }

View File

@@ -1,7 +1,7 @@
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
// https://vite.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
}) })

View File

@@ -1,3 +1,9 @@
{ {
"extends": ["local>renovate/presets"] "$schema": "https://docs.renovatebot.com/renovate-schema.json",
"packageRules": [
{
"matchUpdateTypes": ["major", "minor", "patch"],
"automerge": true
}
]
} }