18 Commits

Author SHA1 Message Date
f403c85f0a Add release configuration
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-04-07 12:31:42 +02:00
db25c7e426 Update Rust crate sysinfo to 0.34.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-04-02 00:39:29 +00:00
98b67534cb Update Rust crate rust-embed to 8.6.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-03-31 20:54:10 +00:00
09d3cf08f3 Update Rust crate quick-xml to 0.37.3
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-03-31 00:38:34 +00:00
9b3d32811f Update Rust crate num to 0.4.3
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-03-30 02:24:42 +00:00
26d391ea96 Update Rust crate log to 0.4.27
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-03-29 23:01:14 +00:00
c044996014 Update Rust crate image to 0.25.6
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-03-29 18:17:12 +00:00
34b04968b2 Update renovate.json
All checks were successful
continuous-integration/drone/push Build is passing
2025-03-29 17:30:56 +00:00
c44a3f2673 Merge pull request 'Update Rust crate sysinfo to v0.34.1' (#305) from renovate/sysinfo-0.x-lockfile into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #305
2025-03-29 12:52:01 +00:00
e46adcd1da Merge pull request 'Update Rust crate basic-jwt to 0.3.0' (#306) from renovate/basic-jwt-0.x into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #306
2025-03-29 12:51:53 +00:00
d1506f26ab Update renovate.json
All checks were successful
continuous-integration/drone/push Build is passing
2025-03-29 09:04:05 +00:00
23194d13d2 Update Rust crate basic-jwt to 0.3.0
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-03-29 00:34:32 +00:00
131dec892d Update Rust crate sysinfo to v0.34.1
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-03-29 00:34:22 +00:00
c2e6105aff Reload page when signedIn state change
All checks were successful
continuous-integration/drone/push Build is passing
2025-03-28 13:10:47 +01:00
f5202f596d Fix all ESLint errors
All checks were successful
continuous-integration/drone/push Build is passing
2025-03-28 12:25:04 +01:00
3bf8859ff9 WIP ESLint
All checks were successful
continuous-integration/drone/push Build is passing
2025-03-28 12:12:11 +01:00
9a905e83f7 WIP ESLint fixes 2025-03-28 11:35:51 +01:00
4b9df95721 Remove deprecated code 2025-03-28 11:29:31 +01:00
61 changed files with 308 additions and 204 deletions

View File

@@ -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: {}

View File

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

View File

@@ -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",

View File

@@ -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"

View File

@@ -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",
}, },
} }
); );

View File

@@ -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)!;
} }

View File

@@ -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]);
} }

View File

@@ -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!}`,
}); });
} }
} }

View File

@@ -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" : ""}`;
} }

View File

@@ -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" : ""}`;
} }

View File

@@ -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)!;
} }

View File

@@ -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)!;
} }

View File

@@ -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)!;
} }

View File

@@ -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)!;
} }

View File

@@ -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>

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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>
</> </>

View File

@@ -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>

View File

@@ -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

View File

@@ -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>
); );

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">

View File

@@ -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");

View File

@@ -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))

View File

@@ -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>
) )
); );

View File

@@ -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>
); );

View File

@@ -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) {

View File

@@ -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}`);
} }
}; };

View File

@@ -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 }} />

View File

@@ -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}

View File

@@ -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";

View File

@@ -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);

View File

@@ -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);

View File

@@ -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 },

View File

@@ -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";

View File

@@ -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"

View File

@@ -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>

View File

@@ -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={() => {

View File

@@ -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>

View File

@@ -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}

View File

@@ -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}
/> />

View File

@@ -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 />}

View File

@@ -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

View File

@@ -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={{
htmlInput: {
maxLength: p.size?.max, maxLength: p.size?.max,
}} },
InputProps={{ input: {
readOnly: !p.editable, readOnly: !p.editable,
type: p.type, type: p.type,
},
}} }}
variant={"standard"} variant={"standard"}
style={p.style ?? { width: "100%", marginBottom: "15px" }} style={p.style ?? { width: "100%", marginBottom: "15px" }}

View File

@@ -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}

View File

@@ -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,

View File

@@ -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);

View File

@@ -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
@@ -73,8 +73,7 @@ export function VMSelectIsoInput(p: {
label: `${i.filename} ${filesize(i.size)}`, label: `${i.filename} ${filesize(i.size)}`,
value: i.filename, value: i.filename,
}; };
}) })}
}
/> />
</> </>
); );

View File

@@ -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);
}} }}
/> />
)} )}

View File

@@ -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)

View File

@@ -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}
/> />

View File

@@ -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"
/> />

View File

@@ -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>

View File

@@ -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>
)} )}

View File

@@ -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>

View File

@@ -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 () => {

View File

@@ -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={() => {}}
/> />
) )