Compare commits
21 Commits
5d49ce17a6
...
20250409
| Author | SHA1 | Date | |
|---|---|---|---|
| 3c636406af | |||
| 578f1432a0 | |||
| f403c85f0a | |||
| db25c7e426 | |||
| 98b67534cb | |||
| 09d3cf08f3 | |||
| 9b3d32811f | |||
| 26d391ea96 | |||
| c044996014 | |||
| 34b04968b2 | |||
| c44a3f2673 | |||
| e46adcd1da | |||
| d1506f26ab | |||
| 23194d13d2 | |||
| 131dec892d | |||
| c2e6105aff | |||
| f5202f596d | |||
| 3bf8859ff9 | |||
| 9a905e83f7 | |||
| 4b9df95721 | |||
| e14f51ef7e |
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"
|
||||||
|
|||||||
@@ -17,10 +17,11 @@ pub const ROUTES_WITHOUT_AUTH: [&str; 5] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
/// Allowed ISO mimetypes
|
/// Allowed ISO mimetypes
|
||||||
pub const ALLOWED_ISO_MIME_TYPES: [&str; 3] = [
|
pub const ALLOWED_ISO_MIME_TYPES: [&str; 4] = [
|
||||||
"application/x-cd-image",
|
"application/x-cd-image",
|
||||||
"application/x-iso9660-image",
|
"application/x-iso9660-image",
|
||||||
"application/octet-stream",
|
"application/octet-stream",
|
||||||
|
"application/vnd.efi.iso",
|
||||||
];
|
];
|
||||||
|
|
||||||
/// ISO max size
|
/// ISO max size
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ STORAGE=/home/virtweb/storage
|
|||||||
HYPERVISOR_URI=qemu:///system
|
HYPERVISOR_URI=qemu:///system
|
||||||
```
|
```
|
||||||
|
|
||||||
> Note: `HYPERVISOR_URI=qemu:///system` is used to sepcify that we want to use the main hypervisor.
|
> Note: `HYPERVISOR_URI=qemu:///system` is used to specify that we want to use the main hypervisor.
|
||||||
|
|
||||||
## Register Virtweb service
|
## Register Virtweb service
|
||||||
Before registering service, check that the configuration works correctly:
|
Before registering service, check that the configuration works correctly:
|
||||||
|
|||||||
@@ -1,46 +1,12 @@
|
|||||||
# Getting Started with Create React App
|
# Virtweb frontend
|
||||||
|
Built with Vite + React + TypeScript
|
||||||
|
|
||||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
## Get dependencies
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
## Available Scripts
|
# Run for developpment
|
||||||
|
```bash
|
||||||
In the project directory, you can run:
|
npm run dev
|
||||||
|
```
|
||||||
### `npm start`
|
|
||||||
|
|
||||||
Runs the app in the development mode.\
|
|
||||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
|
||||||
|
|
||||||
The page will reload if you make edits.\
|
|
||||||
You will also see any lint errors in the console.
|
|
||||||
|
|
||||||
### `npm test`
|
|
||||||
|
|
||||||
Launches the test runner in the interactive watch mode.\
|
|
||||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
|
||||||
|
|
||||||
### `npm run build`
|
|
||||||
|
|
||||||
Builds the app for production to the `build` folder.\
|
|
||||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
|
||||||
|
|
||||||
The build is minified and the filenames include the hashes.\
|
|
||||||
Your app is ready to be deployed!
|
|
||||||
|
|
||||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
|
||||||
|
|
||||||
### `npm run eject`
|
|
||||||
|
|
||||||
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
|
||||||
|
|
||||||
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
|
||||||
|
|
||||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
|
||||||
|
|
||||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
|
||||||
|
|
||||||
## Learn More
|
|
||||||
|
|
||||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
|
||||||
|
|
||||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
|
||||||
54
virtweb_frontend/eslint.config.js
Normal file
54
virtweb_frontend/eslint.config.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import js from "@eslint/js";
|
||||||
|
import reactDom from "eslint-plugin-react-dom";
|
||||||
|
import reactHooks from "eslint-plugin-react-hooks";
|
||||||
|
import reactRefresh from "eslint-plugin-react-refresh";
|
||||||
|
import reactX from "eslint-plugin-react-x";
|
||||||
|
import globals from "globals";
|
||||||
|
import tseslint from "typescript-eslint";
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{ ignores: ["dist"] },
|
||||||
|
{
|
||||||
|
extends: [
|
||||||
|
js.configs.recommended,
|
||||||
|
...tseslint.configs.strictTypeChecked,
|
||||||
|
...tseslint.configs.stylisticTypeChecked,
|
||||||
|
],
|
||||||
|
files: ["**/*.{ts,tsx}"],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
parserOptions: {
|
||||||
|
project: ["./tsconfig.node.json", "./tsconfig.app.json"],
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
"react-hooks": reactHooks,
|
||||||
|
"react-refresh": reactRefresh,
|
||||||
|
"react-x": reactX,
|
||||||
|
"react-dom": reactDom,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
"react-refresh/only-export-components": [
|
||||||
|
"warn",
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
...reactX.configs["recommended-typescript"].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",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
2264
virtweb_frontend/package-lock.json
generated
2264
virtweb_frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,12 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.0",
|
"@emotion/styled": "^11.14.0",
|
||||||
@@ -13,16 +19,6 @@
|
|||||||
"@mui/material": "^7.0.0",
|
"@mui/material": "^7.0.0",
|
||||||
"@mui/x-charts": "^7.28.0",
|
"@mui/x-charts": "^7.28.0",
|
||||||
"@mui/x-data-grid": "^7.28.1",
|
"@mui/x-data-grid": "^7.28.1",
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
|
||||||
"@testing-library/react": "^16.2.0",
|
|
||||||
"@testing-library/user-event": "^14.5.2",
|
|
||||||
"@types/humanize-duration": "^3.27.1",
|
|
||||||
"@types/jest": "^29.5.14",
|
|
||||||
"@types/react": "^19.0.12",
|
|
||||||
"@types/react-dom": "^19.0.4",
|
|
||||||
"@types/react-syntax-highlighter": "^15.5.13",
|
|
||||||
"@types/uuid": "^10.0.0",
|
|
||||||
"@vitejs/plugin-react": "^4.3.3",
|
|
||||||
"date-and-time": "^3.6.0",
|
"date-and-time": "^3.6.0",
|
||||||
"filesize": "^10.1.6",
|
"filesize": "^10.1.6",
|
||||||
"humanize-duration": "^3.29.0",
|
"humanize-duration": "^3.29.0",
|
||||||
@@ -32,33 +28,26 @@
|
|||||||
"react-router-dom": "^7.4.0",
|
"react-router-dom": "^7.4.0",
|
||||||
"react-syntax-highlighter": "^15.6.1",
|
"react-syntax-highlighter": "^15.6.1",
|
||||||
"react-vnc": "^3.0.7",
|
"react-vnc": "^3.0.7",
|
||||||
"typescript": "^5.8.2",
|
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
"vite": "^6.2.3",
|
|
||||||
"vite-tsconfig-paths": "^5.1.3",
|
|
||||||
"xml-formatter": "^3.6.0"
|
"xml-formatter": "^3.6.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"devDependencies": {
|
||||||
"start": "vite",
|
"@eslint/js": "^9.21.0",
|
||||||
"build": "tsc && vite build",
|
"@types/humanize-duration": "^3.27.1",
|
||||||
"preview": "vite preview"
|
"@types/jest": "^29.5.14",
|
||||||
},
|
"@types/react": "^19.0.12",
|
||||||
"eslintConfig": {
|
"@types/react-dom": "^19.0.4",
|
||||||
"extends": [
|
"@types/react-syntax-highlighter": "^15.5.13",
|
||||||
"react-app",
|
"@types/uuid": "^10.0.0",
|
||||||
"react-app/jest"
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
]
|
"eslint": "^9.21.0",
|
||||||
},
|
"eslint-plugin-react-dom": "^1.38.3",
|
||||||
"browserslist": {
|
"eslint-plugin-react-hooks": "^5.1.0",
|
||||||
"production": [
|
"eslint-plugin-react-refresh": "^0.4.19",
|
||||||
">0.2%",
|
"eslint-plugin-react-x": "^1.38.3",
|
||||||
"not dead",
|
"globals": "^15.15.0",
|
||||||
"not op_mini all"
|
"typescript": "^5.8.2",
|
||||||
],
|
"typescript-eslint": "^8.24.1",
|
||||||
"development": [
|
"vite": "^6.2.3"
|
||||||
"last 1 chrome version",
|
|
||||||
"last 1 firefox version",
|
|
||||||
"last 1 safari version"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>}
|
||||||
@@ -147,6 +149,8 @@ function UploadIsoFileFromUrlCard(p: {
|
|||||||
loadingMessage.show("Downloading file from URL...");
|
loadingMessage.show("Downloading file from URL...");
|
||||||
await IsoFilesApi.UploadFromURL(url, actualFileName);
|
await IsoFilesApi.UploadFromURL(url, actualFileName);
|
||||||
|
|
||||||
|
p.onFileUploaded();
|
||||||
|
|
||||||
setURL("");
|
setURL("");
|
||||||
setFilename(null);
|
setFilename(null);
|
||||||
snackbar("Successfully downloaded file!");
|
snackbar("Successfully downloaded file!");
|
||||||
@@ -164,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>
|
||||||
@@ -198,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!");
|
||||||
@@ -236,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",
|
||||||
@@ -301,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,5 +0,0 @@
|
|||||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
|
||||||
// allows you to do things like:
|
|
||||||
// expect(element).toHaveTextContent(/react/i)
|
|
||||||
// learn more: https://github.com/testing-library/jest-dom
|
|
||||||
import '@testing-library/jest-dom';
|
|
||||||
@@ -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,11 +13,9 @@ 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 { isDebug } from "../utils/DebugUtils";
|
|
||||||
import { RouterLink } from "./RouterLink";
|
import { RouterLink } from "./RouterLink";
|
||||||
import { VirtWebAppBar } from "./VirtWebAppBar";
|
import { VirtWebAppBar } from "./VirtWebAppBar";
|
||||||
|
|
||||||
@@ -96,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 (
|
||||||
@@ -104,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<number | 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={() => {}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
26
virtweb_frontend/tsconfig.app.json
Normal file
26
virtweb_frontend/tsconfig.app.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
@@ -1,27 +1,7 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"files": [],
|
||||||
"lib": [
|
"references": [
|
||||||
"dom",
|
{ "path": "./tsconfig.app.json" },
|
||||||
"dom.iterable",
|
{ "path": "./tsconfig.node.json" }
|
||||||
"esnext"
|
|
||||||
],
|
|
||||||
"target": "ESNext",
|
|
||||||
"types": ["vite/client"],
|
|
||||||
"allowJs": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"strict": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"module": "esnext",
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"jsx": "react-jsx"
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"src"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
24
virtweb_frontend/tsconfig.node.json
Normal file
24
virtweb_frontend/tsconfig.node.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
@@ -1,15 +1,7 @@
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig } from 'vite'
|
||||||
import react from "@vitejs/plugin-react";
|
import react from '@vitejs/plugin-react'
|
||||||
import viteTsconfigPaths from "vite-tsconfig-paths";
|
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
// depending on your application, base can also be "/"
|
plugins: [react()],
|
||||||
base: "/",
|
})
|
||||||
plugins: [react(), viteTsconfigPaths()],
|
|
||||||
server: {
|
|
||||||
// this ensures that the browser opens upon server start
|
|
||||||
open: true,
|
|
||||||
// this sets a default port to 3000
|
|
||||||
port: 3000,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|||||||
Reference in New Issue
Block a user