17 Commits

Author SHA1 Message Date
3c636406af Update virtweb_backend/src/constants.rs
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-04-09 09:25:28 +00:00
578f1432a0 Fix typo
All checks were successful
continuous-integration/drone/push Build is passing
2025-04-07 11:14:12 +00:00
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
27 changed files with 116 additions and 66 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

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

View File

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

View File

@@ -46,6 +46,7 @@ export default tseslint.config(
"@typescript-eslint/no-unsafe-assignment": "off", "@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-return": "off", "@typescript-eslint/no-unsafe-return": "off",
"@typescript-eslint/no-unsafe-call": "off", "@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/no-unsafe-argument": "off", "@typescript-eslint/no-unsafe-argument": "off",
"react-refresh/only-export-components": "off", "react-refresh/only-export-components": "off",
}, },

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(

View File

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

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

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

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,

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

@@ -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,7 +125,9 @@ function VMListWidget(p: {
> >
<IconButton <IconButton
size="small" size="small"
onClick={() => { toggleHiddenGroup(g); }} onClick={() => {
toggleHiddenGroup(g);
}}
> >
{!hiddenGroups.has(g) ? ( {!hiddenGroups.has(g) ? (
<KeyboardArrowUpIcon /> <KeyboardArrowUpIcon />
@@ -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);
};
} }
}); });
@@ -143,7 +145,9 @@ function VNCInner(p: { vm: VMInfo }): React.ReactElement {
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

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

View File

@@ -31,9 +31,11 @@ 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;

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

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

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

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

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

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