Compare commits
17 Commits
3bf8859ff9
...
20250409
| Author | SHA1 | Date | |
|---|---|---|---|
| 3c636406af | |||
| 578f1432a0 | |||
| f403c85f0a | |||
| db25c7e426 | |||
| 98b67534cb | |||
| 09d3cf08f3 | |||
| 9b3d32811f | |||
| 26d391ea96 | |||
| c044996014 | |||
| 34b04968b2 | |||
| c44a3f2673 | |||
| e46adcd1da | |||
| d1506f26ab | |||
| 23194d13d2 | |||
| 131dec892d | |||
| c2e6105aff | |||
| f5202f596d |
23
.drone.yml
23
.drone.yml
@@ -12,6 +12,7 @@ steps:
|
||||
commands:
|
||||
- cd virtweb_frontend
|
||||
- npm install --legacy-peer-deps # TODO : remove when mui-file-input is updated
|
||||
- npm run lint
|
||||
- npm run build
|
||||
- mv dist /tmp/web_build
|
||||
|
||||
@@ -35,6 +36,8 @@ steps:
|
||||
path: /usr/local/cargo/registry
|
||||
- name: web_app
|
||||
path: /tmp/web_build
|
||||
- name: release
|
||||
path: /tmp/release
|
||||
depends_on:
|
||||
- backend_check
|
||||
- web_build
|
||||
@@ -44,10 +47,30 @@ steps:
|
||||
- mv /tmp/web_build/dist static
|
||||
- cargo build --release
|
||||
- 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:
|
||||
- name: rust_registry
|
||||
temp: {}
|
||||
- name: web_app
|
||||
temp: {}
|
||||
- name: release
|
||||
temp: {}
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"packageRules": [
|
||||
{
|
||||
"matchUpdateTypes": ["major", "minor", "patch"],
|
||||
"automerge": true
|
||||
}
|
||||
]
|
||||
"extends": ["local>renovate/presets"]
|
||||
}
|
||||
10
virtweb_backend/Cargo.lock
generated
10
virtweb_backend/Cargo.lock
generated
@@ -590,15 +590,15 @@ checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3"
|
||||
|
||||
[[package]]
|
||||
name = "basic-jwt"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "741afb780192f091b1ceebdc794540a956f3eb96628939f83c5d15e0cb98fa71"
|
||||
checksum = "1b87da415ea2a5c22d90143d39a2db6c0c910d3d01bfd19d0f50369de91215f0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"elliptic-curve",
|
||||
"jsonwebtoken",
|
||||
"p384",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.0",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@@ -3261,9 +3261,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sysinfo"
|
||||
version = "0.34.0"
|
||||
version = "0.34.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "36913edff7a70c19d204c379e484b2a04d121fbb4e68bebc9fea651eb4386397"
|
||||
checksum = "a4b93974b3d3aeaa036504b8eefd4c039dced109171c1ae973f1dc63b2c7e4b2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"memchr",
|
||||
|
||||
@@ -6,7 +6,7 @@ edition = "2024"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
log = "0.4.26"
|
||||
log = "0.4.27"
|
||||
env_logger = "0.11.7"
|
||||
clap = { version = "4.5.34", features = ["derive", "env"] }
|
||||
light-openid = { version = "1.0.4", features = ["crypto-wrapper"] }
|
||||
@@ -22,7 +22,7 @@ actix-ws = "0.3.0"
|
||||
actix-http = "3.10.0"
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
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"
|
||||
anyhow = "1.0.97"
|
||||
actix-multipart = "0.7.2"
|
||||
@@ -30,19 +30,19 @@ tempfile = "3.19.1"
|
||||
reqwest = { version = "0.12.15", features = ["stream"] }
|
||||
url = "2.5.4"
|
||||
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"] }
|
||||
lazy-regex = "3.4.1"
|
||||
thiserror = "2.0.12"
|
||||
image = "0.25.5"
|
||||
image = "0.25.6"
|
||||
rand = "0.9.0"
|
||||
bytes = "1.10.1"
|
||||
tokio = { version = "1.44.1", features = ["rt", "time", "macros"] }
|
||||
futures = "0.3.31"
|
||||
ipnetwork = { version = "0.21.1", features = ["serde"] }
|
||||
num = "0.4.2"
|
||||
rust-embed = { version = "8.5.0" }
|
||||
num = "0.4.3"
|
||||
rust-embed = { version = "8.6.0" }
|
||||
mime_guess = "2.0.5"
|
||||
dotenvy = "0.15.7"
|
||||
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
|
||||
pub const ALLOWED_ISO_MIME_TYPES: [&str; 3] = [
|
||||
pub const ALLOWED_ISO_MIME_TYPES: [&str; 4] = [
|
||||
"application/x-cd-image",
|
||||
"application/x-iso9660-image",
|
||||
"application/octet-stream",
|
||||
"application/vnd.efi.iso",
|
||||
];
|
||||
|
||||
/// ISO max size
|
||||
|
||||
@@ -61,7 +61,7 @@ STORAGE=/home/virtweb/storage
|
||||
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
|
||||
Before registering service, check that the configuration works correctly:
|
||||
|
||||
@@ -46,6 +46,7 @@ export default tseslint.config(
|
||||
"@typescript-eslint/no-unsafe-assignment": "off",
|
||||
"@typescript-eslint/no-unsafe-return": "off",
|
||||
"@typescript-eslint/no-unsafe-call": "off",
|
||||
"@typescript-eslint/no-unsafe-member-access": "off",
|
||||
"@typescript-eslint/no-unsafe-argument": "off",
|
||||
"react-refresh/only-export-components": "off",
|
||||
},
|
||||
|
||||
@@ -51,7 +51,10 @@ export function App() {
|
||||
|
||||
const context: AuthContext = {
|
||||
signedIn: signedIn,
|
||||
setSignedIn: (s) => { setSignedIn(s); },
|
||||
setSignedIn: (s) => {
|
||||
setSignedIn(s);
|
||||
location.reload();
|
||||
},
|
||||
};
|
||||
|
||||
const router = createBrowserRouter(
|
||||
|
||||
@@ -66,22 +66,25 @@ export class APIClient {
|
||||
if (args.upProgress) {
|
||||
const res: XMLHttpRequest = await new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.upload.addEventListener("progress", (e) =>
|
||||
{ args.upProgress!(e.loaded / e.total); }
|
||||
);
|
||||
xhr.addEventListener("load", () => { resolve(xhr); });
|
||||
xhr.addEventListener("error", () =>
|
||||
{ reject(new Error("File upload failed")); }
|
||||
);
|
||||
xhr.addEventListener("abort", () =>
|
||||
{ reject(new Error("File upload aborted")); }
|
||||
);
|
||||
xhr.addEventListener("timeout", () =>
|
||||
{ reject(new Error("File upload timeout")); }
|
||||
);
|
||||
xhr.upload.addEventListener("progress", (e) => {
|
||||
args.upProgress!(e.loaded / e.total);
|
||||
});
|
||||
xhr.addEventListener("load", () => {
|
||||
resolve(xhr);
|
||||
});
|
||||
xhr.addEventListener("error", () => {
|
||||
reject(new Error("File upload failed"));
|
||||
});
|
||||
xhr.addEventListener("abort", () => {
|
||||
reject(new Error("File upload aborted"));
|
||||
});
|
||||
xhr.addEventListener("timeout", () => {
|
||||
reject(new Error("File upload timeout"));
|
||||
});
|
||||
xhr.open(args.method, url, true);
|
||||
xhr.withCredentials = true;
|
||||
for (const key in headers) {
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
if (headers.hasOwnProperty(key))
|
||||
xhr.setRequestHeader(key, headers[key]);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ export function CreateVMRoute(): React.ReactElement {
|
||||
const alert = useAlert();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [vm, setVM] = React.useState(VMInfo.NewEmpty);
|
||||
const [vm, setVM] = React.useState(VMInfo.NewEmpty());
|
||||
|
||||
const create = async (v: VMInfo) => {
|
||||
try {
|
||||
@@ -103,7 +103,9 @@ function EditVMInner(p: {
|
||||
const [changed, setChanged] = React.useState(false);
|
||||
|
||||
const [, updateState] = React.useState<any>();
|
||||
const forceUpdate = React.useCallback(() => { updateState({}); }, []);
|
||||
const forceUpdate = React.useCallback(() => {
|
||||
updateState({});
|
||||
}, []);
|
||||
|
||||
const valueChanged = () => {
|
||||
setChanged(true);
|
||||
|
||||
@@ -206,7 +206,7 @@ function IsoFilesList(p: {
|
||||
try {
|
||||
const blob = await IsoFilesApi.Download(entry, setDlProgress);
|
||||
|
||||
await downloadBlob(blob, entry.filename);
|
||||
downloadBlob(blob, entry.filename);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert("Failed to download iso file!");
|
||||
|
||||
@@ -66,7 +66,7 @@ function NetworkFiltersListRouteInner(p: {
|
||||
const onlyBuiltin = visibleFilters === VisibleFilters.Builtin;
|
||||
|
||||
return p.list.filter((f) => NWFilterIsBuiltin(f) === onlyBuiltin);
|
||||
}, [visibleFilters]);
|
||||
}, [visibleFilters, p.list]);
|
||||
|
||||
return (
|
||||
<VirtWebRouteContainer
|
||||
@@ -78,7 +78,9 @@ function NetworkFiltersListRouteInner(p: {
|
||||
size="small"
|
||||
value={visibleFilters}
|
||||
exclusive
|
||||
onChange={(_ev, v) => { setVisibleFilters(v); }}
|
||||
onChange={(_ev, v) => {
|
||||
setVisibleFilters(v);
|
||||
}}
|
||||
aria-label="visible filters"
|
||||
>
|
||||
<ToggleButton value={VisibleFilters.All}>All</ToggleButton>
|
||||
@@ -130,8 +132,8 @@ function NetworkFiltersListRouteInner(p: {
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<ul>
|
||||
{t.join_filters.map((f, n) => (
|
||||
<li key={n}>{f}</li>
|
||||
{t.join_filters.map((f) => (
|
||||
<li key={f}>{f}</li>
|
||||
))}
|
||||
</ul>
|
||||
</TableCell>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable react-x/no-array-index-key */
|
||||
import {
|
||||
mdiHarddisk,
|
||||
mdiInformation,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable react-x/no-array-index-key */
|
||||
import VisibilityIcon from "@mui/icons-material/Visibility";
|
||||
import {
|
||||
Button,
|
||||
@@ -99,9 +100,9 @@ export function TokensListRouteInner(p: {
|
||||
{t.max_inactivity && timeDiff(0, t.max_inactivity)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{t.rights.map((r) => {
|
||||
{t.rights.map((r, n) => {
|
||||
return (
|
||||
<div>
|
||||
<div key={n}>
|
||||
{r.verb} {r.path}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -115,8 +115,8 @@ function VMListWidget(p: {
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{p.groups.map((g, num) => (
|
||||
<React.Fragment key={num}>
|
||||
{p.groups.map((g) => (
|
||||
<React.Fragment key={g}>
|
||||
{p.groups.length > 1 && (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
@@ -125,7 +125,9 @@ function VMListWidget(p: {
|
||||
>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => { toggleHiddenGroup(g); }}
|
||||
onClick={() => {
|
||||
toggleHiddenGroup(g);
|
||||
}}
|
||||
>
|
||||
{!hiddenGroups.has(g) ? (
|
||||
<KeyboardArrowUpIcon />
|
||||
@@ -157,7 +159,9 @@ function VMListWidget(p: {
|
||||
<TableCell>
|
||||
<VMStatusWidget
|
||||
vm={row}
|
||||
onChange={(s) => { updateVMState(row, s); }}
|
||||
onChange={(s) => {
|
||||
updateVMState(row, s);
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
|
||||
@@ -44,8 +44,8 @@ function VNCInner(p: { vm: VMInfo }): React.ReactElement {
|
||||
const [counter, setCounter] = React.useState(1);
|
||||
const [connected, setConnected] = React.useState(false);
|
||||
|
||||
const vncRef = React.createRef<HTMLDivElement>();
|
||||
const vncScreenRef = React.createRef<VncScreenHandle>();
|
||||
const vncRef = React.useRef<HTMLDivElement>(null);
|
||||
const vncScreenRef = React.useRef<VncScreenHandle>(null);
|
||||
|
||||
const connect = async (force: boolean) => {
|
||||
try {
|
||||
@@ -91,7 +91,9 @@ function VNCInner(p: { vm: VMInfo }): React.ReactElement {
|
||||
connect(false);
|
||||
|
||||
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);
|
||||
disconnected();
|
||||
}}
|
||||
onConnect={() => { setConnected(true); }}
|
||||
onConnect={() => {
|
||||
setConnected(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
* Generate a random MAC address
|
||||
*/
|
||||
export function randomMacAddress(prefix: string | undefined): string {
|
||||
prefix = prefix ?? "";
|
||||
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, () =>
|
||||
"0123456789abcdef".charAt(Math.floor(Math.random() * 16))
|
||||
|
||||
@@ -19,7 +19,7 @@ export function AsyncWidget(p: {
|
||||
}): React.ReactElement {
|
||||
const [state, setState] = useState(State.Loading);
|
||||
|
||||
const counter = useRef<any | null>(null);
|
||||
const counter = useRef<any>(null);
|
||||
|
||||
const load = async () => {
|
||||
try {
|
||||
|
||||
@@ -31,9 +31,11 @@ export function ConfigImportExportButtons(p: {
|
||||
fileEl.click();
|
||||
|
||||
// Wait for a file to be chosen
|
||||
await new Promise((res, _rej) =>
|
||||
{ fileEl.addEventListener("change", () => { res(null); }); }
|
||||
);
|
||||
await new Promise((res) => {
|
||||
fileEl.addEventListener("change", () => {
|
||||
res(null);
|
||||
});
|
||||
});
|
||||
|
||||
if ((fileEl.files?.length ?? 0) === 0) return null;
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ export function StateActionButton<S>(p: {
|
||||
p.onExecuted();
|
||||
} catch (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";
|
||||
|
||||
export interface TabWidgetOption<E> {
|
||||
@@ -24,7 +25,9 @@ export function TabsWidget<E>(p: {
|
||||
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
|
||||
<Tabs
|
||||
value={currTabIndex}
|
||||
onChange={(_ev, newVal) => { updateActiveTab(newVal); }}
|
||||
onChange={(_ev, newVal) => {
|
||||
updateActiveTab(newVal);
|
||||
}}
|
||||
>
|
||||
{activeOptions.map((o, index) => (
|
||||
<Tab key={index} label={o.label} style={{ color: o.color }} />
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
|
||||
import { Paper, Typography } from "@mui/material";
|
||||
import React, { PropsWithChildren } from "react";
|
||||
import Grid from "@mui/material/Grid";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
|
||||
import React from "react";
|
||||
import { TextInput } from "./TextInput";
|
||||
|
||||
@@ -32,7 +33,7 @@ export function IPInputWithMask(p: {
|
||||
|
||||
const currValue =
|
||||
p.ipAndMask ??
|
||||
(p.ip ?? "") + (p.mask || showSlash.current ? "/" : "") + (p.mask ?? "");
|
||||
`${p.ip ?? ""}${p.mask || showSlash.current ? "/" : ""}${p.mask ?? ""}`;
|
||||
|
||||
const { onValueChange, ...props } = p;
|
||||
return (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable react-x/no-array-index-key */
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { NWFilter, NWFilterURL } from "../../api/NWFilterApi";
|
||||
|
||||
@@ -25,9 +25,7 @@ export function NWFilterSelectInput(p: {
|
||||
value={selectedValue}
|
||||
onDelete={p.editable ? () => p.onChange?.(undefined) : undefined}
|
||||
onClick={
|
||||
!p.editable && selectedValue
|
||||
? () => navigate(NWFilterURL(selectedValue))
|
||||
: undefined
|
||||
!p.editable ? () => navigate(NWFilterURL(selectedValue)) : undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
@@ -48,7 +46,7 @@ export function NWFilterSelectInput(p: {
|
||||
renderInput={(params) => (
|
||||
<TextField {...params} variant="standard" label={p.label} />
|
||||
)}
|
||||
renderOption={(_props, option, _state) => (
|
||||
renderOption={(_props, option) => (
|
||||
<NWFilterItem
|
||||
dense
|
||||
onClick={() => {
|
||||
|
||||
@@ -24,10 +24,13 @@ export function RadioGroupInput(p: {
|
||||
<RadioGroup
|
||||
row
|
||||
value={p.value}
|
||||
onChange={(_ev, v) => { p.onValueChange(v); }}
|
||||
onChange={(_ev, v) => {
|
||||
p.onValueChange(v);
|
||||
}}
|
||||
>
|
||||
{p.options.map((o) => (
|
||||
<FormControlLabel
|
||||
key={o.value}
|
||||
disabled={!p.editable}
|
||||
value={o.value}
|
||||
control={<Radio />}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable react-x/no-array-index-key */
|
||||
import { mdiNetworkOutline } from "@mdi/js";
|
||||
import Icon from "@mdi/react";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
@@ -51,7 +52,6 @@ export function VMNetworksList(p: {
|
||||
{p.vm.networks.map((n, num) => (
|
||||
<EditSection key={num}>
|
||||
<NetworkInfoWidget
|
||||
key={num}
|
||||
network={n}
|
||||
removeFromList={() => {
|
||||
p.vm.networks.splice(num, 1);
|
||||
|
||||
Reference in New Issue
Block a user