Compare commits
1 Commits
master
...
87752bd32a
| Author | SHA1 | Date | |
|---|---|---|---|
| 87752bd32a |
@@ -5,13 +5,13 @@ name: default
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: web_build
|
- name: web_build
|
||||||
image: node:24
|
image: node:23
|
||||||
volumes:
|
volumes:
|
||||||
- name: web_app
|
- name: web_app
|
||||||
path: /tmp/web_build
|
path: /tmp/web_build
|
||||||
commands:
|
commands:
|
||||||
- cd virtweb_frontend
|
- cd virtweb_frontend
|
||||||
- npm install --legacy-peer-deps # TODO remove --legacy-peer-deps ASAP
|
- npm install
|
||||||
- npm run lint
|
- npm run lint
|
||||||
- npm run build
|
- npm run build
|
||||||
- mv dist /tmp/web_build
|
- mv dist /tmp/web_build
|
||||||
|
|||||||
2237
virtweb_backend/Cargo.lock
generated
2237
virtweb_backend/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -7,43 +7,43 @@ edition = "2024"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
log = "0.4.29"
|
log = "0.4.29"
|
||||||
env_logger = "0.11.10"
|
env_logger = "0.11.8"
|
||||||
clap = { version = "4.6.0", features = ["derive", "env"] }
|
clap = { version = "4.5.54", features = ["derive", "env"] }
|
||||||
light-openid = { version = "1.1.0", features = ["crypto-wrapper"] }
|
light-openid = { version = "1.0.4", features = ["crypto-wrapper"] }
|
||||||
lazy_static = "1.5.0"
|
lazy_static = "1.5.0"
|
||||||
actix = "0.13.5"
|
actix = "0.13.5"
|
||||||
actix-web = "4.13.0"
|
actix-web = "4.12.1"
|
||||||
actix-remote-ip = "1.0.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.1"
|
||||||
actix-files = "0.6.10"
|
actix-files = "0.6.9"
|
||||||
actix-ws = "0.4.0"
|
actix-ws = "0.3.1"
|
||||||
actix-http = "3.12.0"
|
actix-http = "3.11.2"
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
serde_json = "1.0.149"
|
serde_json = "1.0.149"
|
||||||
serde_yml = "0.0.12"
|
serde_yml = "0.0.12"
|
||||||
quick-xml = { version = "0.39.2", features = ["serialize", "overlapped-lists"] }
|
quick-xml = { version = "0.38.4", features = ["serialize", "overlapped-lists"] }
|
||||||
futures-util = "0.3.32"
|
futures-util = "0.3.31"
|
||||||
anyhow = "1.0.102"
|
anyhow = "1.0.100"
|
||||||
actix-multipart = "0.7.2"
|
actix-multipart = "0.7.2"
|
||||||
tempfile = "3.27.0"
|
tempfile = "3.20.0"
|
||||||
reqwest = { version = "0.13.2", features = ["stream"] }
|
reqwest = { version = "0.12.28", features = ["stream"] }
|
||||||
url = "2.5.8"
|
url = "2.5.8"
|
||||||
virt = "0.4.3"
|
virt = "0.4.3"
|
||||||
sysinfo = { version = "0.38.4", features = ["serde"] }
|
sysinfo = { version = "0.36.1", features = ["serde"] }
|
||||||
uuid = { version = "1.22.0", features = ["v4", "serde"] }
|
uuid = { version = "1.17.0", features = ["v4", "serde"] }
|
||||||
lazy-regex = "3.6.0"
|
lazy-regex = "3.4.2"
|
||||||
thiserror = "2.0.18"
|
thiserror = "2.0.18"
|
||||||
image = "0.25.10"
|
image = "0.25.9"
|
||||||
rand = "0.10.0"
|
rand = "0.9.2"
|
||||||
tokio = { version = "1.50.0", features = ["rt", "time", "macros"] }
|
tokio = { version = "1.47.1", features = ["rt", "time", "macros"] }
|
||||||
futures = "0.3.32"
|
futures = "0.3.31"
|
||||||
ipnetwork = { version = "0.21.1", features = ["serde"] }
|
ipnetwork = { version = "0.21.1", features = ["serde"] }
|
||||||
num = "0.4.3"
|
num = "0.4.3"
|
||||||
rust-embed = { version = "8.11.0", features = ["mime-guess"] }
|
rust-embed = { version = "8.7.2", features = ["mime-guess"] }
|
||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
nix = { version = "0.31.2", features = ["net"] }
|
nix = { version = "0.30.1", features = ["net"] }
|
||||||
basic-jwt = "0.4.0"
|
basic-jwt = "0.3.0"
|
||||||
zip = "8.4.0"
|
zip = "4.3.0"
|
||||||
chrono = "0.4.44"
|
chrono = "0.4.43"
|
||||||
@@ -83,8 +83,7 @@ pub struct Token {
|
|||||||
pub pub_key: Option<JWTPublicKey>,
|
pub pub_key: Option<JWTPublicKey>,
|
||||||
pub rights: TokenRights,
|
pub rights: TokenRights,
|
||||||
pub last_used: u64,
|
pub last_used: u64,
|
||||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
pub ip_restriction: Option<ipnetwork::IpNetwork>,
|
||||||
pub allowed_ip_networks: Vec<ipnetwork::IpNetwork>,
|
|
||||||
pub max_inactivity: Option<u64>,
|
pub max_inactivity: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,8 +161,7 @@ pub struct NewToken {
|
|||||||
pub name: String,
|
pub name: String,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
pub rights: TokenRights,
|
pub rights: TokenRights,
|
||||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
pub ip_restriction: Option<ipnetwork::IpNetwork>,
|
||||||
pub allowed_ip_networks: Vec<ipnetwork::IpNetwork>,
|
|
||||||
pub max_inactivity: Option<u64>,
|
pub max_inactivity: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,7 +212,7 @@ pub async fn create(t: &NewToken) -> anyhow::Result<(Token, JWTPrivateKey)> {
|
|||||||
pub_key: Some(pub_key),
|
pub_key: Some(pub_key),
|
||||||
rights: t.rights.clone(),
|
rights: t.rights.clone(),
|
||||||
last_used: time(),
|
last_used: time(),
|
||||||
allowed_ip_networks: t.allowed_ip_networks.clone(),
|
ip_restriction: t.ip_restriction,
|
||||||
max_inactivity: t.max_inactivity,
|
max_inactivity: t.max_inactivity,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ use crate::libvirt_client::LibVirtClient;
|
|||||||
use actix_http::StatusCode;
|
use actix_http::StatusCode;
|
||||||
use actix_web::body::BoxBody;
|
use actix_web::body::BoxBody;
|
||||||
use actix_web::{HttpResponse, web};
|
use actix_web::{HttpResponse, web};
|
||||||
use light_openid::errors::OpenIdError;
|
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::fmt::{Display, Formatter};
|
use std::fmt::{Display, Formatter};
|
||||||
use zip::result::ZipError;
|
use zip::result::ZipError;
|
||||||
@@ -110,18 +109,11 @@ impl From<ZipError> for HttpErr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<OpenIdError> for HttpErr {
|
|
||||||
fn from(err: OpenIdError) -> HttpErr {
|
|
||||||
HttpErr::Err(std::io::Error::other(err.to_string()).into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<HttpResponse> for HttpErr {
|
impl From<HttpResponse> for HttpErr {
|
||||||
fn from(value: HttpResponse) -> Self {
|
fn from(value: HttpResponse) -> Self {
|
||||||
HttpErr::HTTPResponse(value)
|
HttpErr::HTTPResponse(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type HttpResult = Result<HttpResponse, HttpErr>;
|
pub type HttpResult = Result<HttpResponse, HttpErr>;
|
||||||
|
|
||||||
pub type LibVirtReq = web::Data<LibVirtClient>;
|
pub type LibVirtReq = web::Data<LibVirtClient>;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use actix_web::{Error, FromRequest, HttpRequest};
|
|||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
|
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
||||||
pub struct TokenClaims {
|
pub struct TokenClaims {
|
||||||
pub sub: String,
|
pub sub: String,
|
||||||
pub iat: usize,
|
pub iat: usize,
|
||||||
@@ -128,11 +128,8 @@ impl FromRequest for ApiAuthExtractor {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if !token.allowed_ip_networks.is_empty()
|
if let Some(ip) = token.ip_restriction
|
||||||
&& !token
|
&& !ip.contains(remote_ip.0)
|
||||||
.allowed_ip_networks
|
|
||||||
.iter()
|
|
||||||
.any(|n| n.contains(remote_ip.0))
|
|
||||||
{
|
{
|
||||||
log::error!(
|
log::error!(
|
||||||
"Attempt to use a token for an unauthorized IP! {remote_ip:?} token_id={}",
|
"Attempt to use a token for an unauthorized IP! {remote_ip:?} token_id={}",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use actix::Actor;
|
use actix::Actor;
|
||||||
use actix_cors::Cors;
|
use actix_cors::Cors;
|
||||||
use actix_identity::IdentityMiddleware;
|
use actix_identity::IdentityMiddleware;
|
||||||
use actix_identity::config::LogoutBehavior;
|
use actix_identity::config::LogoutBehaviour;
|
||||||
use actix_multipart::form::MultipartFormConfig;
|
use actix_multipart::form::MultipartFormConfig;
|
||||||
use actix_multipart::form::tempfile::TempFileConfig;
|
use actix_multipart::form::tempfile::TempFileConfig;
|
||||||
use actix_remote_ip::RemoteIPConfig;
|
use actix_remote_ip::RemoteIPConfig;
|
||||||
@@ -98,7 +98,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(MAX_INACTIVITY_DURATION)))
|
.visit_deadline(Some(Duration::from_secs(MAX_INACTIVITY_DURATION)))
|
||||||
.login_deadline(Some(Duration::from_secs(MAX_SESSION_DURATION)))
|
.login_deadline(Some(Duration::from_secs(MAX_SESSION_DURATION)))
|
||||||
.build();
|
.build();
|
||||||
@@ -123,9 +123,9 @@ async fn main() -> std::io::Result<()> {
|
|||||||
.wrap(cors)
|
.wrap(cors)
|
||||||
.app_data(state_manager.clone())
|
.app_data(state_manager.clone())
|
||||||
.app_data(vnc_tokens.clone())
|
.app_data(vnc_tokens.clone())
|
||||||
.app_data(Data::new(RemoteIPConfig::parse_opt(
|
.app_data(Data::new(RemoteIPConfig {
|
||||||
AppConfig::get().proxy_ip.clone(),
|
proxy: AppConfig::get().proxy_ip.clone(),
|
||||||
)))
|
}))
|
||||||
.app_data(conn.clone())
|
.app_data(conn.clone())
|
||||||
// Uploaded files
|
// Uploaded files
|
||||||
.app_data(MultipartFormConfig::default().total_limit(
|
.app_data(MultipartFormConfig::default().total_limit(
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ export default tseslint.config(
|
|||||||
"@typescript-eslint/no-unsafe-member-access": "off",
|
"@typescript-eslint/no-unsafe-member-access": "off",
|
||||||
"@typescript-eslint/no-unsafe-argument": "off",
|
"@typescript-eslint/no-unsafe-argument": "off",
|
||||||
"react-refresh/only-export-components": "off",
|
"react-refresh/only-export-components": "off",
|
||||||
"react-hooks/immutability": "off",
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
3331
virtweb_frontend/package-lock.json
generated
3331
virtweb_frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -12,47 +12,45 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.1",
|
"@emotion/styled": "^11.14.1",
|
||||||
"@fontsource/roboto": "^5.2.10",
|
"@fontsource/roboto": "^5.2.9",
|
||||||
"@mdi/js": "^7.4.47",
|
"@mdi/js": "^7.4.47",
|
||||||
"@mdi/react": "^1.6.1",
|
"@mdi/react": "^1.6.1",
|
||||||
"@monaco-editor/react": "^4.7.0",
|
"@monaco-editor/react": "^4.7.0",
|
||||||
"@mui/icons-material": "^7.3.9",
|
"@mui/icons-material": "^7.3.7",
|
||||||
"@mui/material": "^7.3.9",
|
"@mui/material": "^7.3.7",
|
||||||
"@mui/x-charts": "^8.28.0",
|
"@mui/x-charts": "^8.3.1",
|
||||||
"@mui/x-data-grid": "^8.28.0",
|
"@mui/x-data-grid": "^8.23.0",
|
||||||
"date-and-time": "^4.3.1",
|
"date-and-time": "^3.6.0",
|
||||||
"filesize": "^11.0.13",
|
"filesize": "^10.1.6",
|
||||||
"humanize-duration": "^3.33.2",
|
"humanize-duration": "^3.33.2",
|
||||||
"is-cidr": "^6.0.3",
|
"monaco-editor": "^0.52.2",
|
||||||
"monaco-editor": "^0.55.1",
|
"monaco-yaml": "^5.4.0",
|
||||||
"monaco-yaml": "^5.4.1",
|
"react": "^19.2.3",
|
||||||
"react": "^19.2.4",
|
"react-dom": "^19.2.3",
|
||||||
"react-dom": "^19.2.4",
|
"react-router-dom": "^7.9.6",
|
||||||
"react-router-dom": "^7.13.2",
|
"react-syntax-highlighter": "^15.6.6",
|
||||||
"react-syntax-highlighter": "^16.1.1",
|
"react-vnc": "^3.1.0",
|
||||||
"react-vnc": "^3.2.0",
|
"uuid": "^11.1.0",
|
||||||
"uuid": "^13.0.0",
|
"xml-formatter": "^3.6.6",
|
||||||
"xml-formatter": "^3.7.0",
|
"yaml": "^2.8.2"
|
||||||
"yaml": "^2.8.3"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^9.39.2",
|
||||||
"@types/humanize-duration": "^3.27.4",
|
"@types/humanize-duration": "^3.27.4",
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^30.0.0",
|
||||||
"@types/node": "^25.5.0",
|
"@types/react": "^19.2.9",
|
||||||
"@types/react": "^19.2.14",
|
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@types/react-syntax-highlighter": "^15.5.13",
|
"@types/react-syntax-highlighter": "^15.5.13",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"@vitejs/plugin-react": "^6.0.1",
|
"@vitejs/plugin-react": "^4.7.0",
|
||||||
"eslint": "^10.1.0",
|
"eslint": "^9.39.2",
|
||||||
"eslint-plugin-react-dom": "^3.0.0",
|
"eslint-plugin-react-dom": "^1.53.1",
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"eslint-plugin-react-refresh": "^0.5.2",
|
"eslint-plugin-react-refresh": "^0.4.26",
|
||||||
"eslint-plugin-react-x": "^3.0.0",
|
"eslint-plugin-react-x": "^1.53.1",
|
||||||
"globals": "^17.4.0",
|
"globals": "^16.3.0",
|
||||||
"typescript": "^6.0.2",
|
"typescript": "^5.9.3",
|
||||||
"typescript-eslint": "^8.57.2",
|
"typescript-eslint": "^8.43.0",
|
||||||
"vite": "^8.0.2"
|
"vite": "^6.3.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export interface APIToken {
|
|||||||
updated: number;
|
updated: number;
|
||||||
rights: TokenRight[];
|
rights: TokenRight[];
|
||||||
last_used: number;
|
last_used: number;
|
||||||
allowed_ip_networks?: string[];
|
ip_restriction?: string;
|
||||||
max_inactivity?: number;
|
max_inactivity?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,6 @@ export function DiskImagesRoute(): React.ReactElement {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<AsyncWidget
|
<AsyncWidget
|
||||||
// eslint-disable-next-line react-hooks/refs
|
|
||||||
loadKey={loadKey.current}
|
loadKey={loadKey.current}
|
||||||
errMsg="Failed to load disk images list!"
|
errMsg="Failed to load disk images list!"
|
||||||
load={load}
|
load={load}
|
||||||
@@ -85,7 +84,7 @@ function UploadDiskImageCard(p: {
|
|||||||
|
|
||||||
const [value, setValue] = React.useState<File | null>(null);
|
const [value, setValue] = React.useState<File | null>(null);
|
||||||
const [uploadProgress, setUploadProgress] = React.useState<number | null>(
|
const [uploadProgress, setUploadProgress] = React.useState<number | null>(
|
||||||
null,
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleChange = (newValue: File | null) => {
|
const handleChange = (newValue: File | null) => {
|
||||||
@@ -95,8 +94,8 @@ function UploadDiskImageCard(p: {
|
|||||||
) {
|
) {
|
||||||
alert(
|
alert(
|
||||||
`The file is too big (max size allowed: ${filesize(
|
`The file is too big (max size allowed: ${filesize(
|
||||||
ServerApi.Config.constraints.disk_image_max_size,
|
ServerApi.Config.constraints.disk_image_max_size
|
||||||
)}`,
|
)}`
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -201,7 +200,7 @@ function DiskImageList(p: {
|
|||||||
const deleteDiskImage = async (entry: DiskImage) => {
|
const deleteDiskImage = async (entry: DiskImage) => {
|
||||||
if (
|
if (
|
||||||
!(await confirm(
|
!(await confirm(
|
||||||
`Do you really want to delete this disk image (${entry.file_name}) ?`,
|
`Do you really want to delete this disk image (${entry.file_name}) ?`
|
||||||
))
|
))
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -28,8 +28,7 @@ export function CreateApiTokenRoute(): React.ReactElement {
|
|||||||
CreatedAPIToken | undefined
|
CreatedAPIToken | undefined
|
||||||
>();
|
>();
|
||||||
|
|
||||||
const [token] = React.useState<APIToken>(() => {
|
const [token] = React.useState<APIToken>({
|
||||||
return {
|
|
||||||
id: "",
|
id: "",
|
||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
@@ -37,7 +36,6 @@ export function CreateApiTokenRoute(): React.ReactElement {
|
|||||||
updated: time(),
|
updated: time(),
|
||||||
last_used: time(),
|
last_used: time(),
|
||||||
rights: [],
|
rights: [],
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const createApiToken = async (n: APIToken) => {
|
const createApiToken = async (n: APIToken) => {
|
||||||
@@ -117,11 +115,8 @@ function EditApiTokenRouteInner(p: {
|
|||||||
|
|
||||||
const [changed, setChanged] = React.useState(false);
|
const [changed, setChanged] = React.useState(false);
|
||||||
|
|
||||||
// eslint-disable-next-line react-x/use-state
|
|
||||||
const [, updateState] = React.useState<any>();
|
const [, updateState] = React.useState<any>();
|
||||||
const forceUpdate = React.useCallback(() => {
|
const forceUpdate = React.useCallback(() => { updateState({}); }, []);
|
||||||
updateState({});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const valueChanged = () => {
|
const valueChanged = () => {
|
||||||
setChanged(true);
|
setChanged(true);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export function CreateNWFilterRoute(): React.ReactElement {
|
|||||||
const snackbar = useSnackbar();
|
const snackbar = useSnackbar();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [nwFilter, setNWFilter] = React.useState<NWFilter>({
|
const [nwfilter, setNWFilter] = React.useState<NWFilter>({
|
||||||
name: "my-filter",
|
name: "my-filter",
|
||||||
chain: { protocol: "root" },
|
chain: { protocol: "root" },
|
||||||
join_filters: [],
|
join_filters: [],
|
||||||
@@ -35,7 +35,7 @@ export function CreateNWFilterRoute(): React.ReactElement {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<EditNetworkFilterRouteInner
|
<EditNetworkFilterRouteInner
|
||||||
nwfilter={nwFilter}
|
nwfilter={nwfilter}
|
||||||
creating={true}
|
creating={true}
|
||||||
onCancel={() => navigate("/nwfilter")}
|
onCancel={() => navigate("/nwfilter")}
|
||||||
onSave={createNWFilter}
|
onSave={createNWFilter}
|
||||||
@@ -51,7 +51,7 @@ export function EditNWFilterRoute(): React.ReactElement {
|
|||||||
const { uuid } = useParams();
|
const { uuid } = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [nwFilter, setNWFilter] = React.useState<NWFilter | undefined>();
|
const [nwfilter, setNWFilter] = React.useState<NWFilter | undefined>();
|
||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
setNWFilter(await NWFilterApi.GetSingle(uuid!));
|
setNWFilter(await NWFilterApi.GetSingle(uuid!));
|
||||||
@@ -61,7 +61,7 @@ export function EditNWFilterRoute(): React.ReactElement {
|
|||||||
try {
|
try {
|
||||||
await NWFilterApi.Update(n);
|
await NWFilterApi.Update(n);
|
||||||
snackbar("The network filter was successfully updated!");
|
snackbar("The network filter was successfully updated!");
|
||||||
navigate(NWFilterURL(nwFilter!));
|
navigate(NWFilterURL(nwfilter!));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
alert(`Failed to update network filter!\n${e}`);
|
alert(`Failed to update network filter!\n${e}`);
|
||||||
@@ -71,12 +71,12 @@ export function EditNWFilterRoute(): React.ReactElement {
|
|||||||
return (
|
return (
|
||||||
<AsyncWidget
|
<AsyncWidget
|
||||||
loadKey={uuid}
|
loadKey={uuid}
|
||||||
ready={nwFilter !== undefined}
|
ready={nwfilter !== undefined}
|
||||||
errMsg="Failed to fetch network filter information!"
|
errMsg="Failed to fetch network filter information!"
|
||||||
load={load}
|
load={load}
|
||||||
build={() => (
|
build={() => (
|
||||||
<EditNetworkFilterRouteInner
|
<EditNetworkFilterRouteInner
|
||||||
nwfilter={nwFilter!}
|
nwfilter={nwfilter!}
|
||||||
creating={false}
|
creating={false}
|
||||||
onCancel={() => navigate(`/nwfilter/${uuid}`)}
|
onCancel={() => navigate(`/nwfilter/${uuid}`)}
|
||||||
onSave={updateNetworkFilter}
|
onSave={updateNetworkFilter}
|
||||||
@@ -98,11 +98,8 @@ function EditNetworkFilterRouteInner(p: {
|
|||||||
|
|
||||||
const [changed, setChanged] = React.useState(false);
|
const [changed, setChanged] = React.useState(false);
|
||||||
|
|
||||||
// eslint-disable-next-line react-x/use-state
|
|
||||||
const [, updateState] = React.useState<any>();
|
const [, updateState] = React.useState<any>();
|
||||||
const forceUpdate = React.useCallback(() => {
|
const forceUpdate = React.useCallback(() => { updateState({}); }, []);
|
||||||
updateState({});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const valueChanged = () => {
|
const valueChanged = () => {
|
||||||
setChanged(true);
|
setChanged(true);
|
||||||
|
|||||||
@@ -96,11 +96,8 @@ function EditNetworkRouteInner(p: {
|
|||||||
|
|
||||||
const [changed, setChanged] = React.useState(false);
|
const [changed, setChanged] = React.useState(false);
|
||||||
|
|
||||||
// eslint-disable-next-line react-x/use-state
|
|
||||||
const [, updateState] = React.useState<any>();
|
const [, updateState] = React.useState<any>();
|
||||||
const forceUpdate = React.useCallback(() => {
|
const forceUpdate = React.useCallback(() => { updateState({}); }, []);
|
||||||
updateState({});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const valueChanged = () => {
|
const valueChanged = () => {
|
||||||
setChanged(true);
|
setChanged(true);
|
||||||
|
|||||||
@@ -102,7 +102,6 @@ function EditVMInner(p: {
|
|||||||
|
|
||||||
const [changed, setChanged] = React.useState(false);
|
const [changed, setChanged] = React.useState(false);
|
||||||
|
|
||||||
// eslint-disable-next-line react-x/use-state
|
|
||||||
const [, updateState] = React.useState<any>();
|
const [, updateState] = React.useState<any>();
|
||||||
const forceUpdate = React.useCallback(() => {
|
const forceUpdate = React.useCallback(() => {
|
||||||
updateState({});
|
updateState({});
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ export function IsoFilesRoute(): React.ReactElement {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AsyncWidget
|
<AsyncWidget
|
||||||
// eslint-disable-next-line react-hooks/refs
|
|
||||||
loadKey={loadKey.current}
|
loadKey={loadKey.current}
|
||||||
errMsg="Failed to load ISO files list!"
|
errMsg="Failed to load ISO files list!"
|
||||||
load={load}
|
load={load}
|
||||||
@@ -57,11 +56,7 @@ export function IsoFilesRoute(): React.ReactElement {
|
|||||||
actions={
|
actions={
|
||||||
<span>
|
<span>
|
||||||
<Tooltip title="Open the ISO catalog">
|
<Tooltip title="Open the ISO catalog">
|
||||||
<IconButton
|
<IconButton onClick={() => { setIsoCatalog(true); }}>
|
||||||
onClick={() => {
|
|
||||||
setIsoCatalog(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MenuBookIcon />
|
<MenuBookIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -81,9 +76,7 @@ export function IsoFilesRoute(): React.ReactElement {
|
|||||||
/>
|
/>
|
||||||
<IsoCatalogDialog
|
<IsoCatalogDialog
|
||||||
open={isoCatalog}
|
open={isoCatalog}
|
||||||
onClose={() => {
|
onClose={() => { setIsoCatalog(false); }}
|
||||||
setIsoCatalog(false);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -97,15 +90,15 @@ function UploadIsoFileCard(p: {
|
|||||||
|
|
||||||
const [value, setValue] = React.useState<File | null>(null);
|
const [value, setValue] = React.useState<File | null>(null);
|
||||||
const [uploadProgress, setUploadProgress] = React.useState<number | null>(
|
const [uploadProgress, setUploadProgress] = React.useState<number | null>(
|
||||||
null,
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleChange = (newValue: File | null) => {
|
const handleChange = (newValue: File | null) => {
|
||||||
if (newValue && newValue.size > ServerApi.Config.constraints.iso_max_size) {
|
if (newValue && newValue.size > ServerApi.Config.constraints.iso_max_size) {
|
||||||
alert(
|
alert(
|
||||||
`The file is too big (max size allowed: ${filesize(
|
`The file is too big (max size allowed: ${filesize(
|
||||||
ServerApi.Config.constraints.iso_max_size,
|
ServerApi.Config.constraints.iso_max_size
|
||||||
)}`,
|
)}`
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -251,7 +244,7 @@ function IsoFilesList(p: {
|
|||||||
const deleteIso = async (entry: IsoFile) => {
|
const deleteIso = async (entry: IsoFile) => {
|
||||||
if (
|
if (
|
||||||
!(await confirm(
|
!(await confirm(
|
||||||
`Do you really want to delete this file (${entry.filename}) ?`,
|
`Do you really want to delete this file (${entry.filename}) ?`
|
||||||
))
|
))
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
mdiNetwork,
|
mdiNetwork,
|
||||||
mdiPackageVariantClosed,
|
mdiPackageVariantClosed,
|
||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
import { Icon } from "@mdi/react";
|
import Icon from "@mdi/react";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
IconButton,
|
IconButton,
|
||||||
@@ -81,7 +81,7 @@ export function SysInfoRouteInner(p: {
|
|||||||
free: prev.free + disk.available_space,
|
free: prev.free + disk.available_space,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{ used: 0, free: 0 },
|
{ used: 0, free: 0 }
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -199,7 +199,6 @@ export function SysInfoRouteInner(p: {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Bootime",
|
label: "Bootime",
|
||||||
// eslint-disable-next-line react-x/purity
|
|
||||||
value: new Date(p.info.system.boot_time * 1000).toString(),
|
value: new Date(p.info.system.boot_time * 1000).toString(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -320,7 +319,7 @@ function DiskDetailsTable(p: { disks: DiskInfo[] }): React.ReactElement {
|
|||||||
{p.disks.map((e, c) => (
|
{p.disks.map((e, c) => (
|
||||||
<TableRow hover key={c}>
|
<TableRow hover key={c}>
|
||||||
<TableCell>{e.name}</TableCell>
|
<TableCell>{e.name}</TableCell>
|
||||||
<TableCell>{e.DiskKind}</TableCell>
|
<TableCell>{String(e.DiskKind)}</TableCell>
|
||||||
<TableCell>{e.mount_point}</TableCell>
|
<TableCell>{e.mount_point}</TableCell>
|
||||||
<TableCell>{filesize(e.total_space)}</TableCell>
|
<TableCell>{filesize(e.total_space)}</TableCell>
|
||||||
<TableCell>{filesize(e.available_space)}</TableCell>
|
<TableCell>{filesize(e.available_space)}</TableCell>
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export function TokensListRouteInner(p: {
|
|||||||
<TableCell>Created</TableCell>
|
<TableCell>Created</TableCell>
|
||||||
<TableCell>Updated</TableCell>
|
<TableCell>Updated</TableCell>
|
||||||
<TableCell>Last used</TableCell>
|
<TableCell>Last used</TableCell>
|
||||||
<TableCell>Allowed networks</TableCell>
|
<TableCell>IP restriction</TableCell>
|
||||||
<TableCell>Max inactivity</TableCell>
|
<TableCell>Max inactivity</TableCell>
|
||||||
<TableCell>Rights</TableCell>
|
<TableCell>Rights</TableCell>
|
||||||
<TableCell>Actions</TableCell>
|
<TableCell>Actions</TableCell>
|
||||||
@@ -97,7 +97,7 @@ export function TokensListRouteInner(p: {
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
<TimeWidget time={t.last_used} />
|
<TimeWidget time={t.last_used} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{t.allowed_ip_networks?.join(", ")}</TableCell>
|
<TableCell>{t.ip_restriction}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{t.max_inactivity && timeDiff(0, t.max_inactivity)}
|
{t.max_inactivity && timeDiff(0, t.max_inactivity)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ export function VMListRoute(): React.ReactElement {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AsyncWidget
|
<AsyncWidget
|
||||||
// eslint-disable-next-line react-hooks/refs
|
|
||||||
loadKey={loadKey.current}
|
loadKey={loadKey.current}
|
||||||
errMsg="Failed to load Virtual Machines list!"
|
errMsg="Failed to load Virtual Machines list!"
|
||||||
load={load}
|
load={load}
|
||||||
@@ -77,11 +76,11 @@ function VMListWidget(p: {
|
|||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [hiddenGroups, setHiddenGroups] = React.useState(() => new Set());
|
const [hiddenGroups, setHiddenGroups] = React.useState<
|
||||||
|
Set<string | undefined>
|
||||||
|
>(new Set());
|
||||||
|
|
||||||
const [runningVMs, setRunningVMs] = React.useState<Set<string>>(
|
const [runningVMs, setRunningVMs] = React.useState<Set<string>>(new Set());
|
||||||
() => new Set(),
|
|
||||||
);
|
|
||||||
|
|
||||||
const toggleHiddenGroup = (g: string | undefined) => {
|
const toggleHiddenGroup = (g: string | undefined) => {
|
||||||
if (hiddenGroups.has(g)) hiddenGroups.delete(g);
|
if (hiddenGroups.has(g)) hiddenGroups.delete(g);
|
||||||
@@ -187,7 +186,7 @@ function VMListWidget(p: {
|
|||||||
{filesize(
|
{filesize(
|
||||||
p.list
|
p.list
|
||||||
.filter((v) => runningVMs.has(v.name))
|
.filter((v) => runningVMs.has(v.name))
|
||||||
.reduce((s, v) => s + v.memory, 0),
|
.reduce((s, v) => s + v.memory, 0)
|
||||||
)}
|
)}
|
||||||
{" / "}
|
{" / "}
|
||||||
{filesize(p.list.reduce((s, v) => s + v.memory, 0))}
|
{filesize(p.list.reduce((s, v) => s + v.memory, 0))}
|
||||||
|
|||||||
@@ -88,7 +88,6 @@ function VNCInner(p: { vm: VMInfo }): React.ReactElement {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
||||||
connect(false);
|
connect(false);
|
||||||
|
|
||||||
if (vncRef.current) {
|
if (vncRef.current) {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { NWFilterDetails } from "../widgets/nwfilter/NWFilterDetails";
|
|||||||
export function ViewNWFilterRoute() {
|
export function ViewNWFilterRoute() {
|
||||||
const { uuid } = useParams();
|
const { uuid } = useParams();
|
||||||
|
|
||||||
const [nwFilter, setNWFilter] = React.useState<NWFilter | undefined>();
|
const [nwfilter, setNWFilter] = React.useState<NWFilter | undefined>();
|
||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
setNWFilter(await NWFilterApi.GetSingle(uuid!));
|
setNWFilter(await NWFilterApi.GetSingle(uuid!));
|
||||||
@@ -24,10 +24,10 @@ export function ViewNWFilterRoute() {
|
|||||||
return (
|
return (
|
||||||
<AsyncWidget
|
<AsyncWidget
|
||||||
loadKey={uuid}
|
loadKey={uuid}
|
||||||
ready={nwFilter !== undefined}
|
ready={nwfilter !== undefined}
|
||||||
errMsg="Failed to fetch network filter information!"
|
errMsg="Failed to fetch network filter information!"
|
||||||
load={load}
|
load={load}
|
||||||
build={() => <ViewNetworkFilterRouteInner nwfilter={nwFilter!} />}
|
build={() => <ViewNetworkFilterRouteInner nwfilter={nwfilter!} />}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,14 +41,12 @@ export function LoginRoute(): React.ReactElement {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseDownPassword = (
|
const handleMouseDownPassword = (
|
||||||
event: React.MouseEvent<HTMLButtonElement>,
|
event: React.MouseEvent<HTMLButtonElement>
|
||||||
) => {
|
) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLoginSubmit = async (
|
const handleLoginSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
event: React.SubmitEvent<HTMLFormElement>,
|
|
||||||
) => {
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!canSubmit) return;
|
if (!canSubmit) return;
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import isCidr from "is-cidr";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check whether a given IP network address is valid or not
|
|
||||||
*
|
|
||||||
* @param ip The IP network to check
|
|
||||||
* @returns true if the address is valid, false otherwise
|
|
||||||
*/
|
|
||||||
export function isIPNetworkValid(ip: string): boolean {
|
|
||||||
return isCidr(ip) !== 0;
|
|
||||||
}
|
|
||||||
@@ -1,25 +1,26 @@
|
|||||||
import { Alert, Box, Button, CircularProgress } from "@mui/material";
|
import { Alert, Box, Button, CircularProgress } from "@mui/material";
|
||||||
import React from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
const State = {
|
enum State {
|
||||||
Loading: 0,
|
Loading,
|
||||||
Ready: 1,
|
Ready,
|
||||||
Error: 2,
|
Error,
|
||||||
} as const;
|
}
|
||||||
|
|
||||||
type State = keyof typeof State;
|
|
||||||
|
|
||||||
export function AsyncWidget(p: {
|
export function AsyncWidget(p: {
|
||||||
loadKey: unknown;
|
loadKey: any;
|
||||||
load: () => Promise<void>;
|
load: () => Promise<void>;
|
||||||
errMsg: string;
|
errMsg: string;
|
||||||
build: () => React.ReactElement;
|
build: () => React.ReactElement;
|
||||||
|
ready?: boolean;
|
||||||
buildLoading?: () => React.ReactElement;
|
buildLoading?: () => React.ReactElement;
|
||||||
buildError?: (e: string) => React.ReactElement;
|
buildError?: (e: string) => React.ReactElement;
|
||||||
ready?: boolean;
|
|
||||||
errAdditionalElement?: () => React.ReactElement;
|
errAdditionalElement?: () => React.ReactElement;
|
||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
const [state, setState] = React.useState(State.Loading as number);
|
const [state, setState] = useState(State.Loading);
|
||||||
|
|
||||||
|
const counter = useRef<any>(null);
|
||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
try {
|
try {
|
||||||
setState(State.Loading);
|
setState(State.Loading);
|
||||||
@@ -31,10 +32,12 @@ export function AsyncWidget(p: {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (counter.current === p.loadKey) return;
|
||||||
|
counter.current = p.loadKey;
|
||||||
|
|
||||||
load();
|
load();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps, react-x/exhaustive-deps
|
});
|
||||||
}, [p.loadKey]);
|
|
||||||
|
|
||||||
if (state === State.Error)
|
if (state === State.Error)
|
||||||
return (
|
return (
|
||||||
@@ -48,6 +51,10 @@ export function AsyncWidget(p: {
|
|||||||
height: "100%",
|
height: "100%",
|
||||||
flex: "1",
|
flex: "1",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
|
backgroundColor: (theme) =>
|
||||||
|
theme.palette.mode === "light"
|
||||||
|
? theme.palette.grey[100]
|
||||||
|
: theme.palette.grey[900],
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Alert
|
<Alert
|
||||||
@@ -76,6 +83,10 @@ export function AsyncWidget(p: {
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
flex: "1",
|
flex: "1",
|
||||||
|
backgroundColor: (theme) =>
|
||||||
|
theme.palette.mode === "light"
|
||||||
|
? theme.palette.grey[100]
|
||||||
|
: theme.palette.grey[900],
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CircularProgress />
|
<CircularProgress />
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
mdiLan,
|
mdiLan,
|
||||||
mdiSecurityNetwork,
|
mdiSecurityNetwork,
|
||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
import { Icon } from "@mdi/react";
|
import Icon from "@mdi/react";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
List,
|
List,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
/* eslint-disable react-x/purity */
|
|
||||||
import { mdiServer } from "@mdi/js";
|
import { mdiServer } from "@mdi/js";
|
||||||
import { Icon } from "@mdi/react";
|
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";
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
export function DateWidget(p: { time: number }): React.ReactElement {
|
export function DateWidget(p: { time: number }): React.ReactElement {
|
||||||
const date = React.useMemo(() => new Date(p.time * 1000), [p.time]);
|
const date = new Date(p.time * 1000);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Avatar, ListItem, ListItemAvatar, ListItemText } from "@mui/material";
|
|||||||
import { DiskImage } from "../api/DiskImageApi";
|
import { DiskImage } from "../api/DiskImageApi";
|
||||||
import { mdiHarddisk } from "@mdi/js";
|
import { mdiHarddisk } from "@mdi/js";
|
||||||
import { filesize } from "filesize";
|
import { filesize } from "filesize";
|
||||||
import { Icon } from "@mdi/react";
|
import Icon from "@mdi/react";
|
||||||
|
|
||||||
export function FileDiskImageWidget(p: {
|
export function FileDiskImageWidget(p: {
|
||||||
image: DiskImage;
|
image: DiskImage;
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Tooltip } from "@mui/material";
|
import { Tooltip } from "@mui/material";
|
||||||
import { format } from "date-and-time";
|
import date from "date-and-time";
|
||||||
import { time } from "../utils/DateUtils";
|
import { time } from "../utils/DateUtils";
|
||||||
|
|
||||||
export function formatDate(time: number): string {
|
export function formatDate(time: number): string {
|
||||||
const t = new Date();
|
const t = new Date();
|
||||||
t.setTime(1000 * time);
|
t.setTime(1000 * time);
|
||||||
return format(t, "DD/MM/YYYY HH:mm:ss");
|
return date.format(t, "DD/MM/YYYY HH:mm:ss");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function timeDiff(a: number, b: number): string {
|
export function timeDiff(a: number, b: number): string {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { mdiLogout, mdiServer } from "@mdi/js";
|
import { mdiLogout, mdiServer } from "@mdi/js";
|
||||||
import { Icon } from "@mdi/react";
|
import Icon from "@mdi/react";
|
||||||
import { AppBar, IconButton, Toolbar, Typography } from "@mui/material";
|
import { AppBar, IconButton, Toolbar, Typography } from "@mui/material";
|
||||||
import { RouterLink } from "./RouterLink";
|
import { RouterLink } from "./RouterLink";
|
||||||
import { AsyncWidget } from "./AsyncWidget";
|
import { AsyncWidget } from "./AsyncWidget";
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ export function IPInputWithMask(p: {
|
|||||||
|
|
||||||
const currValue =
|
const currValue =
|
||||||
p.ipAndMask ??
|
p.ipAndMask ??
|
||||||
// eslint-disable-next-line react-hooks/refs
|
|
||||||
`${p.ip ?? ""}${p.mask || showSlash.current ? "/" : ""}${p.mask ?? ""}`;
|
`${p.ip ?? ""}${p.mask || showSlash.current ? "/" : ""}${p.mask ?? ""}`;
|
||||||
|
|
||||||
const { onValueChange, ...props } = p;
|
const { onValueChange, ...props } = p;
|
||||||
@@ -59,7 +58,7 @@ export function IPInputWithMask(p: {
|
|||||||
onValueChange?.(
|
onValueChange?.(
|
||||||
ip,
|
ip,
|
||||||
mask,
|
mask,
|
||||||
mask || showSlash.current ? `${ip}/${mask ?? ""}` : ip,
|
mask || showSlash.current ? `${ip}/${mask ?? ""}` : ip
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
value={currValue}
|
value={currValue}
|
||||||
|
|||||||
@@ -20,13 +20,12 @@ export function NWFSelectReferencedFilters(p: {
|
|||||||
p.excludedFilters
|
p.excludedFilters
|
||||||
? p.nwFiltersList.filter((f) => !p.excludedFilters!.includes(f.name))
|
? p.nwFiltersList.filter((f) => !p.excludedFilters!.includes(f.name))
|
||||||
: p.nwFiltersList,
|
: p.nwFiltersList,
|
||||||
[p.excludedFilters, p.nwFiltersList],
|
[p.excludedFilters]
|
||||||
);
|
);
|
||||||
|
|
||||||
const selectedFilters = React.useMemo(
|
const selectedFilters = React.useMemo(
|
||||||
() => p.selected.map((f) => p.nwFiltersList.find((s) => s.name === f)),
|
() => p.selected.map((f) => p.nwFiltersList.find((s) => s.name === f)),
|
||||||
// eslint-disable-next-line react-x/exhaustive-deps
|
[p.selected.length]
|
||||||
[p.selected.length, p.nwFiltersList],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable react-x/no-array-index-key */
|
/* eslint-disable react-x/no-array-index-key */
|
||||||
import { mdiIp } from "@mdi/js";
|
import { mdiIp } from "@mdi/js";
|
||||||
import { Icon } from "@mdi/react";
|
import Icon from "@mdi/react";
|
||||||
import DeleteIcon from "@mui/icons-material/Delete";
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
import { isIPNetworkValid } from "../../utils/NetworkUtils";
|
|
||||||
import { TextInput } from "./TextInput";
|
|
||||||
|
|
||||||
function rebuildNetworksList(val?: string): string[] | undefined {
|
|
||||||
if (!val || val.trim() === "") return undefined;
|
|
||||||
|
|
||||||
return val.split(",").map((v) => v.trim());
|
|
||||||
}
|
|
||||||
|
|
||||||
export function NetworksInput(p: {
|
|
||||||
editable: boolean;
|
|
||||||
label: string;
|
|
||||||
value?: string[];
|
|
||||||
onChange: (n: string[] | undefined) => void;
|
|
||||||
}): React.ReactElement {
|
|
||||||
const textValue = (p.value ?? []).join(", ").trim();
|
|
||||||
return (
|
|
||||||
<TextInput
|
|
||||||
{...p}
|
|
||||||
type="string"
|
|
||||||
value={textValue}
|
|
||||||
onValueChange={(i) => {
|
|
||||||
p.onChange(rebuildNetworksList(i));
|
|
||||||
}}
|
|
||||||
checkValue={(v) => (rebuildNetworksList(v) ?? []).every(isIPNetworkValid)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { mdiHarddiskPlus } from "@mdi/js";
|
import { mdiHarddiskPlus } from "@mdi/js";
|
||||||
import { Icon } from "@mdi/react";
|
import Icon from "@mdi/react";
|
||||||
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
|
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
|
||||||
import DeleteIcon from "@mui/icons-material/Delete";
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
import ExpandIcon from "@mui/icons-material/Expand";
|
import ExpandIcon from "@mui/icons-material/Expand";
|
||||||
@@ -125,7 +125,7 @@ function DiskInfo(p: {
|
|||||||
`You asked to delete the disk ${p.disk.name}. Do you want to keep the block file or not ? `,
|
`You asked to delete the disk ${p.disk.name}. Do you want to keep the block file or not ? `,
|
||||||
"Delete disk",
|
"Delete disk",
|
||||||
"Keep the file",
|
"Keep the file",
|
||||||
"Delete the file",
|
"Delete the file"
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!(await confirm("Do you really want to delete this disk?"))) return;
|
if (!(await confirm("Do you really want to delete this disk?"))) return;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable react-x/no-array-index-key */
|
/* eslint-disable react-x/no-array-index-key */
|
||||||
import { mdiNetworkOutline } from "@mdi/js";
|
import { mdiNetworkOutline } from "@mdi/js";
|
||||||
import { Icon } from "@mdi/react";
|
import Icon from "@mdi/react";
|
||||||
import DeleteIcon from "@mui/icons-material/Delete";
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
@@ -191,7 +191,7 @@ function NetworkInfoWidget(p: {
|
|||||||
editable={p.editable}
|
editable={p.editable}
|
||||||
label="Defined network"
|
label="Defined network"
|
||||||
options={p.networksList.map((n) => {
|
options={p.networksList.map((n) => {
|
||||||
const chars = [n.forward_mode as string];
|
const chars = [n.forward_mode.toString()];
|
||||||
if (n.ip_v4) chars.push("IPv4");
|
if (n.ip_v4) chars.push("IPv4");
|
||||||
if (n.ip_v6) chars.push("IPv6");
|
if (n.ip_v6) chars.push("IPv6");
|
||||||
if (n.description) chars.push(n.description);
|
if (n.description) chars.push(n.description);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import DeleteIcon from "@mui/icons-material/Delete";
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
import { mdiDisc } from "@mdi/js";
|
import { mdiDisc } from "@mdi/js";
|
||||||
import { Icon } from "@mdi/react";
|
import Icon from "@mdi/react";
|
||||||
|
|
||||||
export function VMSelectIsoInput(p: {
|
export function VMSelectIsoInput(p: {
|
||||||
editable: boolean;
|
editable: boolean;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { mdiSecurityNetwork } from "@mdi/js";
|
import { mdiSecurityNetwork } from "@mdi/js";
|
||||||
import { Icon } from "@mdi/react";
|
import Icon from "@mdi/react";
|
||||||
import DeleteIcon from "@mui/icons-material/Delete";
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ import { useSnackbar } from "../../hooks/providers/SnackbarProvider";
|
|||||||
import { AsyncWidget } from "../AsyncWidget";
|
import { AsyncWidget } from "../AsyncWidget";
|
||||||
import { TabsWidget } from "../TabsWidget";
|
import { TabsWidget } from "../TabsWidget";
|
||||||
import { EditSection } from "../forms/EditSection";
|
import { EditSection } from "../forms/EditSection";
|
||||||
import { NetworksInput } from "../forms/NetworksInput";
|
import { IPInputWithMask } from "../forms/IPInput";
|
||||||
|
import { RadioGroupInput } from "../forms/RadioGroupInput";
|
||||||
import { TextInput } from "../forms/TextInput";
|
import { TextInput } from "../forms/TextInput";
|
||||||
import { TokenRawRightsEditor } from "./TokenRawRightsEditor";
|
import { TokenRawRightsEditor } from "./TokenRawRightsEditor";
|
||||||
import { TokenRightsEditor } from "./TokenRightsEditor";
|
import { TokenRightsEditor } from "./TokenRightsEditor";
|
||||||
@@ -34,17 +35,17 @@ interface DetailsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function APITokenDetails(p: DetailsProps): React.ReactElement {
|
export function APITokenDetails(p: DetailsProps): React.ReactElement {
|
||||||
const [vms, setVms] = React.useState<VMInfo[]>();
|
const [vms, setVMs] = React.useState<VMInfo[]>();
|
||||||
const [groups, setGroups] = React.useState<string[]>();
|
const [groups, setGroups] = React.useState<string[]>();
|
||||||
const [networks, setNetworks] = React.useState<NetworkInfo[]>();
|
const [networks, setNetworks] = React.useState<NetworkInfo[]>();
|
||||||
const [nwFilters, setNwFilters] = React.useState<NWFilter[]>();
|
const [nwFilters, setNetworkFilters] = React.useState<NWFilter[]>();
|
||||||
const [tokens, setTokens] = React.useState<APIToken[]>();
|
const [tokens, setTokens] = React.useState<APIToken[]>();
|
||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
setVms(await VMApi.GetList());
|
setVMs(await VMApi.GetList());
|
||||||
setGroups(await GroupApi.GetList());
|
setGroups(await GroupApi.GetList());
|
||||||
setNetworks(await NetworkApi.GetList());
|
setNetworks(await NetworkApi.GetList());
|
||||||
setNwFilters(await NWFilterApi.GetList());
|
setNetworkFilters(await NWFilterApi.GetList());
|
||||||
setTokens(await TokensApi.GetList());
|
setTokens(await TokensApi.GetList());
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -120,6 +121,10 @@ function APITokenDetailsInner(p: DetailsInnerProps): React.ReactElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function APITokenTabGeneral(p: DetailsInnerProps): React.ReactElement {
|
function APITokenTabGeneral(p: DetailsInnerProps): React.ReactElement {
|
||||||
|
const [ipVersion, setIpVersion] = React.useState<4 | 6>(
|
||||||
|
(p.token.ip_restriction ?? "").includes(":") ? 6 : 4
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid container spacing={2}>
|
<Grid container spacing={2}>
|
||||||
{/* Metadata section */}
|
{/* Metadata section */}
|
||||||
@@ -153,13 +158,29 @@ function APITokenTabGeneral(p: DetailsInnerProps): React.ReactElement {
|
|||||||
</EditSection>
|
</EditSection>
|
||||||
|
|
||||||
<EditSection title="General settings">
|
<EditSection title="General settings">
|
||||||
<NetworksInput
|
{p.status === TokenWidgetStatus.Create && (
|
||||||
|
<RadioGroupInput
|
||||||
{...p}
|
{...p}
|
||||||
label="Allowd IP networks (comma-separated list, leave empty to allow from anywhere)"
|
editable={true}
|
||||||
|
options={[
|
||||||
|
{ label: "IPv4", value: "4" },
|
||||||
|
{ label: "IPv6", value: "6" },
|
||||||
|
]}
|
||||||
|
value={ipVersion.toString()}
|
||||||
|
onValueChange={(v) => {
|
||||||
|
setIpVersion(Number(v) as 4 | 6);
|
||||||
|
}}
|
||||||
|
label="Token IP restriction version"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<IPInputWithMask
|
||||||
|
{...p}
|
||||||
|
label="Token IP network restriction"
|
||||||
|
ipAndMask={p.token.ip_restriction}
|
||||||
editable={p.status === TokenWidgetStatus.Create}
|
editable={p.status === TokenWidgetStatus.Create}
|
||||||
value={p.token.allowed_ip_networks}
|
version={ipVersion}
|
||||||
onChange={(v) => {
|
onValueChange={(_ip, _mask, ipAndMask) => {
|
||||||
p.token.allowed_ip_networks = v;
|
p.token.ip_restriction = ipAndMask;
|
||||||
p.onChange?.();
|
p.onChange?.();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -218,7 +239,7 @@ function APITokenTabDanger(p: DetailsInnerProps): React.ReactElement {
|
|||||||
!(await confirm(
|
!(await confirm(
|
||||||
"Do you really want to delete this API token?",
|
"Do you really want to delete this API token?",
|
||||||
`Delete API token ${p.token.name}`,
|
`Delete API token ${p.token.name}`,
|
||||||
"Delete",
|
"Delete"
|
||||||
))
|
))
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"types": ["node"],
|
|
||||||
|
|
||||||
/* Bundler mode */
|
/* Bundler mode */
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
|
|||||||
Reference in New Issue
Block a user