Can edit more network settings
This commit is contained in:
parent
7bf4e87df1
commit
b7d44f3091
@ -5,7 +5,7 @@ use crate::controllers::{HttpResult, LibVirtReq};
|
|||||||
use crate::extractors::local_auth_extractor::LocalAuthEnabled;
|
use crate::extractors::local_auth_extractor::LocalAuthEnabled;
|
||||||
use crate::libvirt_rest_structures::HypervisorInfo;
|
use crate::libvirt_rest_structures::HypervisorInfo;
|
||||||
use actix_web::{HttpResponse, Responder};
|
use actix_web::{HttpResponse, Responder};
|
||||||
use sysinfo::{System, SystemExt};
|
use sysinfo::{NetworksExt, System, SystemExt};
|
||||||
|
|
||||||
pub async fn root_index() -> impl Responder {
|
pub async fn root_index() -> impl Responder {
|
||||||
HttpResponse::Ok().body("Hello world!")
|
HttpResponse::Ok().body("Hello world!")
|
||||||
@ -86,3 +86,16 @@ pub async fn server_info(client: LibVirtReq) -> HttpResult {
|
|||||||
system,
|
system,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn networks_list() -> HttpResult {
|
||||||
|
let mut system = System::new();
|
||||||
|
system.refresh_networks_list();
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(
|
||||||
|
system
|
||||||
|
.networks()
|
||||||
|
.iter()
|
||||||
|
.map(|n| n.0.to_string())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
@ -229,10 +229,10 @@ pub struct NetworkForwardXML {
|
|||||||
pub mode: String,
|
pub mode: String,
|
||||||
#[serde(
|
#[serde(
|
||||||
default,
|
default,
|
||||||
rename(serialize = "@mode"),
|
rename(serialize = "@dev"),
|
||||||
skip_serializing_if = "Option::is_none"
|
skip_serializing_if = "String::is_empty"
|
||||||
)]
|
)]
|
||||||
pub dev: Option<String>,
|
pub dev: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Network DNS information
|
/// Network DNS information
|
||||||
|
@ -421,7 +421,7 @@ impl NetworkInfo {
|
|||||||
forward: match self.forward_mode {
|
forward: match self.forward_mode {
|
||||||
NetworkForwardMode::NAT => Some(NetworkForwardXML {
|
NetworkForwardMode::NAT => Some(NetworkForwardXML {
|
||||||
mode: "nat".to_string(),
|
mode: "nat".to_string(),
|
||||||
dev: self.device,
|
dev: self.device.unwrap_or_default(),
|
||||||
}),
|
}),
|
||||||
NetworkForwardMode::Isolated => None,
|
NetworkForwardMode::Isolated => None,
|
||||||
},
|
},
|
||||||
@ -443,7 +443,13 @@ impl NetworkInfo {
|
|||||||
None => NetworkForwardMode::Isolated,
|
None => NetworkForwardMode::Isolated,
|
||||||
Some(_) => NetworkForwardMode::NAT,
|
Some(_) => NetworkForwardMode::NAT,
|
||||||
},
|
},
|
||||||
device: xml.forward.map(|f| f.dev).unwrap_or(None),
|
device: xml
|
||||||
|
.forward
|
||||||
|
.map(|f| match f.dev.is_empty() {
|
||||||
|
true => None,
|
||||||
|
false => Some(f.dev),
|
||||||
|
})
|
||||||
|
.unwrap_or(None),
|
||||||
dns_server: xml.dns.map(|d| d.forwarder.addr),
|
dns_server: xml.dns.map(|d| d.forwarder.addr),
|
||||||
domain: xml.domain.map(|d| d.name),
|
domain: xml.domain.map(|d| d.name),
|
||||||
ip_v4: xml
|
ip_v4: xml
|
||||||
|
@ -101,6 +101,10 @@ async fn main() -> std::io::Result<()> {
|
|||||||
"/api/server/info",
|
"/api/server/info",
|
||||||
web::get().to(server_controller::server_info),
|
web::get().to(server_controller::server_info),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/api/server/networks",
|
||||||
|
web::get().to(server_controller::networks_list),
|
||||||
|
)
|
||||||
// Auth controller
|
// Auth controller
|
||||||
.route(
|
.route(
|
||||||
"/api/auth/local",
|
"/api/auth/local",
|
||||||
|
68
virtweb_frontend/package-lock.json
generated
68
virtweb_frontend/package-lock.json
generated
@ -31,6 +31,7 @@
|
|||||||
"mui-file-input": "^3.0.1",
|
"mui-file-input": "^3.0.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"react-imask": "^7.1.3",
|
||||||
"react-router-dom": "^6.15.0",
|
"react-router-dom": "^6.15.0",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"react-vnc": "^1.0.0",
|
"react-vnc": "^1.0.0",
|
||||||
@ -1971,6 +1972,19 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/runtime-corejs3": {
|
||||||
|
"version": "7.23.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.23.5.tgz",
|
||||||
|
"integrity": "sha512-7+ziVclejQTLYhXl+Oi1f6gTGD1XDCeLa4R472TNGQxb08zbEJ0OdNoh5Piz+57Ltmui6xR88BXR4gS3/Toslw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"core-js-pure": "^3.30.2",
|
||||||
|
"regenerator-runtime": "^0.14.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/template": {
|
"node_modules/@babel/template": {
|
||||||
"version": "7.22.5",
|
"version": "7.22.5",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz",
|
||||||
@ -9641,6 +9655,18 @@
|
|||||||
"node": ">= 4"
|
"node": ">= 4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/imask": {
|
||||||
|
"version": "7.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/imask/-/imask-7.1.3.tgz",
|
||||||
|
"integrity": "sha512-jZCqTI5Jgukhl2ff+znBQd8BiHOTlnFYCIgggzHYDdoJsHmSSWr1BaejcYBxsjy4ZIs8Rm0HhbOxQcobcdESRQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime-corejs3": "^7.22.6"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"npm": ">=4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/immer": {
|
"node_modules/immer": {
|
||||||
"version": "9.0.21",
|
"version": "9.0.21",
|
||||||
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
|
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
|
||||||
@ -15154,6 +15180,22 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz",
|
||||||
"integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg=="
|
"integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg=="
|
||||||
},
|
},
|
||||||
|
"node_modules/react-imask": {
|
||||||
|
"version": "7.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-imask/-/react-imask-7.1.3.tgz",
|
||||||
|
"integrity": "sha512-anCnzdkqpDzNwe7ot76kQSvmnz4Sw7AW/QFjjLh3B87HVNv9e2oHC+1m9hQKSIui2Tqm7w68ooMgDFsCQlDMyg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"imask": "^7.1.3",
|
||||||
|
"prop-types": "^15.8.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"npm": ">=4.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=0.14.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-is": {
|
"node_modules/react-is": {
|
||||||
"version": "17.0.2",
|
"version": "17.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||||
@ -19550,6 +19592,15 @@
|
|||||||
"regenerator-runtime": "^0.14.0"
|
"regenerator-runtime": "^0.14.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@babel/runtime-corejs3": {
|
||||||
|
"version": "7.23.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.23.5.tgz",
|
||||||
|
"integrity": "sha512-7+ziVclejQTLYhXl+Oi1f6gTGD1XDCeLa4R472TNGQxb08zbEJ0OdNoh5Piz+57Ltmui6xR88BXR4gS3/Toslw==",
|
||||||
|
"requires": {
|
||||||
|
"core-js-pure": "^3.30.2",
|
||||||
|
"regenerator-runtime": "^0.14.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@babel/template": {
|
"@babel/template": {
|
||||||
"version": "7.22.5",
|
"version": "7.22.5",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz",
|
||||||
@ -25051,6 +25102,14 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
|
||||||
"integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ=="
|
"integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ=="
|
||||||
},
|
},
|
||||||
|
"imask": {
|
||||||
|
"version": "7.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/imask/-/imask-7.1.3.tgz",
|
||||||
|
"integrity": "sha512-jZCqTI5Jgukhl2ff+znBQd8BiHOTlnFYCIgggzHYDdoJsHmSSWr1BaejcYBxsjy4ZIs8Rm0HhbOxQcobcdESRQ==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime-corejs3": "^7.22.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"immer": {
|
"immer": {
|
||||||
"version": "9.0.21",
|
"version": "9.0.21",
|
||||||
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
|
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
|
||||||
@ -28839,6 +28898,15 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz",
|
||||||
"integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg=="
|
"integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg=="
|
||||||
},
|
},
|
||||||
|
"react-imask": {
|
||||||
|
"version": "7.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-imask/-/react-imask-7.1.3.tgz",
|
||||||
|
"integrity": "sha512-anCnzdkqpDzNwe7ot76kQSvmnz4Sw7AW/QFjjLh3B87HVNv9e2oHC+1m9hQKSIui2Tqm7w68ooMgDFsCQlDMyg==",
|
||||||
|
"requires": {
|
||||||
|
"imask": "^7.1.3",
|
||||||
|
"prop-types": "^15.8.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-is": {
|
"react-is": {
|
||||||
"version": "17.0.2",
|
"version": "17.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||||
|
@ -26,6 +26,7 @@
|
|||||||
"mui-file-input": "^3.0.1",
|
"mui-file-input": "^3.0.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"react-imask": "^7.1.3",
|
||||||
"react-router-dom": "^6.15.0",
|
"react-router-dom": "^6.15.0",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"react-vnc": "^1.0.0",
|
"react-vnc": "^1.0.0",
|
||||||
|
@ -19,6 +19,8 @@ export interface NetworkInfo {
|
|||||||
ip_v6?: IpConfig;
|
ip_v6?: IpConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type NetworkStatus = "Started" | "Stopped";
|
||||||
|
|
||||||
export function NetworkURL(n: NetworkInfo, edit: boolean = false): string {
|
export function NetworkURL(n: NetworkInfo, edit: boolean = false): string {
|
||||||
return `/net/${n.uuid}${edit ? "/edit" : ""}`;
|
return `/net/${n.uuid}${edit ? "/edit" : ""}`;
|
||||||
}
|
}
|
||||||
@ -61,6 +63,38 @@ export class NetworkApi {
|
|||||||
).data;
|
).data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the status of network
|
||||||
|
*/
|
||||||
|
static async GetState(net: NetworkInfo): Promise<NetworkStatus> {
|
||||||
|
return (
|
||||||
|
await APIClient.exec({
|
||||||
|
method: "GET",
|
||||||
|
uri: `/network/${net.uuid}/status`,
|
||||||
|
})
|
||||||
|
).data.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the network
|
||||||
|
*/
|
||||||
|
static async Start(net: NetworkInfo): Promise<void> {
|
||||||
|
await APIClient.exec({
|
||||||
|
method: "GET",
|
||||||
|
uri: `/network/${net.uuid}/start`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the network
|
||||||
|
*/
|
||||||
|
static async Stop(net: NetworkInfo): Promise<void> {
|
||||||
|
await APIClient.exec({
|
||||||
|
method: "GET",
|
||||||
|
uri: `/network/${net.uuid}/stop`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update an existing network
|
* Update an existing network
|
||||||
*/
|
*/
|
||||||
|
@ -170,4 +170,16 @@ export class ServerApi {
|
|||||||
})
|
})
|
||||||
).data;
|
).data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get host networks card list
|
||||||
|
*/
|
||||||
|
static async GetNetworksList(): Promise<string[]> {
|
||||||
|
return (
|
||||||
|
await APIClient.exec({
|
||||||
|
method: "GET",
|
||||||
|
uri: "/server/networks",
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import { NetworkApi, NetworkInfo } from "../api/NetworksApi";
|
import { NetworkApi, NetworkInfo, NetworkURL } from "../api/NetworksApi";
|
||||||
import { useAlert } from "../hooks/providers/AlertDialogProvider";
|
import { useAlert } from "../hooks/providers/AlertDialogProvider";
|
||||||
import { useSnackbar } from "../hooks/providers/SnackbarProvider";
|
import { useSnackbar } from "../hooks/providers/SnackbarProvider";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
@ -56,7 +56,7 @@ export function EditNetworkRoute(): React.ReactElement {
|
|||||||
try {
|
try {
|
||||||
await NetworkApi.Update(n);
|
await NetworkApi.Update(n);
|
||||||
snackbar("The network was successfully updated!");
|
snackbar("The network was successfully updated!");
|
||||||
navigate(`/net/${network!.uuid}`);
|
navigate(NetworkURL(network!));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
alert("Failed to update network!");
|
alert("Failed to update network!");
|
||||||
|
@ -20,6 +20,7 @@ import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer";
|
|||||||
import { useConfirm } from "../hooks/providers/ConfirmDialogProvider";
|
import { useConfirm } from "../hooks/providers/ConfirmDialogProvider";
|
||||||
import { useSnackbar } from "../hooks/providers/SnackbarProvider";
|
import { useSnackbar } from "../hooks/providers/SnackbarProvider";
|
||||||
import { useAlert } from "../hooks/providers/AlertDialogProvider";
|
import { useAlert } from "../hooks/providers/AlertDialogProvider";
|
||||||
|
import { NetworkStatusWidget } from "../widgets/net/NetworkStatusWidget";
|
||||||
|
|
||||||
export function NetworksListRoute(): React.ReactElement {
|
export function NetworksListRoute(): React.ReactElement {
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
@ -93,6 +94,7 @@ function NetworksListRouteInner(p: {
|
|||||||
<TableCell>Description</TableCell>
|
<TableCell>Description</TableCell>
|
||||||
<TableCell>Network type</TableCell>
|
<TableCell>Network type</TableCell>
|
||||||
<TableCell>IP</TableCell>
|
<TableCell>IP</TableCell>
|
||||||
|
<TableCell>State</TableCell>
|
||||||
<TableCell>Actions</TableCell>
|
<TableCell>Actions</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
@ -112,6 +114,9 @@ function NetworksListRouteInner(p: {
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
{t.ip_v4 && "IPv4"} {t.ip_v6 && "IPv6"}
|
{t.ip_v4 && "IPv4"} {t.ip_v6 && "IPv6"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<NetworkStatusWidget net={t} />
|
||||||
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<RouterLink to={NetworkURL(t)}>
|
<RouterLink to={NetworkURL(t)}>
|
||||||
<IconButton>
|
<IconButton>
|
||||||
|
@ -1,10 +1,16 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { NetworkApi, NetworkInfo } from "../api/NetworksApi";
|
import {
|
||||||
|
NetworkApi,
|
||||||
|
NetworkInfo,
|
||||||
|
NetworkStatus,
|
||||||
|
NetworkURL,
|
||||||
|
} from "../api/NetworksApi";
|
||||||
import { AsyncWidget } from "../widgets/AsyncWidget";
|
import { AsyncWidget } from "../widgets/AsyncWidget";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer";
|
import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer";
|
||||||
import { Button } from "@mui/material";
|
import { Button } from "@mui/material";
|
||||||
import { NetworkDetails } from "../widgets/net/NetworkDetails";
|
import { NetworkDetails } from "../widgets/net/NetworkDetails";
|
||||||
|
import { NetworkStatusWidget } from "../widgets/net/NetworkStatusWidget";
|
||||||
|
|
||||||
export function ViewNetworkRoute() {
|
export function ViewNetworkRoute() {
|
||||||
const { uuid } = useParams();
|
const { uuid } = useParams();
|
||||||
@ -31,18 +37,25 @@ function ViewNetworkRouteInner(p: {
|
|||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const [netStatus, setNetStatus] = React.useState<NetworkStatus | undefined>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VirtWebRouteContainer
|
<VirtWebRouteContainer
|
||||||
label={`Network ${p.network.name}`}
|
label={`Network ${p.network.name}`}
|
||||||
actions={
|
actions={
|
||||||
/* TODO: show only if network is stopped */
|
<span>
|
||||||
<Button
|
<NetworkStatusWidget net={p.network} onChange={setNetStatus} />
|
||||||
variant="contained"
|
|
||||||
style={{ marginLeft: "15px" }}
|
{netStatus === "Stopped" && (
|
||||||
onClick={() => navigate(`/net/${p.network.uuid}/edit`)}
|
<Button
|
||||||
>
|
variant="contained"
|
||||||
Edit
|
style={{ marginLeft: "15px" }}
|
||||||
</Button>
|
onClick={() => navigate(NetworkURL(p.network, true))}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<NetworkDetails net={p.network} editable={false} />
|
<NetworkDetails net={p.network} editable={false} />
|
||||||
|
41
virtweb_frontend/src/widgets/StateActionButton.tsx
Normal file
41
virtweb_frontend/src/widgets/StateActionButton.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { IconButton, Tooltip } from "@mui/material";
|
||||||
|
import { useAlert } from "../hooks/providers/AlertDialogProvider";
|
||||||
|
import { useConfirm } from "../hooks/providers/ConfirmDialogProvider";
|
||||||
|
|
||||||
|
export function StateActionButton<S>(p: {
|
||||||
|
currState: S;
|
||||||
|
cond: S[];
|
||||||
|
icon: React.ReactElement;
|
||||||
|
tooltip: string;
|
||||||
|
confirmMessage?: string;
|
||||||
|
performAction: () => Promise<void>;
|
||||||
|
onExecuted: () => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const confirm = useConfirm();
|
||||||
|
const alert = useAlert();
|
||||||
|
|
||||||
|
if (!p.cond.includes(p.currState)) return <></>;
|
||||||
|
|
||||||
|
const performAction = async () => {
|
||||||
|
try {
|
||||||
|
if (p.confirmMessage && !(await confirm(p.confirmMessage))) return;
|
||||||
|
await p.performAction();
|
||||||
|
p.onExecuted();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
alert("Failed to perform action! " + e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip title={p.tooltip}>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={performAction}
|
||||||
|
style={{ paddingBottom: "0px", paddingTop: "0px" }}
|
||||||
|
>
|
||||||
|
{p.icon}
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
@ -28,7 +28,11 @@ export function SelectInput(p: {
|
|||||||
onChange={(e) => p.onValueChange(e.target.value)}
|
onChange={(e) => p.onValueChange(e.target.value)}
|
||||||
>
|
>
|
||||||
{p.options.map((e) => (
|
{p.options.map((e) => (
|
||||||
<MenuItem key={e.value} value={e.value}>
|
<MenuItem
|
||||||
|
key={e.value}
|
||||||
|
value={e.value}
|
||||||
|
style={{ fontStyle: e.value === undefined ? "italic" : undefined }}
|
||||||
|
>
|
||||||
{e.label}
|
{e.label}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
|
@ -34,7 +34,7 @@ export function TextInput(p: {
|
|||||||
return (
|
return (
|
||||||
<TextField
|
<TextField
|
||||||
label={p.label}
|
label={p.label}
|
||||||
value={p.value}
|
value={p.value ?? ""}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
p.onValueChange?.(
|
p.onValueChange?.(
|
||||||
e.target.value.length === 0 ? undefined : e.target.value
|
e.target.value.length === 0 ? undefined : e.target.value
|
||||||
@ -47,7 +47,7 @@ export function TextInput(p: {
|
|||||||
readOnly: !p.editable,
|
readOnly: !p.editable,
|
||||||
type: p.type,
|
type: p.type,
|
||||||
}}
|
}}
|
||||||
variant={p.editable ? "standard" : "standard"}
|
variant={"standard"}
|
||||||
style={{ width: "100%", marginBottom: "15px" }}
|
style={{ width: "100%", marginBottom: "15px" }}
|
||||||
multiline={p.multiline}
|
multiline={p.multiline}
|
||||||
minRows={p.minRows}
|
minRows={p.minRows}
|
||||||
|
@ -3,19 +3,44 @@ import { NetworkInfo } from "../../api/NetworksApi";
|
|||||||
import { ServerApi } from "../../api/ServerApi";
|
import { ServerApi } from "../../api/ServerApi";
|
||||||
import { EditSection } from "../forms/EditSection";
|
import { EditSection } from "../forms/EditSection";
|
||||||
import { TextInput } from "../forms/TextInput";
|
import { TextInput } from "../forms/TextInput";
|
||||||
|
import { SelectInput } from "../forms/SelectInput";
|
||||||
|
import React from "react";
|
||||||
|
import { AsyncWidget } from "../AsyncWidget";
|
||||||
|
import { IPv4Input } from "../forms/IPv4Input";
|
||||||
|
|
||||||
export function NetworkDetails(p: {
|
interface DetailsProps {
|
||||||
net: NetworkInfo;
|
net: NetworkInfo;
|
||||||
editable: boolean;
|
editable: boolean;
|
||||||
onChange?: () => void;
|
onChange?: () => void;
|
||||||
}): React.ReactElement {
|
}
|
||||||
|
|
||||||
|
export function NetworkDetails(p: DetailsProps): React.ReactElement {
|
||||||
|
const [cardsList, setCardsList] = React.useState<string[] | any>();
|
||||||
|
|
||||||
|
const load = async () => {
|
||||||
|
setCardsList(await ServerApi.GetNetworksList());
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AsyncWidget
|
||||||
|
loadKey={"1"}
|
||||||
|
load={load}
|
||||||
|
errMsg="Failed to load the list of host network cards!"
|
||||||
|
build={() => <NetworkDetailsInner cardsList={cardsList} {...p} />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NetworkDetailsInner(
|
||||||
|
p: DetailsProps & { cardsList: string[] }
|
||||||
|
): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
<Grid container spacing={2}>
|
<Grid container spacing={2}>
|
||||||
{/* Metadata section */}
|
{/* Metadata section */}
|
||||||
<EditSection title="Metadata">
|
<EditSection title="Metadata">
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Name"
|
label="Name"
|
||||||
editable={p.editable}
|
editable={p.editable && !p.net.uuid}
|
||||||
value={p.net.name}
|
value={p.net.name}
|
||||||
onValueChange={(v) => {
|
onValueChange={(v) => {
|
||||||
p.net.name = v ?? "";
|
p.net.name = v ?? "";
|
||||||
@ -49,7 +74,58 @@ export function NetworkDetails(p: {
|
|||||||
multiline={true}
|
multiline={true}
|
||||||
/>
|
/>
|
||||||
</EditSection>
|
</EditSection>
|
||||||
TODO:continue
|
{/* TODO : autostart */}
|
||||||
|
|
||||||
|
<EditSection title="General settings">
|
||||||
|
<SelectInput
|
||||||
|
editable={p.editable}
|
||||||
|
label="Forward mode"
|
||||||
|
onValueChange={(v) => {
|
||||||
|
p.net.forward_mode = v as any;
|
||||||
|
p.onChange?.();
|
||||||
|
}}
|
||||||
|
value={p.net.forward_mode}
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
label: "NAT",
|
||||||
|
value: "NAT",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
label: "Isolated network",
|
||||||
|
value: "Isolated",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{p.net.forward_mode === "NAT" && (
|
||||||
|
<SelectInput
|
||||||
|
editable={p.editable}
|
||||||
|
label="Network output device"
|
||||||
|
onValueChange={(v) => {
|
||||||
|
p.net.device = v;
|
||||||
|
p.onChange?.();
|
||||||
|
}}
|
||||||
|
value={p.net.device}
|
||||||
|
options={[
|
||||||
|
{ label: "Default interface", value: undefined },
|
||||||
|
...p.cardsList.map((d) => {
|
||||||
|
return { label: d, value: d };
|
||||||
|
}),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<IPv4Input
|
||||||
|
editable={p.editable}
|
||||||
|
label="DNS server to use"
|
||||||
|
value={p.net.dns_server}
|
||||||
|
onValueChange={(v) => {
|
||||||
|
p.net.dns_server = v;
|
||||||
|
p.onChange?.();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</EditSection>
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
74
virtweb_frontend/src/widgets/net/NetworkStatusWidget.tsx
Normal file
74
virtweb_frontend/src/widgets/net/NetworkStatusWidget.tsx
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
|
||||||
|
import StopIcon from "@mui/icons-material/Stop";
|
||||||
|
import { CircularProgress, Typography } from "@mui/material";
|
||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
NetworkApi,
|
||||||
|
NetworkInfo,
|
||||||
|
NetworkStatus as NetworkState,
|
||||||
|
} from "../../api/NetworksApi";
|
||||||
|
import { useSnackbar } from "../../hooks/providers/SnackbarProvider";
|
||||||
|
import { StateActionButton } from "../StateActionButton";
|
||||||
|
|
||||||
|
export function NetworkStatusWidget(p: {
|
||||||
|
net: NetworkInfo;
|
||||||
|
onChange?: (s: NetworkState) => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const snackbar = useSnackbar();
|
||||||
|
|
||||||
|
const [state, setState] = React.useState<undefined | NetworkState>();
|
||||||
|
|
||||||
|
const refresh = async () => {
|
||||||
|
try {
|
||||||
|
const s = await NetworkApi.GetState(p.net);
|
||||||
|
if (s !== state) p.onChange?.(s);
|
||||||
|
setState(s);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
snackbar("Failed to refresh network status!");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const changedAction = () => setState(undefined);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
refresh();
|
||||||
|
const i = setInterval(() => refresh(), 3000);
|
||||||
|
|
||||||
|
return () => clearInterval(i);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (state === undefined)
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CircularProgress size={"1rem"} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ display: "inline-flex" }}>
|
||||||
|
<Typography>{state}</Typography>
|
||||||
|
|
||||||
|
{/* Start Network */}
|
||||||
|
<StateActionButton
|
||||||
|
currState={state}
|
||||||
|
cond={["Stopped"]}
|
||||||
|
icon={<PlayArrowIcon />}
|
||||||
|
tooltip="Start the Network"
|
||||||
|
performAction={() => NetworkApi.Start(p.net)}
|
||||||
|
onExecuted={changedAction}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Stop network */}
|
||||||
|
<StateActionButton
|
||||||
|
currState={state}
|
||||||
|
cond={["Started"]}
|
||||||
|
icon={<StopIcon />}
|
||||||
|
tooltip="Stop the network"
|
||||||
|
confirmMessage="Do you really want to kill stop this network?"
|
||||||
|
performAction={() => NetworkApi.Stop(p.net)}
|
||||||
|
onExecuted={changedAction}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -4,18 +4,12 @@ import PlayArrowIcon from "@mui/icons-material/PlayArrow";
|
|||||||
import PowerSettingsNewIcon from "@mui/icons-material/PowerSettingsNew";
|
import PowerSettingsNewIcon from "@mui/icons-material/PowerSettingsNew";
|
||||||
import ReplayIcon from "@mui/icons-material/Replay";
|
import ReplayIcon from "@mui/icons-material/Replay";
|
||||||
import StopIcon from "@mui/icons-material/Stop";
|
import StopIcon from "@mui/icons-material/Stop";
|
||||||
import {
|
import { CircularProgress, Typography } from "@mui/material";
|
||||||
CircularProgress,
|
|
||||||
IconButton,
|
|
||||||
Tooltip,
|
|
||||||
Typography,
|
|
||||||
} from "@mui/material";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { VMApi, VMInfo, VMState } from "../../api/VMApi";
|
import { VMApi, VMInfo, VMState } from "../../api/VMApi";
|
||||||
import { useAlert } from "../../hooks/providers/AlertDialogProvider";
|
|
||||||
import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider";
|
|
||||||
import { useSnackbar } from "../../hooks/providers/SnackbarProvider";
|
import { useSnackbar } from "../../hooks/providers/SnackbarProvider";
|
||||||
|
import { StateActionButton } from "../StateActionButton";
|
||||||
|
|
||||||
export function VMStatusWidget(p: {
|
export function VMStatusWidget(p: {
|
||||||
vm: VMInfo;
|
vm: VMInfo;
|
||||||
@ -59,7 +53,7 @@ export function VMStatusWidget(p: {
|
|||||||
|
|
||||||
{
|
{
|
||||||
/* VNC console */ p.vm.vnc_access && (
|
/* VNC console */ p.vm.vnc_access && (
|
||||||
<ActionButton
|
<StateActionButton
|
||||||
currState={state}
|
currState={state}
|
||||||
cond={["Running"]}
|
cond={["Running"]}
|
||||||
icon={<PersonalVideoIcon />}
|
icon={<PersonalVideoIcon />}
|
||||||
@ -71,7 +65,7 @@ export function VMStatusWidget(p: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{/* Start VM */}
|
{/* Start VM */}
|
||||||
<ActionButton
|
<StateActionButton
|
||||||
currState={state}
|
currState={state}
|
||||||
cond={["Shutdown", "Shutoff", "Crashed"]}
|
cond={["Shutdown", "Shutoff", "Crashed"]}
|
||||||
icon={<PlayArrowIcon />}
|
icon={<PlayArrowIcon />}
|
||||||
@ -81,7 +75,7 @@ export function VMStatusWidget(p: {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Resume VM */}
|
{/* Resume VM */}
|
||||||
<ActionButton
|
<StateActionButton
|
||||||
currState={state}
|
currState={state}
|
||||||
cond={["Paused", "PowerManagementSuspended"]}
|
cond={["Paused", "PowerManagementSuspended"]}
|
||||||
icon={<PlayArrowIcon />}
|
icon={<PlayArrowIcon />}
|
||||||
@ -91,7 +85,7 @@ export function VMStatusWidget(p: {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Suspend VM */}
|
{/* Suspend VM */}
|
||||||
<ActionButton
|
<StateActionButton
|
||||||
currState={state}
|
currState={state}
|
||||||
cond={["Running"]}
|
cond={["Running"]}
|
||||||
icon={<PauseIcon />}
|
icon={<PauseIcon />}
|
||||||
@ -102,7 +96,7 @@ export function VMStatusWidget(p: {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Shutdown VM */}
|
{/* Shutdown VM */}
|
||||||
<ActionButton
|
<StateActionButton
|
||||||
currState={state}
|
currState={state}
|
||||||
cond={["Running"]}
|
cond={["Running"]}
|
||||||
icon={<PowerSettingsNewIcon />}
|
icon={<PowerSettingsNewIcon />}
|
||||||
@ -113,7 +107,7 @@ export function VMStatusWidget(p: {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Kill VM */}
|
{/* Kill VM */}
|
||||||
<ActionButton
|
<StateActionButton
|
||||||
currState={state}
|
currState={state}
|
||||||
cond={["Running", "Paused", "PowerManagementSuspended", "Blocked"]}
|
cond={["Running", "Paused", "PowerManagementSuspended", "Blocked"]}
|
||||||
icon={<StopIcon />}
|
icon={<StopIcon />}
|
||||||
@ -124,7 +118,7 @@ export function VMStatusWidget(p: {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Reset VM */}
|
{/* Reset VM */}
|
||||||
<ActionButton
|
<StateActionButton
|
||||||
currState={state}
|
currState={state}
|
||||||
cond={["Running", "Paused", "PowerManagementSuspended", "Blocked"]}
|
cond={["Running", "Paused", "PowerManagementSuspended", "Blocked"]}
|
||||||
icon={<ReplayIcon />}
|
icon={<ReplayIcon />}
|
||||||
@ -136,41 +130,3 @@ export function VMStatusWidget(p: {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ActionButton(p: {
|
|
||||||
currState: VMState;
|
|
||||||
cond: VMState[];
|
|
||||||
icon: React.ReactElement;
|
|
||||||
tooltip: string;
|
|
||||||
confirmMessage?: string;
|
|
||||||
performAction: () => Promise<void>;
|
|
||||||
onExecuted: () => void;
|
|
||||||
}): React.ReactElement {
|
|
||||||
const confirm = useConfirm();
|
|
||||||
const alert = useAlert();
|
|
||||||
|
|
||||||
if (!p.cond.includes(p.currState)) return <></>;
|
|
||||||
|
|
||||||
const performAction = async () => {
|
|
||||||
try {
|
|
||||||
if (p.confirmMessage && !(await confirm(p.confirmMessage))) return;
|
|
||||||
await p.performAction();
|
|
||||||
p.onExecuted();
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
alert("Failed to perform action! " + e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip title={p.tooltip}>
|
|
||||||
<IconButton
|
|
||||||
size="small"
|
|
||||||
onClick={performAction}
|
|
||||||
style={{ paddingBottom: "0px", paddingTop: "0px" }}
|
|
||||||
>
|
|
||||||
{p.icon}
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user