Can edit more network settings

This commit is contained in:
2023-12-06 15:30:30 +01:00
parent 7bf4e87df1
commit b7d44f3091
17 changed files with 384 additions and 77 deletions

View File

@ -19,6 +19,8 @@ export interface NetworkInfo {
ip_v6?: IpConfig;
}
export type NetworkStatus = "Started" | "Stopped";
export function NetworkURL(n: NetworkInfo, edit: boolean = false): string {
return `/net/${n.uuid}${edit ? "/edit" : ""}`;
}
@ -61,6 +63,38 @@ export class NetworkApi {
).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
*/

View File

@ -170,4 +170,16 @@ export class ServerApi {
})
).data;
}
/**
* Get host networks card list
*/
static async GetNetworksList(): Promise<string[]> {
return (
await APIClient.exec({
method: "GET",
uri: "/server/networks",
})
).data;
}
}

View File

@ -1,5 +1,5 @@
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 { useSnackbar } from "../hooks/providers/SnackbarProvider";
import React from "react";
@ -56,7 +56,7 @@ export function EditNetworkRoute(): React.ReactElement {
try {
await NetworkApi.Update(n);
snackbar("The network was successfully updated!");
navigate(`/net/${network!.uuid}`);
navigate(NetworkURL(network!));
} catch (e) {
console.error(e);
alert("Failed to update network!");

View File

@ -20,6 +20,7 @@ import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer";
import { useConfirm } from "../hooks/providers/ConfirmDialogProvider";
import { useSnackbar } from "../hooks/providers/SnackbarProvider";
import { useAlert } from "../hooks/providers/AlertDialogProvider";
import { NetworkStatusWidget } from "../widgets/net/NetworkStatusWidget";
export function NetworksListRoute(): React.ReactElement {
const confirm = useConfirm();
@ -93,6 +94,7 @@ function NetworksListRouteInner(p: {
<TableCell>Description</TableCell>
<TableCell>Network type</TableCell>
<TableCell>IP</TableCell>
<TableCell>State</TableCell>
<TableCell>Actions</TableCell>
</TableRow>
</TableHead>
@ -112,6 +114,9 @@ function NetworksListRouteInner(p: {
<TableCell>
{t.ip_v4 && "IPv4"} {t.ip_v6 && "IPv6"}
</TableCell>
<TableCell>
<NetworkStatusWidget net={t} />
</TableCell>
<TableCell>
<RouterLink to={NetworkURL(t)}>
<IconButton>

View File

@ -1,10 +1,16 @@
import React from "react";
import { NetworkApi, NetworkInfo } from "../api/NetworksApi";
import {
NetworkApi,
NetworkInfo,
NetworkStatus,
NetworkURL,
} from "../api/NetworksApi";
import { AsyncWidget } from "../widgets/AsyncWidget";
import { useNavigate, useParams } from "react-router-dom";
import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer";
import { Button } from "@mui/material";
import { NetworkDetails } from "../widgets/net/NetworkDetails";
import { NetworkStatusWidget } from "../widgets/net/NetworkStatusWidget";
export function ViewNetworkRoute() {
const { uuid } = useParams();
@ -31,18 +37,25 @@ function ViewNetworkRouteInner(p: {
}): React.ReactElement {
const navigate = useNavigate();
const [netStatus, setNetStatus] = React.useState<NetworkStatus | undefined>();
return (
<VirtWebRouteContainer
label={`Network ${p.network.name}`}
actions={
/* TODO: show only if network is stopped */
<Button
variant="contained"
style={{ marginLeft: "15px" }}
onClick={() => navigate(`/net/${p.network.uuid}/edit`)}
>
Edit
</Button>
<span>
<NetworkStatusWidget net={p.network} onChange={setNetStatus} />
{netStatus === "Stopped" && (
<Button
variant="contained"
style={{ marginLeft: "15px" }}
onClick={() => navigate(NetworkURL(p.network, true))}
>
Edit
</Button>
)}
</span>
}
>
<NetworkDetails net={p.network} editable={false} />

View 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>
);
}

View File

@ -28,7 +28,11 @@ export function SelectInput(p: {
onChange={(e) => p.onValueChange(e.target.value)}
>
{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}
</MenuItem>
))}

View File

@ -34,7 +34,7 @@ export function TextInput(p: {
return (
<TextField
label={p.label}
value={p.value}
value={p.value ?? ""}
onChange={(e) =>
p.onValueChange?.(
e.target.value.length === 0 ? undefined : e.target.value
@ -47,7 +47,7 @@ export function TextInput(p: {
readOnly: !p.editable,
type: p.type,
}}
variant={p.editable ? "standard" : "standard"}
variant={"standard"}
style={{ width: "100%", marginBottom: "15px" }}
multiline={p.multiline}
minRows={p.minRows}

View File

@ -3,19 +3,44 @@ import { NetworkInfo } from "../../api/NetworksApi";
import { ServerApi } from "../../api/ServerApi";
import { EditSection } from "../forms/EditSection";
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;
editable: boolean;
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 (
<Grid container spacing={2}>
{/* Metadata section */}
<EditSection title="Metadata">
<TextInput
label="Name"
editable={p.editable}
editable={p.editable && !p.net.uuid}
value={p.net.name}
onValueChange={(v) => {
p.net.name = v ?? "";
@ -49,7 +74,58 @@ export function NetworkDetails(p: {
multiline={true}
/>
</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>
);
}

View 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>
);
}

View File

@ -4,18 +4,12 @@ import PlayArrowIcon from "@mui/icons-material/PlayArrow";
import PowerSettingsNewIcon from "@mui/icons-material/PowerSettingsNew";
import ReplayIcon from "@mui/icons-material/Replay";
import StopIcon from "@mui/icons-material/Stop";
import {
CircularProgress,
IconButton,
Tooltip,
Typography,
} from "@mui/material";
import { CircularProgress, Typography } from "@mui/material";
import React from "react";
import { useNavigate } from "react-router-dom";
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 { StateActionButton } from "../StateActionButton";
export function VMStatusWidget(p: {
vm: VMInfo;
@ -59,7 +53,7 @@ export function VMStatusWidget(p: {
{
/* VNC console */ p.vm.vnc_access && (
<ActionButton
<StateActionButton
currState={state}
cond={["Running"]}
icon={<PersonalVideoIcon />}
@ -71,7 +65,7 @@ export function VMStatusWidget(p: {
}
{/* Start VM */}
<ActionButton
<StateActionButton
currState={state}
cond={["Shutdown", "Shutoff", "Crashed"]}
icon={<PlayArrowIcon />}
@ -81,7 +75,7 @@ export function VMStatusWidget(p: {
/>
{/* Resume VM */}
<ActionButton
<StateActionButton
currState={state}
cond={["Paused", "PowerManagementSuspended"]}
icon={<PlayArrowIcon />}
@ -91,7 +85,7 @@ export function VMStatusWidget(p: {
/>
{/* Suspend VM */}
<ActionButton
<StateActionButton
currState={state}
cond={["Running"]}
icon={<PauseIcon />}
@ -102,7 +96,7 @@ export function VMStatusWidget(p: {
/>
{/* Shutdown VM */}
<ActionButton
<StateActionButton
currState={state}
cond={["Running"]}
icon={<PowerSettingsNewIcon />}
@ -113,7 +107,7 @@ export function VMStatusWidget(p: {
/>
{/* Kill VM */}
<ActionButton
<StateActionButton
currState={state}
cond={["Running", "Paused", "PowerManagementSuspended", "Blocked"]}
icon={<StopIcon />}
@ -124,7 +118,7 @@ export function VMStatusWidget(p: {
/>
{/* Reset VM */}
<ActionButton
<StateActionButton
currState={state}
cond={["Running", "Paused", "PowerManagementSuspended", "Blocked"]}
icon={<ReplayIcon />}
@ -136,41 +130,3 @@ export function VMStatusWidget(p: {
</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>
);
}