Compare commits
18 Commits
e14f51ef7e
...
20240407
| Author | SHA1 | Date | |
|---|---|---|---|
| f403c85f0a | |||
| db25c7e426 | |||
| 98b67534cb | |||
| 09d3cf08f3 | |||
| 9b3d32811f | |||
| 26d391ea96 | |||
| c044996014 | |||
| 34b04968b2 | |||
| c44a3f2673 | |||
| e46adcd1da | |||
| d1506f26ab | |||
| 23194d13d2 | |||
| 131dec892d | |||
| c2e6105aff | |||
| f5202f596d | |||
| 3bf8859ff9 | |||
| 9a905e83f7 | |||
| 4b9df95721 |
23
.drone.yml
23
.drone.yml
@@ -12,6 +12,7 @@ steps:
|
|||||||
commands:
|
commands:
|
||||||
- cd virtweb_frontend
|
- cd virtweb_frontend
|
||||||
- npm install --legacy-peer-deps # TODO : remove when mui-file-input is updated
|
- npm install --legacy-peer-deps # TODO : remove when mui-file-input is updated
|
||||||
|
- npm run lint
|
||||||
- npm run build
|
- npm run build
|
||||||
- mv dist /tmp/web_build
|
- mv dist /tmp/web_build
|
||||||
|
|
||||||
@@ -35,6 +36,8 @@ steps:
|
|||||||
path: /usr/local/cargo/registry
|
path: /usr/local/cargo/registry
|
||||||
- name: web_app
|
- name: web_app
|
||||||
path: /tmp/web_build
|
path: /tmp/web_build
|
||||||
|
- name: release
|
||||||
|
path: /tmp/release
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend_check
|
- backend_check
|
||||||
- web_build
|
- web_build
|
||||||
@@ -44,10 +47,30 @@ steps:
|
|||||||
- mv /tmp/web_build/dist static
|
- mv /tmp/web_build/dist static
|
||||||
- cargo build --release
|
- cargo build --release
|
||||||
- ls -lah target/release/virtweb_backend
|
- ls -lah target/release/virtweb_backend
|
||||||
|
- cp target/release/virtweb_backend /tmp/release
|
||||||
|
|
||||||
|
- name: gitea_release
|
||||||
|
image: plugins/gitea-release
|
||||||
|
depends_on:
|
||||||
|
- backend_compile
|
||||||
|
when:
|
||||||
|
event:
|
||||||
|
- tag
|
||||||
|
volumes:
|
||||||
|
- name: release
|
||||||
|
path: /tmp/release
|
||||||
|
environment:
|
||||||
|
PLUGIN_API_KEY:
|
||||||
|
from_secret: API_KEY
|
||||||
|
settings:
|
||||||
|
base_url: https://gitea.communiquons.org
|
||||||
|
files: /tmp/release/*
|
||||||
|
checksum: sha512
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
- name: rust_registry
|
- name: rust_registry
|
||||||
temp: {}
|
temp: {}
|
||||||
- name: web_app
|
- name: web_app
|
||||||
temp: {}
|
temp: {}
|
||||||
|
- name: release
|
||||||
|
temp: {}
|
||||||
|
|||||||
@@ -1,9 +1,3 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
"extends": ["local>renovate/presets"]
|
||||||
"packageRules": [
|
|
||||||
{
|
|
||||||
"matchUpdateTypes": ["major", "minor", "patch"],
|
|
||||||
"automerge": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
10
virtweb_backend/Cargo.lock
generated
10
virtweb_backend/Cargo.lock
generated
@@ -590,15 +590,15 @@ checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "basic-jwt"
|
name = "basic-jwt"
|
||||||
version = "0.2.0"
|
version = "0.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "741afb780192f091b1ceebdc794540a956f3eb96628939f83c5d15e0cb98fa71"
|
checksum = "1b87da415ea2a5c22d90143d39a2db6c0c910d3d01bfd19d0f50369de91215f0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"elliptic-curve",
|
"elliptic-curve",
|
||||||
"jsonwebtoken",
|
"jsonwebtoken",
|
||||||
"p384",
|
"p384",
|
||||||
"rand 0.8.5",
|
"rand 0.9.0",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3261,9 +3261,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sysinfo"
|
name = "sysinfo"
|
||||||
version = "0.34.0"
|
version = "0.34.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "36913edff7a70c19d204c379e484b2a04d121fbb4e68bebc9fea651eb4386397"
|
checksum = "a4b93974b3d3aeaa036504b8eefd4c039dced109171c1ae973f1dc63b2c7e4b2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"memchr",
|
"memchr",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ edition = "2024"
|
|||||||
# 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.26"
|
log = "0.4.27"
|
||||||
env_logger = "0.11.7"
|
env_logger = "0.11.7"
|
||||||
clap = { version = "4.5.34", features = ["derive", "env"] }
|
clap = { version = "4.5.34", features = ["derive", "env"] }
|
||||||
light-openid = { version = "1.0.4", features = ["crypto-wrapper"] }
|
light-openid = { version = "1.0.4", features = ["crypto-wrapper"] }
|
||||||
@@ -22,7 +22,7 @@ actix-ws = "0.3.0"
|
|||||||
actix-http = "3.10.0"
|
actix-http = "3.10.0"
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
serde_json = "1.0.140"
|
serde_json = "1.0.140"
|
||||||
quick-xml = { version = "0.37.2", features = ["serialize", "overlapped-lists"] }
|
quick-xml = { version = "0.37.3", features = ["serialize", "overlapped-lists"] }
|
||||||
futures-util = "0.3.31"
|
futures-util = "0.3.31"
|
||||||
anyhow = "1.0.97"
|
anyhow = "1.0.97"
|
||||||
actix-multipart = "0.7.2"
|
actix-multipart = "0.7.2"
|
||||||
@@ -30,19 +30,19 @@ tempfile = "3.19.1"
|
|||||||
reqwest = { version = "0.12.15", features = ["stream"] }
|
reqwest = { version = "0.12.15", features = ["stream"] }
|
||||||
url = "2.5.4"
|
url = "2.5.4"
|
||||||
virt = "0.4.2"
|
virt = "0.4.2"
|
||||||
sysinfo = { version = "0.34.0", features = ["serde"] }
|
sysinfo = { version = "0.34.2", features = ["serde"] }
|
||||||
uuid = { version = "1.16.0", features = ["v4", "serde"] }
|
uuid = { version = "1.16.0", features = ["v4", "serde"] }
|
||||||
lazy-regex = "3.4.1"
|
lazy-regex = "3.4.1"
|
||||||
thiserror = "2.0.12"
|
thiserror = "2.0.12"
|
||||||
image = "0.25.5"
|
image = "0.25.6"
|
||||||
rand = "0.9.0"
|
rand = "0.9.0"
|
||||||
bytes = "1.10.1"
|
bytes = "1.10.1"
|
||||||
tokio = { version = "1.44.1", features = ["rt", "time", "macros"] }
|
tokio = { version = "1.44.1", features = ["rt", "time", "macros"] }
|
||||||
futures = "0.3.31"
|
futures = "0.3.31"
|
||||||
ipnetwork = { version = "0.21.1", features = ["serde"] }
|
ipnetwork = { version = "0.21.1", features = ["serde"] }
|
||||||
num = "0.4.2"
|
num = "0.4.3"
|
||||||
rust-embed = { version = "8.5.0" }
|
rust-embed = { version = "8.6.0" }
|
||||||
mime_guess = "2.0.5"
|
mime_guess = "2.0.5"
|
||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
nix = { version = "0.29.0", features = ["net"] }
|
nix = { version = "0.29.0", features = ["net"] }
|
||||||
basic-jwt = "0.2.0"
|
basic-jwt = "0.3.0"
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import js from "@eslint/js";
|
import js from "@eslint/js";
|
||||||
import reactDom from 'eslint-plugin-react-dom';
|
import reactDom from "eslint-plugin-react-dom";
|
||||||
import reactHooks from "eslint-plugin-react-hooks";
|
import reactHooks from "eslint-plugin-react-hooks";
|
||||||
import reactRefresh from "eslint-plugin-react-refresh";
|
import reactRefresh from "eslint-plugin-react-refresh";
|
||||||
import reactX from 'eslint-plugin-react-x';
|
import reactX from "eslint-plugin-react-x";
|
||||||
import globals from "globals";
|
import globals from "globals";
|
||||||
import tseslint from "typescript-eslint";
|
import tseslint from "typescript-eslint";
|
||||||
|
|
||||||
|
|
||||||
export default tseslint.config(
|
export default tseslint.config(
|
||||||
{ ignores: ["dist"] },
|
{ ignores: ["dist"] },
|
||||||
{
|
{
|
||||||
@@ -38,6 +37,18 @@ export default tseslint.config(
|
|||||||
],
|
],
|
||||||
...reactX.configs["recommended-typescript"].rules,
|
...reactX.configs["recommended-typescript"].rules,
|
||||||
...reactDom.configs.recommended.rules,
|
...reactDom.configs.recommended.rules,
|
||||||
|
"@typescript-eslint/no-non-null-assertion": "off",
|
||||||
|
"@typescript-eslint/no-misused-promises": "off",
|
||||||
|
"@typescript-eslint/no-floating-promises": "off",
|
||||||
|
"@typescript-eslint/restrict-template-expressions": "off",
|
||||||
|
"@typescript-eslint/no-extraneous-class": "off",
|
||||||
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
"@typescript-eslint/no-unsafe-assignment": "off",
|
||||||
|
"@typescript-eslint/no-unsafe-return": "off",
|
||||||
|
"@typescript-eslint/no-unsafe-call": "off",
|
||||||
|
"@typescript-eslint/no-unsafe-member-access": "off",
|
||||||
|
"@typescript-eslint/no-unsafe-argument": "off",
|
||||||
|
"react-refresh/only-export-components": "off",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -51,7 +51,10 @@ export function App() {
|
|||||||
|
|
||||||
const context: AuthContext = {
|
const context: AuthContext = {
|
||||||
signedIn: signedIn,
|
signedIn: signedIn,
|
||||||
setSignedIn: (s) => setSignedIn(s),
|
setSignedIn: (s) => {
|
||||||
|
setSignedIn(s);
|
||||||
|
location.reload();
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const router = createBrowserRouter(
|
const router = createBrowserRouter(
|
||||||
@@ -97,12 +100,12 @@ export function App() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthContextK.Provider value={context}>
|
<AuthContextK value={context}>
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
</AuthContextK.Provider>
|
</AuthContextK>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAuth(): AuthContext {
|
export function useAuth(): AuthContext {
|
||||||
return React.useContext(AuthContextK)!;
|
return React.use(AuthContextK)!;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export class APIClient {
|
|||||||
* Get backend URL
|
* Get backend URL
|
||||||
*/
|
*/
|
||||||
static backendURL(): string {
|
static backendURL(): string {
|
||||||
const URL = import.meta.env.VITE_APP_BACKEND ?? "";
|
const URL = String(import.meta.env.VITE_APP_BACKEND ?? "");
|
||||||
if (URL.length === 0) throw new Error("Backend URL undefined!");
|
if (URL.length === 0) throw new Error("Backend URL undefined!");
|
||||||
return URL;
|
return URL;
|
||||||
}
|
}
|
||||||
@@ -44,7 +44,7 @@ export class APIClient {
|
|||||||
*/
|
*/
|
||||||
static async exec(args: RequestParams): Promise<APIResponse> {
|
static async exec(args: RequestParams): Promise<APIResponse> {
|
||||||
let body: string | undefined | FormData = undefined;
|
let body: string | undefined | FormData = undefined;
|
||||||
let headers: any = {};
|
const headers: any = {};
|
||||||
|
|
||||||
// JSON request
|
// JSON request
|
||||||
if (args.jsonData) {
|
if (args.jsonData) {
|
||||||
@@ -66,22 +66,25 @@ export class APIClient {
|
|||||||
if (args.upProgress) {
|
if (args.upProgress) {
|
||||||
const res: XMLHttpRequest = await new Promise((resolve, reject) => {
|
const res: XMLHttpRequest = await new Promise((resolve, reject) => {
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
xhr.upload.addEventListener("progress", (e) =>
|
xhr.upload.addEventListener("progress", (e) => {
|
||||||
args.upProgress!(e.loaded / e.total)
|
args.upProgress!(e.loaded / e.total);
|
||||||
);
|
});
|
||||||
xhr.addEventListener("load", () => resolve(xhr));
|
xhr.addEventListener("load", () => {
|
||||||
xhr.addEventListener("error", () =>
|
resolve(xhr);
|
||||||
reject(new Error("File upload failed"))
|
});
|
||||||
);
|
xhr.addEventListener("error", () => {
|
||||||
xhr.addEventListener("abort", () =>
|
reject(new Error("File upload failed"));
|
||||||
reject(new Error("File upload aborted"))
|
});
|
||||||
);
|
xhr.addEventListener("abort", () => {
|
||||||
xhr.addEventListener("timeout", () =>
|
reject(new Error("File upload aborted"));
|
||||||
reject(new Error("File upload timeout"))
|
});
|
||||||
);
|
xhr.addEventListener("timeout", () => {
|
||||||
|
reject(new Error("File upload timeout"));
|
||||||
|
});
|
||||||
xhr.open(args.method, url, true);
|
xhr.open(args.method, url, true);
|
||||||
xhr.withCredentials = true;
|
xhr.withCredentials = true;
|
||||||
for (const key in headers) {
|
for (const key in headers) {
|
||||||
|
// eslint-disable-next-line no-prototype-builtins
|
||||||
if (headers.hasOwnProperty(key))
|
if (headers.hasOwnProperty(key))
|
||||||
xhr.setRequestHeader(key, headers[key]);
|
xhr.setRequestHeader(key, headers[key]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ export interface NWFilter {
|
|||||||
rules: NWFilterRule[];
|
rules: NWFilterRule[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NWFilterURL(n: NWFilter, edit: boolean = false): string {
|
export function NWFilterURL(n: NWFilter, edit = false): string {
|
||||||
return `/nwfilter/${n.uuid}${edit ? "/edit" : ""}`;
|
return `/nwfilter/${n.uuid}${edit ? "/edit" : ""}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,7 +221,7 @@ export class NWFilterApi {
|
|||||||
static async Delete(n: NWFilter): Promise<void> {
|
static async Delete(n: NWFilter): Promise<void> {
|
||||||
await APIClient.exec({
|
await APIClient.exec({
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
uri: `/nwfilter/${n.uuid}`,
|
uri: `/nwfilter/${n.uuid!}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export interface NetworkInfo {
|
|||||||
|
|
||||||
export type NetworkStatus = "Started" | "Stopped";
|
export type NetworkStatus = "Started" | "Stopped";
|
||||||
|
|
||||||
export function NetworkURL(n: NetworkInfo, edit: boolean = false): string {
|
export function NetworkURL(n: NetworkInfo, edit = false): string {
|
||||||
return `/net/${n.uuid}${edit ? "/edit" : ""}`;
|
return `/net/${n.uuid}${edit ? "/edit" : ""}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export interface APIToken {
|
|||||||
max_inactivity?: number;
|
max_inactivity?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function APITokenURL(t: APIToken, edit: boolean = false): string {
|
export function APITokenURL(t: APIToken, edit = false): string {
|
||||||
return `/token/${t.id}${edit ? "/edit" : ""}`;
|
return `/token/${t.id}${edit ? "/edit" : ""}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export function AlertDialogProvider(p: PropsWithChildren): React.ReactElement {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AlertContextK.Provider value={hook}>{p.children}</AlertContextK.Provider>
|
<AlertContextK value={hook}>{p.children}</AlertContextK>
|
||||||
|
|
||||||
<Dialog
|
<Dialog
|
||||||
open={open}
|
open={open}
|
||||||
@@ -67,5 +67,5 @@ export function AlertDialogProvider(p: PropsWithChildren): React.ReactElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useAlert(): AlertContext {
|
export function useAlert(): AlertContext {
|
||||||
return React.useContext(AlertContextK)!;
|
return React.use(AlertContextK)!;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,13 +59,13 @@ export function ConfirmDialogProvider(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ConfirmContextK.Provider value={hook}>
|
<ConfirmContextK value={hook}>
|
||||||
{p.children}
|
{p.children}
|
||||||
</ConfirmContextK.Provider>
|
</ConfirmContextK>
|
||||||
|
|
||||||
<Dialog
|
<Dialog
|
||||||
open={open}
|
open={open}
|
||||||
onClose={() => handleClose(false)}
|
onClose={() => { handleClose(false); }}
|
||||||
aria-labelledby="alert-dialog-title"
|
aria-labelledby="alert-dialog-title"
|
||||||
aria-describedby="alert-dialog-description"
|
aria-describedby="alert-dialog-description"
|
||||||
>
|
>
|
||||||
@@ -76,10 +76,10 @@ export function ConfirmDialogProvider(
|
|||||||
</DialogContentText>
|
</DialogContentText>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={() => handleClose(false)} autoFocus>
|
<Button onClick={() => { handleClose(false); }} autoFocus>
|
||||||
{cancelButton ?? "Cancel"}
|
{cancelButton ?? "Cancel"}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => handleClose(true)} color="error">
|
<Button onClick={() => { handleClose(true); }} color="error">
|
||||||
{confirmButton ?? "Confirm"}
|
{confirmButton ?? "Confirm"}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
@@ -89,5 +89,5 @@ export function ConfirmDialogProvider(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useConfirm(): ConfirmContext {
|
export function useConfirm(): ConfirmContext {
|
||||||
return React.useContext(ConfirmContextK)!;
|
return React.use(ConfirmContextK)!;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import {
|
|||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import React, { PropsWithChildren } from "react";
|
import React, { PropsWithChildren } from "react";
|
||||||
|
|
||||||
type LoadingMessageContext = {
|
interface LoadingMessageContext {
|
||||||
show: (message: string) => void;
|
show: (message: string) => void;
|
||||||
hide: () => void;
|
hide: () => void;
|
||||||
};
|
}
|
||||||
|
|
||||||
const LoadingMessageContextK =
|
const LoadingMessageContextK =
|
||||||
React.createContext<LoadingMessageContext | null>(null);
|
React.createContext<LoadingMessageContext | null>(null);
|
||||||
@@ -34,9 +34,9 @@ export function LoadingMessageProvider(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<LoadingMessageContextK.Provider value={hook}>
|
<LoadingMessageContextK value={hook}>
|
||||||
{p.children}
|
{p.children}
|
||||||
</LoadingMessageContextK.Provider>
|
</LoadingMessageContextK>
|
||||||
|
|
||||||
<Dialog open={open}>
|
<Dialog open={open}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
@@ -60,5 +60,5 @@ export function LoadingMessageProvider(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useLoadingMessage(): LoadingMessageContext {
|
export function useLoadingMessage(): LoadingMessageContext {
|
||||||
return React.useContext(LoadingMessageContextK)!;
|
return React.use(LoadingMessageContextK)!;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ export function SnackbarProvider(p: PropsWithChildren): React.ReactElement {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SnackbarContextK.Provider value={hook}>
|
<SnackbarContextK value={hook}>
|
||||||
{p.children}
|
{p.children}
|
||||||
</SnackbarContextK.Provider>
|
</SnackbarContextK>
|
||||||
|
|
||||||
<Snackbar
|
<Snackbar
|
||||||
open={open}
|
open={open}
|
||||||
@@ -39,5 +39,5 @@ export function SnackbarProvider(p: PropsWithChildren): React.ReactElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useSnackbar(): SnackbarContext {
|
export function useSnackbar(): SnackbarContext {
|
||||||
return React.useContext(SnackbarContextK)!;
|
return React.use(SnackbarContextK)!;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const darkTheme = createTheme({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(
|
const root = ReactDOM.createRoot(
|
||||||
document.getElementById("root") as HTMLElement
|
document.getElementById("root")!
|
||||||
);
|
);
|
||||||
root.render(
|
root.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ function EditApiTokenRouteInner(p: {
|
|||||||
const [changed, setChanged] = React.useState(false);
|
const [changed, setChanged] = React.useState(false);
|
||||||
|
|
||||||
const [, updateState] = React.useState<any>();
|
const [, updateState] = React.useState<any>();
|
||||||
const forceUpdate = React.useCallback(() => updateState({}), []);
|
const forceUpdate = React.useCallback(() => { updateState({}); }, []);
|
||||||
|
|
||||||
const valueChanged = () => {
|
const valueChanged = () => {
|
||||||
setChanged(true);
|
setChanged(true);
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ function EditNetworkFilterRouteInner(p: {
|
|||||||
const [changed, setChanged] = React.useState(false);
|
const [changed, setChanged] = React.useState(false);
|
||||||
|
|
||||||
const [, updateState] = React.useState<any>();
|
const [, updateState] = React.useState<any>();
|
||||||
const forceUpdate = React.useCallback(() => updateState({}), []);
|
const forceUpdate = React.useCallback(() => { updateState({}); }, []);
|
||||||
|
|
||||||
const valueChanged = () => {
|
const valueChanged = () => {
|
||||||
setChanged(true);
|
setChanged(true);
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ function EditNetworkRouteInner(p: {
|
|||||||
const [changed, setChanged] = React.useState(false);
|
const [changed, setChanged] = React.useState(false);
|
||||||
|
|
||||||
const [, updateState] = React.useState<any>();
|
const [, updateState] = React.useState<any>();
|
||||||
const forceUpdate = React.useCallback(() => updateState({}), []);
|
const forceUpdate = React.useCallback(() => { updateState({}); }, []);
|
||||||
|
|
||||||
const valueChanged = () => {
|
const valueChanged = () => {
|
||||||
setChanged(true);
|
setChanged(true);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export function CreateVMRoute(): React.ReactElement {
|
|||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [vm, setVM] = React.useState(VMInfo.NewEmpty);
|
const [vm, setVM] = React.useState(VMInfo.NewEmpty());
|
||||||
|
|
||||||
const create = async (v: VMInfo) => {
|
const create = async (v: VMInfo) => {
|
||||||
try {
|
try {
|
||||||
@@ -103,7 +103,9 @@ function EditVMInner(p: {
|
|||||||
const [changed, setChanged] = React.useState(false);
|
const [changed, setChanged] = React.useState(false);
|
||||||
|
|
||||||
const [, updateState] = React.useState<any>();
|
const [, updateState] = React.useState<any>();
|
||||||
const forceUpdate = React.useCallback(() => updateState({}), []);
|
const forceUpdate = React.useCallback(() => {
|
||||||
|
updateState({});
|
||||||
|
}, []);
|
||||||
|
|
||||||
const valueChanged = () => {
|
const valueChanged = () => {
|
||||||
setChanged(true);
|
setChanged(true);
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ function UploadIsoFileCard(p: {
|
|||||||
p.onFileUploaded();
|
p.onFileUploaded();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
await alert("Failed to perform file upload! " + e);
|
await alert(`Failed to perform file upload! ${e}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
setUploadProgress(null);
|
setUploadProgress(null);
|
||||||
@@ -120,7 +120,9 @@ function UploadIsoFileCard(p: {
|
|||||||
value={value}
|
value={value}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
inputProps={{ accept: ServerApi.Config.iso_mimetypes.join(",") }}
|
slotProps={{
|
||||||
|
htmlInput: { accept: ServerApi.Config.iso_mimetypes.join(",") },
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{value && <Button onClick={upload}>Upload file</Button>}
|
{value && <Button onClick={upload}>Upload file</Button>}
|
||||||
@@ -166,14 +168,18 @@ function UploadIsoFileFromUrlCard(p: {
|
|||||||
label="URL"
|
label="URL"
|
||||||
value={url}
|
value={url}
|
||||||
style={{ flex: 3 }}
|
style={{ flex: 3 }}
|
||||||
onChange={(e) => setURL(e.target.value)}
|
onChange={(e) => {
|
||||||
|
setURL(e.target.value);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<span style={{ width: "10px" }}></span>
|
<span style={{ width: "10px" }}></span>
|
||||||
<TextField
|
<TextField
|
||||||
label="Filename"
|
label="Filename"
|
||||||
value={actualFileName}
|
value={actualFileName}
|
||||||
style={{ flex: 2 }}
|
style={{ flex: 2 }}
|
||||||
onChange={(e) => setFilename(e.target.value)}
|
onChange={(e) => {
|
||||||
|
setFilename(e.target.value);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
{url !== "" && actualFileName !== "" && (
|
{url !== "" && actualFileName !== "" && (
|
||||||
<Button onClick={upload}>Upload file</Button>
|
<Button onClick={upload}>Upload file</Button>
|
||||||
@@ -200,7 +206,7 @@ function IsoFilesList(p: {
|
|||||||
try {
|
try {
|
||||||
const blob = await IsoFilesApi.Download(entry, setDlProgress);
|
const blob = await IsoFilesApi.Download(entry, setDlProgress);
|
||||||
|
|
||||||
await downloadBlob(blob, entry.filename);
|
downloadBlob(blob, entry.filename);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
alert("Failed to download iso file!");
|
alert("Failed to download iso file!");
|
||||||
@@ -238,7 +244,7 @@ function IsoFilesList(p: {
|
|||||||
</Typography>
|
</Typography>
|
||||||
);
|
);
|
||||||
|
|
||||||
const columns: GridColDef[] = [
|
const columns: GridColDef<IsoFile>[] = [
|
||||||
{ field: "filename", headerName: "File name", flex: 3 },
|
{ field: "filename", headerName: "File name", flex: 3 },
|
||||||
{
|
{
|
||||||
field: "size",
|
field: "size",
|
||||||
@@ -303,7 +309,6 @@ function IsoFilesList(p: {
|
|||||||
getRowId={(c) => c.filename}
|
getRowId={(c) => c.filename}
|
||||||
rows={p.list}
|
rows={p.list}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
autoHeight={true}
|
|
||||||
/>
|
/>
|
||||||
</VirtWebPaper>
|
</VirtWebPaper>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ function NetworkFiltersListRouteInner(p: {
|
|||||||
const onlyBuiltin = visibleFilters === VisibleFilters.Builtin;
|
const onlyBuiltin = visibleFilters === VisibleFilters.Builtin;
|
||||||
|
|
||||||
return p.list.filter((f) => NWFilterIsBuiltin(f) === onlyBuiltin);
|
return p.list.filter((f) => NWFilterIsBuiltin(f) === onlyBuiltin);
|
||||||
}, [visibleFilters]);
|
}, [visibleFilters, p.list]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VirtWebRouteContainer
|
<VirtWebRouteContainer
|
||||||
@@ -78,7 +78,9 @@ function NetworkFiltersListRouteInner(p: {
|
|||||||
size="small"
|
size="small"
|
||||||
value={visibleFilters}
|
value={visibleFilters}
|
||||||
exclusive
|
exclusive
|
||||||
onChange={(_ev, v) => setVisibleFilters(v)}
|
onChange={(_ev, v) => {
|
||||||
|
setVisibleFilters(v);
|
||||||
|
}}
|
||||||
aria-label="visible filters"
|
aria-label="visible filters"
|
||||||
>
|
>
|
||||||
<ToggleButton value={VisibleFilters.All}>All</ToggleButton>
|
<ToggleButton value={VisibleFilters.All}>All</ToggleButton>
|
||||||
@@ -130,8 +132,8 @@ function NetworkFiltersListRouteInner(p: {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<ul>
|
<ul>
|
||||||
{t.join_filters.map((f, n) => (
|
{t.join_filters.map((f) => (
|
||||||
<li key={n}>{f}</li>
|
<li key={f}>{f}</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable react-x/no-array-index-key */
|
||||||
import {
|
import {
|
||||||
mdiHarddisk,
|
mdiHarddisk,
|
||||||
mdiInformation,
|
mdiInformation,
|
||||||
@@ -236,7 +237,7 @@ export function SysInfoRouteInner(p: {
|
|||||||
function SysInfoDetailsTable(p: {
|
function SysInfoDetailsTable(p: {
|
||||||
label: string;
|
label: string;
|
||||||
icon: React.ReactElement;
|
icon: React.ReactElement;
|
||||||
entries: Array<{ label: string; value: string | number }>;
|
entries: { label: string; value: string | number }[];
|
||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
<VirtWebPaper
|
<VirtWebPaper
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable react-x/no-array-index-key */
|
||||||
import VisibilityIcon from "@mui/icons-material/Visibility";
|
import VisibilityIcon from "@mui/icons-material/Visibility";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -99,9 +100,9 @@ export function TokensListRouteInner(p: {
|
|||||||
{t.max_inactivity && timeDiff(0, t.max_inactivity)}
|
{t.max_inactivity && timeDiff(0, t.max_inactivity)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{t.rights.map((r) => {
|
{t.rights.map((r, n) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div key={n}>
|
||||||
{r.verb} {r.path}
|
{r.verb} {r.path}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -25,13 +25,13 @@ import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer";
|
|||||||
import { VMStatusWidget } from "../widgets/vms/VMStatusWidget";
|
import { VMStatusWidget } from "../widgets/vms/VMStatusWidget";
|
||||||
|
|
||||||
export function VMListRoute(): React.ReactElement {
|
export function VMListRoute(): React.ReactElement {
|
||||||
const [groups, setGroups] = React.useState<Array<string | undefined>>();
|
const [groups, setGroups] = React.useState<(string | undefined)[]>();
|
||||||
const [list, setList] = React.useState<VMInfo[] | undefined>();
|
const [list, setList] = React.useState<VMInfo[] | undefined>();
|
||||||
|
|
||||||
const loadKey = React.useRef(1);
|
const loadKey = React.useRef(1);
|
||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
const groups: Array<string | undefined> = await GroupApi.GetList();
|
const groups: (string | undefined)[] = await GroupApi.GetList();
|
||||||
const list = await VMApi.GetList();
|
const list = await VMApi.GetList();
|
||||||
|
|
||||||
if (list.find((v) => !v.group) !== undefined) groups.push(undefined);
|
if (list.find((v) => !v.group) !== undefined) groups.push(undefined);
|
||||||
@@ -70,7 +70,7 @@ export function VMListRoute(): React.ReactElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function VMListWidget(p: {
|
function VMListWidget(p: {
|
||||||
groups: Array<string | undefined>;
|
groups: (string | undefined)[];
|
||||||
list: VMInfo[];
|
list: VMInfo[];
|
||||||
onReload: () => void;
|
onReload: () => void;
|
||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
@@ -115,8 +115,8 @@ function VMListWidget(p: {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{p.groups.map((g, num) => (
|
{p.groups.map((g) => (
|
||||||
<React.Fragment key={num}>
|
<React.Fragment key={g}>
|
||||||
{p.groups.length > 1 && (
|
{p.groups.length > 1 && (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell
|
<TableCell
|
||||||
@@ -125,9 +125,11 @@ function VMListWidget(p: {
|
|||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => toggleHiddenGroup(g)}
|
onClick={() => {
|
||||||
|
toggleHiddenGroup(g);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{!hiddenGroups?.has(g) ? (
|
{!hiddenGroups.has(g) ? (
|
||||||
<KeyboardArrowUpIcon />
|
<KeyboardArrowUpIcon />
|
||||||
) : (
|
) : (
|
||||||
<KeyboardArrowDownIcon />
|
<KeyboardArrowDownIcon />
|
||||||
@@ -157,7 +159,9 @@ function VMListWidget(p: {
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
<VMStatusWidget
|
<VMStatusWidget
|
||||||
vm={row}
|
vm={row}
|
||||||
onChange={(s) => updateVMState(row, s)}
|
onChange={(s) => {
|
||||||
|
updateVMState(row, s);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ function VNCInner(p: { vm: VMInfo }): React.ReactElement {
|
|||||||
const [counter, setCounter] = React.useState(1);
|
const [counter, setCounter] = React.useState(1);
|
||||||
const [connected, setConnected] = React.useState(false);
|
const [connected, setConnected] = React.useState(false);
|
||||||
|
|
||||||
const vncRef = React.createRef<HTMLDivElement>();
|
const vncRef = React.useRef<HTMLDivElement>(null);
|
||||||
const vncScreenRef = React.createRef<VncScreenHandle>();
|
const vncScreenRef = React.useRef<VncScreenHandle>(null);
|
||||||
|
|
||||||
const connect = async (force: boolean) => {
|
const connect = async (force: boolean) => {
|
||||||
try {
|
try {
|
||||||
@@ -91,7 +91,9 @@ function VNCInner(p: { vm: VMInfo }): React.ReactElement {
|
|||||||
connect(false);
|
connect(false);
|
||||||
|
|
||||||
if (vncRef.current) {
|
if (vncRef.current) {
|
||||||
vncRef.current.onfullscreenchange = () => setCounter(counter + 1);
|
vncRef.current.onfullscreenchange = () => {
|
||||||
|
setCounter(counter + 1);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -140,10 +142,12 @@ function VNCInner(p: { vm: VMInfo }): React.ReactElement {
|
|||||||
ref={vncScreenRef}
|
ref={vncScreenRef}
|
||||||
url={token.url}
|
url={token.url}
|
||||||
onDisconnect={() => {
|
onDisconnect={() => {
|
||||||
console.info("VNC disconnected " + token?.url);
|
console.info("VNC disconnected " + token.url);
|
||||||
disconnected();
|
disconnected();
|
||||||
}}
|
}}
|
||||||
onConnect={() => setConnected(true)}
|
onConnect={() => {
|
||||||
|
setConnected(true);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export function LoginRoute(): React.ReactElement {
|
|||||||
const canSubmit = username.length > 0 && password.length > 0;
|
const canSubmit = username.length > 0 && password.length > 0;
|
||||||
|
|
||||||
const [showPassword, setShowPassword] = React.useState(false);
|
const [showPassword, setShowPassword] = React.useState(false);
|
||||||
const handleClickShowPassword = () => setShowPassword((show) => !show);
|
const handleClickShowPassword = () => { setShowPassword((show) => !show); };
|
||||||
|
|
||||||
const handleMouseDownPassword = (
|
const handleMouseDownPassword = (
|
||||||
event: React.MouseEvent<HTMLButtonElement>
|
event: React.MouseEvent<HTMLButtonElement>
|
||||||
@@ -105,7 +105,7 @@ export function LoginRoute(): React.ReactElement {
|
|||||||
label="Username"
|
label="Username"
|
||||||
name="username"
|
name="username"
|
||||||
value={username}
|
value={username}
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
onChange={(e) => { setUsername(e.target.value); }}
|
||||||
autoComplete="username"
|
autoComplete="username"
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
@@ -120,7 +120,7 @@ export function LoginRoute(): React.ReactElement {
|
|||||||
type={showPassword ? "text" : "password"}
|
type={showPassword ? "text" : "password"}
|
||||||
id="password"
|
id="password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => { setPassword(e.target.value); }}
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
endAdornment={
|
endAdornment={
|
||||||
<InputAdornment position="end">
|
<InputAdornment position="end">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export async function downloadBlob(blob: Blob, filename: string) {
|
export function downloadBlob(blob: Blob, filename: string) {
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
const link = document.createElement("a");
|
const link = document.createElement("a");
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
* Generate a random MAC address
|
* Generate a random MAC address
|
||||||
*/
|
*/
|
||||||
export function randomMacAddress(prefix: string | undefined): string {
|
export function randomMacAddress(prefix: string | undefined): string {
|
||||||
|
prefix = prefix ?? "";
|
||||||
let mac = "XX:XX:XX:XX:XX:XX";
|
let mac = "XX:XX:XX:XX:XX:XX";
|
||||||
mac = prefix + mac.slice(prefix?.length);
|
mac = prefix + mac.slice(prefix.length);
|
||||||
|
|
||||||
return mac.replace(/X/g, () =>
|
return mac.replace(/X/g, () =>
|
||||||
"0123456789abcdef".charAt(Math.floor(Math.random() * 16))
|
"0123456789abcdef".charAt(Math.floor(Math.random() * 16))
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export function AsyncWidget(p: {
|
|||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
const [state, setState] = useState(State.Loading);
|
const [state, setState] = useState(State.Loading);
|
||||||
|
|
||||||
const counter = useRef<any | null>(null);
|
const counter = useRef<any>(null);
|
||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -67,7 +67,7 @@ export function AsyncWidget(p: {
|
|||||||
|
|
||||||
<Button onClick={load}>Try again</Button>
|
<Button onClick={load}>Try again</Button>
|
||||||
|
|
||||||
{p.errAdditionalElement && p.errAdditionalElement()}
|
{p.errAdditionalElement?.()}
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,8 +13,7 @@ import {
|
|||||||
List,
|
List,
|
||||||
ListItemButton,
|
ListItemButton,
|
||||||
ListItemIcon,
|
ListItemIcon,
|
||||||
ListItemSecondaryAction,
|
ListItemText
|
||||||
ListItemText,
|
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { Outlet, useLocation } from "react-router-dom";
|
import { Outlet, useLocation } from "react-router-dom";
|
||||||
import { RouterLink } from "./RouterLink";
|
import { RouterLink } from "./RouterLink";
|
||||||
@@ -95,7 +94,6 @@ function NavLink(p: {
|
|||||||
icon: React.ReactElement;
|
icon: React.ReactElement;
|
||||||
uri: string;
|
uri: string;
|
||||||
label: string;
|
label: string;
|
||||||
secondaryAction?: React.ReactElement;
|
|
||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
return (
|
return (
|
||||||
@@ -103,9 +101,6 @@ function NavLink(p: {
|
|||||||
<ListItemButton selected={p.uri === location.pathname}>
|
<ListItemButton selected={p.uri === location.pathname}>
|
||||||
<ListItemIcon>{p.icon}</ListItemIcon>
|
<ListItemIcon>{p.icon}</ListItemIcon>
|
||||||
<ListItemText primary={p.label} />
|
<ListItemText primary={p.label} />
|
||||||
{p.secondaryAction && (
|
|
||||||
<ListItemSecondaryAction>{p.secondaryAction}</ListItemSecondaryAction>
|
|
||||||
)}
|
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -31,14 +31,16 @@ export function ConfigImportExportButtons(p: {
|
|||||||
fileEl.click();
|
fileEl.click();
|
||||||
|
|
||||||
// Wait for a file to be chosen
|
// Wait for a file to be chosen
|
||||||
await new Promise((res, _rej) =>
|
await new Promise((res) => {
|
||||||
fileEl.addEventListener("change", () => res(null))
|
fileEl.addEventListener("change", () => {
|
||||||
);
|
res(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
if ((fileEl.files?.length ?? 0) === 0) return null;
|
if ((fileEl.files?.length ?? 0) === 0) return null;
|
||||||
|
|
||||||
// Import conf
|
// Import conf
|
||||||
let file = fileEl.files![0];
|
const file = fileEl.files![0];
|
||||||
const content = await file.text();
|
const content = await file.text();
|
||||||
p.importConf?.(JSON.parse(content));
|
p.importConf?.(JSON.parse(content));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export function StateActionButton<S>(p: {
|
|||||||
p.onExecuted();
|
p.onExecuted();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
alert("Failed to perform action! " + e);
|
alert(`Failed to perform action! ${e}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable react-x/no-array-index-key */
|
||||||
import { Box, Tab, Tabs } from "@mui/material";
|
import { Box, Tab, Tabs } from "@mui/material";
|
||||||
|
|
||||||
export interface TabWidgetOption<E> {
|
export interface TabWidgetOption<E> {
|
||||||
@@ -24,7 +25,9 @@ export function TabsWidget<E>(p: {
|
|||||||
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
|
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
|
||||||
<Tabs
|
<Tabs
|
||||||
value={currTabIndex}
|
value={currTabIndex}
|
||||||
onChange={(_ev, newVal) => updateActiveTab(newVal)}
|
onChange={(_ev, newVal) => {
|
||||||
|
updateActiveTab(newVal);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{activeOptions.map((o, index) => (
|
{activeOptions.map((o, index) => (
|
||||||
<Tab key={index} label={o.label} style={{ color: o.color }} />
|
<Tab key={index} label={o.label} style={{ color: o.color }} />
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export function CheckboxInput(p: {
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
disabled={!p.editable}
|
disabled={!p.editable}
|
||||||
checked={p.checked}
|
checked={p.checked}
|
||||||
onChange={(e) => p.onValueChange(e.target.checked)}
|
onChange={(e) => { p.onValueChange(e.target.checked); }}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label={p.label}
|
label={p.label}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
|
||||||
import { Paper, Typography } from "@mui/material";
|
import { Paper, Typography } from "@mui/material";
|
||||||
import React, { PropsWithChildren } from "react";
|
import React, { PropsWithChildren } from "react";
|
||||||
import Grid from "@mui/material/Grid";
|
import Grid from "@mui/material/Grid";
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { TextInput } from "./TextInput";
|
import { TextInput } from "./TextInput";
|
||||||
|
|
||||||
@@ -32,7 +33,7 @@ export function IPInputWithMask(p: {
|
|||||||
|
|
||||||
const currValue =
|
const currValue =
|
||||||
p.ipAndMask ??
|
p.ipAndMask ??
|
||||||
(p.ip ?? "") + (p.mask || showSlash.current ? "/" : "") + (p.mask ?? "");
|
`${p.ip ?? ""}${p.mask || showSlash.current ? "/" : ""}${p.mask ?? ""}`;
|
||||||
|
|
||||||
const { onValueChange, ...props } = p;
|
const { onValueChange, ...props } = p;
|
||||||
return (
|
return (
|
||||||
@@ -44,7 +45,7 @@ export function IPInputWithMask(p: {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const split = v?.split("/");
|
const split = v.split("/");
|
||||||
const ip =
|
const ip =
|
||||||
p.version === 4 ? sanitizeIpV4(split[0]) : sanitizeIpV6(split[0]);
|
p.version === 4 ? sanitizeIpV4(split[0]) : sanitizeIpV6(split[0]);
|
||||||
let mask = undefined;
|
let mask = undefined;
|
||||||
@@ -69,7 +70,7 @@ export function IPInputWithMask(p: {
|
|||||||
function sanitizeIpV4(s: string | undefined): string | undefined {
|
function sanitizeIpV4(s: string | undefined): string | undefined {
|
||||||
if (s === "" || s === undefined) return s;
|
if (s === "" || s === undefined) return s;
|
||||||
|
|
||||||
let split = s.split(".");
|
const split = s.split(".");
|
||||||
if (split.length > 4) split.splice(4);
|
if (split.length > 4) split.splice(4);
|
||||||
|
|
||||||
let needAnotherIteration = false;
|
let needAnotherIteration = false;
|
||||||
@@ -106,7 +107,7 @@ function sanitizeIpV6(s: string | undefined): string | undefined {
|
|||||||
const num = parseInt(e, 16);
|
const num = parseInt(e, 16);
|
||||||
if (isNaN(num)) return "0";
|
if (isNaN(num)) return "0";
|
||||||
|
|
||||||
let s = num.toString(16);
|
const s = num.toString(16);
|
||||||
if (num > 0xffff) {
|
if (num > 0xffff) {
|
||||||
needAnotherIteration = true;
|
needAnotherIteration = true;
|
||||||
return s.slice(0, 4) + ":" + s.slice(4);
|
return s.slice(0, 4) + ":" + s.slice(4);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
|
||||||
import { TextInput } from "./TextInput";
|
import { TextInput } from "./TextInput";
|
||||||
|
|
||||||
export function MACInput(p: {
|
export function MACInput(p: {
|
||||||
@@ -32,7 +33,7 @@ function sanitizeMacAddress(s: string | undefined): string | undefined {
|
|||||||
const num = parseInt(e, 16);
|
const num = parseInt(e, 16);
|
||||||
if (isNaN(num)) return "0";
|
if (isNaN(num)) return "0";
|
||||||
|
|
||||||
let s = num.toString(16).padStart(2, "0");
|
const s = num.toString(16).padStart(2, "0");
|
||||||
if (num > 0xff) {
|
if (num > 0xff) {
|
||||||
needAnotherIteration = true;
|
needAnotherIteration = true;
|
||||||
return s.slice(0, 2) + ":" + s.slice(2);
|
return s.slice(0, 2) + ":" + s.slice(2);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export function NWFConnStateInput(p: {
|
|||||||
label="Connection state"
|
label="Connection state"
|
||||||
value={p.value}
|
value={p.value}
|
||||||
onValueChange={(s) => {
|
onValueChange={(s) => {
|
||||||
p.onChange?.(s as any);
|
p.onChange(s as any);
|
||||||
}}
|
}}
|
||||||
options={[
|
options={[
|
||||||
{ label: "None", value: undefined },
|
{ label: "None", value: undefined },
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
/* eslint-disable react-x/no-array-index-key */
|
||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { NWFilter, NWFilterURL } from "../../api/NWFilterApi";
|
import { NWFilter, NWFilterURL } from "../../api/NWFilterApi";
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export function NWFilterPriorityInput(p: {
|
|||||||
value={p.value?.toString()}
|
value={p.value?.toString()}
|
||||||
type="number"
|
type="number"
|
||||||
onValueChange={(v) => {
|
onValueChange={(v) => {
|
||||||
p.onChange?.(v && v !== "" ? Number(v) : undefined);
|
p.onChange(v && v !== "" ? Number(v) : undefined);
|
||||||
}}
|
}}
|
||||||
size={ServerApi.Config.constraints.nwfilter_priority}
|
size={ServerApi.Config.constraints.nwfilter_priority}
|
||||||
helperText="A lower priority value is accessed before one with a higher value"
|
helperText="A lower priority value is accessed before one with a higher value"
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable react-x/no-array-index-key */
|
||||||
import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward";
|
import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward";
|
||||||
import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward";
|
import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward";
|
||||||
import DeleteIcon from "@mui/icons-material/Delete";
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
@@ -66,9 +67,19 @@ export function NWFilterRules(p: {
|
|||||||
deleteRule(n);
|
deleteRule(n);
|
||||||
}}
|
}}
|
||||||
onGoDown={
|
onGoDown={
|
||||||
n < p.rules.length - 1 ? () => swapRules(n, n + 1) : undefined
|
n < p.rules.length - 1
|
||||||
|
? () => {
|
||||||
|
swapRules(n, n + 1);
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onGoUp={
|
||||||
|
n > 0
|
||||||
|
? () => {
|
||||||
|
swapRules(n, n - 1);
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
}
|
}
|
||||||
onGoUp={n > 0 ? () => swapRules(n, n - 1) : undefined}
|
|
||||||
{...p}
|
{...p}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -153,7 +164,9 @@ function NWRuleEdit(p: {
|
|||||||
editable={p.editable}
|
editable={p.editable}
|
||||||
onChange={p.onChange}
|
onChange={p.onChange}
|
||||||
selector={s}
|
selector={s}
|
||||||
onDelete={() => deleteSelector(n)}
|
onDelete={() => {
|
||||||
|
deleteSelector(n);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -25,9 +25,7 @@ export function NWFilterSelectInput(p: {
|
|||||||
value={selectedValue}
|
value={selectedValue}
|
||||||
onDelete={p.editable ? () => p.onChange?.(undefined) : undefined}
|
onDelete={p.editable ? () => p.onChange?.(undefined) : undefined}
|
||||||
onClick={
|
onClick={
|
||||||
!p.editable && selectedValue
|
!p.editable ? () => navigate(NWFilterURL(selectedValue)) : undefined
|
||||||
? () => navigate(NWFilterURL(selectedValue))
|
|
||||||
: undefined
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -48,7 +46,7 @@ export function NWFilterSelectInput(p: {
|
|||||||
renderInput={(params) => (
|
renderInput={(params) => (
|
||||||
<TextField {...params} variant="standard" label={p.label} />
|
<TextField {...params} variant="standard" label={p.label} />
|
||||||
)}
|
)}
|
||||||
renderOption={(_props, option, _state) => (
|
renderOption={(_props, option) => (
|
||||||
<NWFilterItem
|
<NWFilterItem
|
||||||
dense
|
dense
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* 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";
|
||||||
@@ -130,7 +131,7 @@ function HostReservationWidget(p: {
|
|||||||
value={p.host.mac}
|
value={p.host.mac}
|
||||||
onValueChange={(v) => {
|
onValueChange={(v) => {
|
||||||
p.host.mac = v!;
|
p.host.mac = v!;
|
||||||
p.onChange?.();
|
p.onChange();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -142,7 +143,7 @@ function HostReservationWidget(p: {
|
|||||||
value={p.host.ip}
|
value={p.host.ip}
|
||||||
onValueChange={(v) => {
|
onValueChange={(v) => {
|
||||||
p.host.ip = v!;
|
p.host.ip = v!;
|
||||||
p.onChange?.();
|
p.onChange();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ export function NetNatConfiguration(p: {
|
|||||||
<>
|
<>
|
||||||
{p.nat.map((e, num) => (
|
{p.nat.map((e, num) => (
|
||||||
<NatEntryForm
|
<NatEntryForm
|
||||||
|
// eslint-disable-next-line react-x/no-array-index-key
|
||||||
key={num}
|
key={num}
|
||||||
{...p}
|
{...p}
|
||||||
entry={e}
|
entry={e}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export function PortInput(p: {
|
|||||||
value={p.value?.toString() ?? ""}
|
value={p.value?.toString() ?? ""}
|
||||||
type="number"
|
type="number"
|
||||||
onValueChange={(v) => {
|
onValueChange={(v) => {
|
||||||
p.onChange?.(sanitizePort(v));
|
p.onChange(sanitizePort(v));
|
||||||
}}
|
}}
|
||||||
checkValue={(v) => Number(v) <= 65535}
|
checkValue={(v) => Number(v) <= 65535}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -24,10 +24,13 @@ export function RadioGroupInput(p: {
|
|||||||
<RadioGroup
|
<RadioGroup
|
||||||
row
|
row
|
||||||
value={p.value}
|
value={p.value}
|
||||||
onChange={(_ev, v) => p.onValueChange?.(v)}
|
onChange={(_ev, v) => {
|
||||||
|
p.onValueChange(v);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{p.options.map((o) => (
|
{p.options.map((o) => (
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
|
key={o.value}
|
||||||
disabled={!p.editable}
|
disabled={!p.editable}
|
||||||
value={o.value}
|
value={o.value}
|
||||||
control={<Radio />}
|
control={<Radio />}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export function SelectInput(p: {
|
|||||||
<Select
|
<Select
|
||||||
value={p.value ?? ""}
|
value={p.value ?? ""}
|
||||||
label={p.label}
|
label={p.label}
|
||||||
onChange={(e) => p.onValueChange(e.target.value)}
|
onChange={(e) => { p.onValueChange(e.target.value); }}
|
||||||
>
|
>
|
||||||
{p.options.map((e) => (
|
{p.options.map((e) => (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { TextField } from "@mui/material";
|
|||||||
import { LenConstraint } from "../../api/ServerApi";
|
import { LenConstraint } from "../../api/ServerApi";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Couple / Member property edition
|
* Text input property edition
|
||||||
*/
|
*/
|
||||||
export function TextInput(p: {
|
export function TextInput(p: {
|
||||||
label?: string;
|
label?: string;
|
||||||
@@ -42,12 +42,14 @@ export function TextInput(p: {
|
|||||||
e.target.value.length === 0 ? undefined : e.target.value
|
e.target.value.length === 0 ? undefined : e.target.value
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
inputProps={{
|
slotProps={{
|
||||||
maxLength: p.size?.max,
|
htmlInput: {
|
||||||
}}
|
maxLength: p.size?.max,
|
||||||
InputProps={{
|
},
|
||||||
readOnly: !p.editable,
|
input: {
|
||||||
type: p.type,
|
readOnly: !p.editable,
|
||||||
|
type: p.type,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
variant={"standard"}
|
variant={"standard"}
|
||||||
style={p.style ?? { width: "100%", marginBottom: "15px" }}
|
style={p.style ?? { width: "100%", marginBottom: "15px" }}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export function VMDisksList(p: {
|
|||||||
{/* disks list */}
|
{/* disks list */}
|
||||||
{p.vm.disks.map((d, num) => (
|
{p.vm.disks.map((d, num) => (
|
||||||
<DiskInfo
|
<DiskInfo
|
||||||
|
// eslint-disable-next-line react-x/no-array-index-key
|
||||||
key={num}
|
key={num}
|
||||||
editable={p.editable}
|
editable={p.editable}
|
||||||
disk={d}
|
disk={d}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable react-x/no-array-index-key */
|
||||||
import DeleteIcon from "@mui/icons-material/Delete";
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* 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";
|
||||||
@@ -51,7 +52,6 @@ export function VMNetworksList(p: {
|
|||||||
{p.vm.networks.map((n, num) => (
|
{p.vm.networks.map((n, num) => (
|
||||||
<EditSection key={num}>
|
<EditSection key={num}>
|
||||||
<NetworkInfoWidget
|
<NetworkInfoWidget
|
||||||
key={num}
|
|
||||||
network={n}
|
network={n}
|
||||||
removeFromList={() => {
|
removeFromList={() => {
|
||||||
p.vm.networks.splice(num, 1);
|
p.vm.networks.splice(num, 1);
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export function VMSelectIsoInput(p: {
|
|||||||
attachedISOs: string[];
|
attachedISOs: string[];
|
||||||
onChange: (newVal: string[]) => void;
|
onChange: (newVal: string[]) => void;
|
||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
if (!p.attachedISOs && !p.editable) return <></>;
|
if (p.attachedISOs.length === 0 && !p.editable) return <></>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -27,7 +27,7 @@ export function VMSelectIsoInput(p: {
|
|||||||
const iso = p.isoList.find((d) => d.filename === isoName);
|
const iso = p.isoList.find((d) => d.filename === isoName);
|
||||||
return (
|
return (
|
||||||
<ListItem
|
<ListItem
|
||||||
key={num}
|
key={isoName}
|
||||||
secondaryAction={
|
secondaryAction={
|
||||||
p.editable && (
|
p.editable && (
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -69,12 +69,11 @@ export function VMSelectIsoInput(p: {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
options={p.isoList.map((i) => {
|
options={p.isoList.map((i) => {
|
||||||
return {
|
return {
|
||||||
label: `${i.filename} ${filesize(i.size)}`,
|
label: `${i.filename} ${filesize(i.size)}`,
|
||||||
value: i.filename,
|
value: i.filename,
|
||||||
};
|
};
|
||||||
})
|
})}
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ interface DetailsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function NetworkDetails(p: DetailsProps): React.ReactElement {
|
export function NetworkDetails(p: DetailsProps): React.ReactElement {
|
||||||
const [nicsList, setNicsList] = React.useState<string[] | any>();
|
const [nicsList, setNicsList] = React.useState<string[] | undefined>();
|
||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
setNicsList(await ServerApi.GetNetworksList());
|
setNicsList(await ServerApi.GetNetworksList());
|
||||||
@@ -36,7 +36,7 @@ export function NetworkDetails(p: DetailsProps): React.ReactElement {
|
|||||||
loadKey={"1"}
|
loadKey={"1"}
|
||||||
load={load}
|
load={load}
|
||||||
errMsg="Failed to load the list of host network cards!"
|
errMsg="Failed to load the list of host network cards!"
|
||||||
build={() => <NetworkDetailsInner nicsList={nicsList} {...p} />}
|
build={() => <NetworkDetailsInner nicsList={nicsList!} {...p} />}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -260,7 +260,7 @@ function IPSection(p: {
|
|||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
|
|
||||||
const toggleNetwork = async () => {
|
const toggleNetwork = async () => {
|
||||||
if (!!p.config) {
|
if (p.config) {
|
||||||
if (
|
if (
|
||||||
!(await confirm(
|
!(await confirm(
|
||||||
`Do you really want to disable IPv${p.version} on this network? Specific configuration will be deleted!`
|
`Do you really want to disable IPv${p.version} on this network? Specific configuration will be deleted!`
|
||||||
@@ -268,11 +268,11 @@ function IPSection(p: {
|
|||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
p.onChange?.(undefined);
|
p.onChange(undefined);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
p.onChange?.({
|
p.onChange({
|
||||||
bridge_address: p.version === 4 ? "192.168.1.1" : "fd00::1",
|
bridge_address: p.version === 4 ? "192.168.1.1" : "fd00::1",
|
||||||
prefix: p.version === 4 ? 24 : 8,
|
prefix: p.version === 4 ? 24 : 8,
|
||||||
});
|
});
|
||||||
@@ -298,7 +298,7 @@ function IPSection(p: {
|
|||||||
p.config!.dhcp = undefined;
|
p.config!.dhcp = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
p.onChange?.(p.config);
|
p.onChange(p.config);
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleNAT = async (v: boolean) => {
|
const toggleNAT = async (v: boolean) => {
|
||||||
@@ -306,7 +306,7 @@ function IPSection(p: {
|
|||||||
p.config!.nat = [];
|
p.config!.nat = [];
|
||||||
} else {
|
} else {
|
||||||
if (
|
if (
|
||||||
(p.config?.nat?.length ?? 0 > 0) &&
|
(p.config?.nat?.length ?? 0) > 0 &&
|
||||||
!(await confirm(
|
!(await confirm(
|
||||||
`Do you really want to disable IPv${p.version} NAT port forwarding on this network? Specific configuration will be deleted!`
|
`Do you really want to disable IPv${p.version} NAT port forwarding on this network? Specific configuration will be deleted!`
|
||||||
))
|
))
|
||||||
@@ -315,7 +315,7 @@ function IPSection(p: {
|
|||||||
p.config!.nat = undefined;
|
p.config!.nat = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
p.onChange?.(p.config);
|
p.onChange(p.config);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!p.config && !p.editable) return <></>;
|
if (!p.config && !p.editable) return <></>;
|
||||||
@@ -338,10 +338,10 @@ function IPSection(p: {
|
|||||||
editable={p.editable}
|
editable={p.editable}
|
||||||
label="Bridge address"
|
label="Bridge address"
|
||||||
version={p.version}
|
version={p.version}
|
||||||
value={p.config?.bridge_address}
|
value={p.config.bridge_address}
|
||||||
onValueChange={(v) => {
|
onValueChange={(v) => {
|
||||||
p.config!.bridge_address = v ?? "";
|
p.config!.bridge_address = v ?? "";
|
||||||
p.onChange?.(p.config);
|
p.onChange(p.config);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -352,7 +352,7 @@ function IPSection(p: {
|
|||||||
type="number"
|
type="number"
|
||||||
onValueChange={(v) => {
|
onValueChange={(v) => {
|
||||||
p.config!.prefix = Number(v);
|
p.config!.prefix = Number(v);
|
||||||
p.onChange?.(p.config);
|
p.onChange(p.config);
|
||||||
}}
|
}}
|
||||||
size={
|
size={
|
||||||
p.version === 4 ? { min: 0, max: 32 } : { min: 0, max: 128 }
|
p.version === 4 ? { min: 0, max: 32 } : { min: 0, max: 128 }
|
||||||
@@ -407,7 +407,7 @@ function IPSection(p: {
|
|||||||
dhcp={p.config.dhcp}
|
dhcp={p.config.dhcp}
|
||||||
onChange={(d) => {
|
onChange={(d) => {
|
||||||
p.config!.dhcp = d;
|
p.config!.dhcp = d;
|
||||||
p.onChange?.(p.config);
|
p.onChange(p.config);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</EditSection>
|
</EditSection>
|
||||||
@@ -431,7 +431,7 @@ function IPSection(p: {
|
|||||||
nat={p.config.nat}
|
nat={p.config.nat}
|
||||||
onChange={(n) => {
|
onChange={(n) => {
|
||||||
p.config!.nat = n;
|
p.config!.nat = n;
|
||||||
p.onChange?.(p.config);
|
p.onChange(p.config);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -29,13 +29,13 @@ export function NetworkStatusWidget(p: {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const changedAction = () => setState(undefined);
|
const changedAction = () => { setState(undefined); };
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
refresh();
|
refresh();
|
||||||
const i = setInterval(() => refresh(), 3000);
|
const i = setInterval(() => refresh(), 3000);
|
||||||
|
|
||||||
return () => clearInterval(i);
|
return () => { clearInterval(i); };
|
||||||
});
|
});
|
||||||
|
|
||||||
if (state === undefined)
|
if (state === undefined)
|
||||||
|
|||||||
@@ -28,7 +28,9 @@ interface DetailsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function NWFilterDetails(p: DetailsProps): ReactElement {
|
export function NWFilterDetails(p: DetailsProps): ReactElement {
|
||||||
const [nwFiltersList, setNwFiltersList] = React.useState<NWFilter[] | any>();
|
const [nwFiltersList, setNwFiltersList] = React.useState<
|
||||||
|
NWFilter[] | undefined
|
||||||
|
>();
|
||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
setNwFiltersList(await NWFilterApi.GetList());
|
setNwFiltersList(await NWFilterApi.GetList());
|
||||||
@@ -40,7 +42,7 @@ export function NWFilterDetails(p: DetailsProps): ReactElement {
|
|||||||
load={load}
|
load={load}
|
||||||
errMsg="Failed to load the list of network filters!"
|
errMsg="Failed to load the list of network filters!"
|
||||||
build={() => (
|
build={() => (
|
||||||
<NetworkFilterDetailsInner nwFiltersList={nwFiltersList} {...p} />
|
<NetworkFilterDetailsInner nwFiltersList={nwFiltersList!} {...p} />
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -116,7 +118,7 @@ function NetworkFilterDetailsTabGeneral(
|
|||||||
p.nwfilter.name = v ?? "";
|
p.nwfilter.name = v ?? "";
|
||||||
p.onChange?.();
|
p.onChange?.();
|
||||||
}}
|
}}
|
||||||
checkValue={(v) => /^[a-zA-Z0-9\_\-]+$/.test(v)}
|
checkValue={(v) => /^[a-zA-Z0-9_-]+$/.test(v)}
|
||||||
size={ServerApi.Config.constraints.nwfilter_name_size}
|
size={ServerApi.Config.constraints.nwfilter_name_size}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -161,14 +161,14 @@ function APITokenTabGeneral(p: DetailsInnerProps): React.ReactElement {
|
|||||||
{p.status === TokenWidgetStatus.Create && (
|
{p.status === TokenWidgetStatus.Create && (
|
||||||
<RadioGroupInput
|
<RadioGroupInput
|
||||||
{...p}
|
{...p}
|
||||||
editable={p.status === TokenWidgetStatus.Create}
|
editable={true}
|
||||||
options={[
|
options={[
|
||||||
{ label: "IPv4", value: "4" },
|
{ label: "IPv4", value: "4" },
|
||||||
{ label: "IPv6", value: "6" },
|
{ label: "IPv6", value: "6" },
|
||||||
]}
|
]}
|
||||||
value={ipVersion.toString()}
|
value={ipVersion.toString()}
|
||||||
onValueChange={(v) => {
|
onValueChange={(v) => {
|
||||||
setIpVersion(Number(v) as any);
|
setIpVersion(Number(v) as 4 | 6);
|
||||||
}}
|
}}
|
||||||
label="Token IP restriction version"
|
label="Token IP restriction version"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ export function TokenRawRightsEditor(p: {
|
|||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{p.token.rights.map((r, num) => (
|
{p.token.rights.map((r, num) => (
|
||||||
|
// eslint-disable-next-line react-x/no-array-index-key
|
||||||
<TableRow key={num} hover>
|
<TableRow key={num} hover>
|
||||||
<TableCell style={{ width: "100px" }}>
|
<TableCell style={{ width: "100px" }}>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
@@ -95,7 +96,11 @@ export function TokenRawRightsEditor(p: {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
{p.editable && (
|
{p.editable && (
|
||||||
<TableCell style={{ width: "100px" }}>
|
<TableCell style={{ width: "100px" }}>
|
||||||
<IconButton onClick={() => deleteRule(num)}>
|
<IconButton
|
||||||
|
onClick={() => {
|
||||||
|
deleteRule(num);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Tooltip title="Remove the rule">
|
<Tooltip title="Remove the rule">
|
||||||
<DeleteIcon />
|
<DeleteIcon />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -85,8 +85,8 @@ export function TokenRightsEditor(p: {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
|
|
||||||
{/* Per VM operations */}
|
{/* Per VM operations */}
|
||||||
{p.vms.map((v, n) => (
|
{p.vms.map((v) => (
|
||||||
<TableRow hover key={n}>
|
<TableRow hover key={v.uuid}>
|
||||||
<TableCell>{v.name}</TableCell>
|
<TableCell>{v.name}</TableCell>
|
||||||
<CellRight
|
<CellRight
|
||||||
{...p}
|
{...p}
|
||||||
@@ -185,8 +185,8 @@ export function TokenRightsEditor(p: {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
|
|
||||||
{/* Per VM operations */}
|
{/* Per VM operations */}
|
||||||
{p.vms.map((v, n) => (
|
{p.vms.map((v) => (
|
||||||
<TableRow hover key={n}>
|
<TableRow hover key={v.uuid}>
|
||||||
<TableCell>{v.name}</TableCell>
|
<TableCell>{v.name}</TableCell>
|
||||||
<CellRight
|
<CellRight
|
||||||
{...p}
|
{...p}
|
||||||
@@ -306,8 +306,8 @@ export function TokenRightsEditor(p: {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
|
|
||||||
{/* Per VM operations */}
|
{/* Per VM operations */}
|
||||||
{p.groups.map((v, n) => (
|
{p.groups.map((v) => (
|
||||||
<TableRow hover key={n}>
|
<TableRow hover key={v}>
|
||||||
<TableCell>{v}</TableCell>
|
<TableCell>{v}</TableCell>
|
||||||
<CellRight
|
<CellRight
|
||||||
{...p}
|
{...p}
|
||||||
@@ -448,8 +448,8 @@ export function TokenRightsEditor(p: {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
|
|
||||||
{/* Per network operations */}
|
{/* Per network operations */}
|
||||||
{p.networks.map((v, n) => (
|
{p.networks.map((v) => (
|
||||||
<TableRow hover key={n}>
|
<TableRow hover key={v.uuid}>
|
||||||
<TableCell>{v.name}</TableCell>
|
<TableCell>{v.name}</TableCell>
|
||||||
<CellRight
|
<CellRight
|
||||||
{...p}
|
{...p}
|
||||||
@@ -568,15 +568,15 @@ export function TokenRightsEditor(p: {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
|
|
||||||
{/* Per network filter operations */}
|
{/* Per network filter operations */}
|
||||||
{p.nwFilters.map((v, n) => (
|
{p.nwFilters.map((v) => (
|
||||||
<TableRow hover key={n}>
|
<TableRow hover key={v.uuid}>
|
||||||
<TableCell>{v.name}</TableCell>
|
<TableCell>{v.name}</TableCell>
|
||||||
<CellRight
|
<CellRight
|
||||||
{...p}
|
{...p}
|
||||||
right={{ verb: "GET", path: `/api/nwfilter/${v.uuid}` }}
|
right={{ verb: "GET", path: `/api/nwfilter/${v.uuid}` }}
|
||||||
parent={{ verb: "GET", path: "/api/nwfilter/*" }}
|
parent={{ verb: "GET", path: "/api/nwfilter/*" }}
|
||||||
/>
|
/>
|
||||||
{ServerApi.Config.builtin_nwfilter_rules.includes(v.name!) ? (
|
{ServerApi.Config.builtin_nwfilter_rules.includes(v.name) ? (
|
||||||
<TableCell></TableCell>
|
<TableCell></TableCell>
|
||||||
) : (
|
) : (
|
||||||
<CellRight
|
<CellRight
|
||||||
@@ -585,7 +585,7 @@ export function TokenRightsEditor(p: {
|
|||||||
parent={{ verb: "PUT", path: "/api/nwfilter/*" }}
|
parent={{ verb: "PUT", path: "/api/nwfilter/*" }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{ServerApi.Config.builtin_nwfilter_rules.includes(v.name!) ? (
|
{ServerApi.Config.builtin_nwfilter_rules.includes(v.name) ? (
|
||||||
<TableCell></TableCell>
|
<TableCell></TableCell>
|
||||||
) : (
|
) : (
|
||||||
<CellRight
|
<CellRight
|
||||||
@@ -645,8 +645,8 @@ export function TokenRightsEditor(p: {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
|
|
||||||
{/* Per API token operations */}
|
{/* Per API token operations */}
|
||||||
{p.tokens.map((v, n) => (
|
{p.tokens.map((v) => (
|
||||||
<TableRow hover key={n}>
|
<TableRow hover key={v.id}>
|
||||||
<TableCell>{v.name}</TableCell>
|
<TableCell>{v.name}</TableCell>
|
||||||
<CellRight
|
<CellRight
|
||||||
{...p}
|
{...p}
|
||||||
@@ -767,7 +767,7 @@ function RouteRight(p: RightOpts): React.ReactElement {
|
|||||||
const parentActivated =
|
const parentActivated =
|
||||||
!!p.parent &&
|
!!p.parent &&
|
||||||
p.token.rights.findIndex(
|
p.token.rights.findIndex(
|
||||||
(r) => r.verb === p.parent?.verb && r.path === p.parent?.path
|
(r) => r.verb === p.parent?.verb && r.path === p.parent.path
|
||||||
) !== -1;
|
) !== -1;
|
||||||
|
|
||||||
const toggle = (a: boolean) => {
|
const toggle = (a: boolean) => {
|
||||||
@@ -804,7 +804,9 @@ function RouteRight(p: RightOpts): React.ReactElement {
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
checked={activated || parentActivated}
|
checked={activated || parentActivated}
|
||||||
disabled={!p.editable || parentActivated}
|
disabled={!p.editable || parentActivated}
|
||||||
onChange={(_e, a) => toggle(a)}
|
onChange={(_e, a) => {
|
||||||
|
toggle(a);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label={p.label}
|
label={p.label}
|
||||||
@@ -814,7 +816,9 @@ function RouteRight(p: RightOpts): React.ReactElement {
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
checked={activated || parentActivated}
|
checked={activated || parentActivated}
|
||||||
disabled={!p.editable || parentActivated}
|
disabled={!p.editable || parentActivated}
|
||||||
onChange={(_e, a) => toggle(a)}
|
onChange={(_e, a) => {
|
||||||
|
toggle(a);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -35,14 +35,16 @@ interface DetailsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function VMDetails(p: DetailsProps): React.ReactElement {
|
export function VMDetails(p: DetailsProps): React.ReactElement {
|
||||||
const [groupsList, setGroupsList] = React.useState<string[] | any>();
|
const [groupsList, setGroupsList] = React.useState<string[] | undefined>();
|
||||||
const [isoList, setIsoList] = React.useState<IsoFile[] | any>();
|
const [isoList, setIsoList] = React.useState<IsoFile[] | undefined>();
|
||||||
const [vcpuCombinations, setVCPUCombinations] = React.useState<
|
const [vcpuCombinations, setVCPUCombinations] = React.useState<
|
||||||
number[] | any
|
number[] | undefined
|
||||||
|
>();
|
||||||
|
const [networksList, setNetworksList] = React.useState<
|
||||||
|
NetworkInfo[] | undefined
|
||||||
>();
|
>();
|
||||||
const [networksList, setNetworksList] = React.useState<NetworkInfo[] | any>();
|
|
||||||
const [networkFiltersList, setNetworkFiltersList] = React.useState<
|
const [networkFiltersList, setNetworkFiltersList] = React.useState<
|
||||||
NWFilter[] | any
|
NWFilter[] | undefined
|
||||||
>();
|
>();
|
||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
@@ -60,11 +62,11 @@ export function VMDetails(p: DetailsProps): React.ReactElement {
|
|||||||
errMsg="Failed to load the list of ISO files"
|
errMsg="Failed to load the list of ISO files"
|
||||||
build={() => (
|
build={() => (
|
||||||
<VMDetailsInner
|
<VMDetailsInner
|
||||||
groupsList={groupsList}
|
groupsList={groupsList!}
|
||||||
isoList={isoList}
|
isoList={isoList!}
|
||||||
vcpuCombinations={vcpuCombinations}
|
vcpuCombinations={vcpuCombinations!}
|
||||||
networksList={networksList}
|
networksList={networksList!}
|
||||||
networkFiltersList={networkFiltersList}
|
networkFiltersList={networkFiltersList!}
|
||||||
{...p}
|
{...p}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -202,7 +204,7 @@ function VMDetailsTabGeneral(p: DetailsInnerProps): React.ReactElement {
|
|||||||
editable={p.editable}
|
editable={p.editable}
|
||||||
label="Group"
|
label="Group"
|
||||||
onValueChange={(v) => {
|
onValueChange={(v) => {
|
||||||
p.vm.group = v! as any;
|
p.vm.group = v!;
|
||||||
p.onChange?.();
|
p.onChange?.();
|
||||||
}}
|
}}
|
||||||
value={p.vm.group}
|
value={p.vm.group}
|
||||||
@@ -222,7 +224,11 @@ function VMDetailsTabGeneral(p: DetailsInnerProps): React.ReactElement {
|
|||||||
: "Add a new group instead of using existing one"
|
: "Add a new group instead of using existing one"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<IconButton onClick={() => setAddGroup(!addGroup)}>
|
<IconButton
|
||||||
|
onClick={() => {
|
||||||
|
setAddGroup(!addGroup);
|
||||||
|
}}
|
||||||
|
>
|
||||||
{addGroup ? <ListIcon /> : <AddIcon />}
|
{addGroup ? <ListIcon /> : <AddIcon />}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export function VMScreenshot(p: { vm: VMInfo }): React.ReactElement {
|
|||||||
string | undefined
|
string | undefined
|
||||||
>();
|
>();
|
||||||
|
|
||||||
const int = React.useRef<any | undefined>(undefined);
|
const int = React.useRef<NodeJS.Timeout | undefined>(undefined);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const refresh = async () => {
|
const refresh = async () => {
|
||||||
@@ -25,7 +25,9 @@ export function VMScreenshot(p: { vm: VMInfo }): React.ReactElement {
|
|||||||
|
|
||||||
if (int.current === undefined) {
|
if (int.current === undefined) {
|
||||||
refresh();
|
refresh();
|
||||||
int.current = setInterval(() => refresh(), 5000);
|
int.current = setInterval(() => {
|
||||||
|
refresh();
|
||||||
|
}, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
@@ -31,13 +31,19 @@ export function VMStatusWidget(p: {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const changedAction = () => setState(undefined);
|
const changedAction = () => {
|
||||||
|
setState(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
refresh();
|
refresh();
|
||||||
const i = setInterval(() => refresh(), 3000);
|
const i = setInterval(() => {
|
||||||
|
refresh();
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
return () => clearInterval(i);
|
return () => {
|
||||||
|
clearInterval(i);
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
if (state === undefined)
|
if (state === undefined)
|
||||||
@@ -59,6 +65,7 @@ export function VMStatusWidget(p: {
|
|||||||
icon={<PersonalVideoIcon />}
|
icon={<PersonalVideoIcon />}
|
||||||
tooltip="Graphical remote control over the VM"
|
tooltip="Graphical remote control over the VM"
|
||||||
performAction={async () => navigate(p.vm.VNCURL)}
|
performAction={async () => navigate(p.vm.VNCURL)}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
onExecuted={() => {}}
|
onExecuted={() => {}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user