diff --git a/virtweb_frontend/src/App.tsx b/virtweb_frontend/src/App.tsx
index 33a96d3..23312d9 100644
--- a/virtweb_frontend/src/App.tsx
+++ b/virtweb_frontend/src/App.tsx
@@ -15,8 +15,9 @@ import { AuthApi } from "./api/AuthApi";
import { IsoFilesRoute } from "./routes/IsoFilesRoute";
import { ServerApi } from "./api/ServerApi";
import { SysInfoRoute } from "./routes/SysInfoRoute";
-import { VirtualMachinesRoute } from "./routes/VirtualMachinesRoute";
+import { VMListRoute } from "./routes/VMListRoute";
import { CreateVMRoute, EditVMRoute } from "./routes/EditVMRoute";
+import { VMRoute } from "./routes/VMRoute";
interface AuthContext {
signedIn: boolean;
@@ -39,8 +40,9 @@ export function App() {
}>
} />
- } />
+ } />
} />
+ } />
} />
} />
diff --git a/virtweb_frontend/src/routes/EditVMRoute.tsx b/virtweb_frontend/src/routes/EditVMRoute.tsx
index ec6abfb..ceeb079 100644
--- a/virtweb_frontend/src/routes/EditVMRoute.tsx
+++ b/virtweb_frontend/src/routes/EditVMRoute.tsx
@@ -1,16 +1,12 @@
-import { Button, Grid, Paper, Typography } from "@mui/material";
-import React, { PropsWithChildren } from "react";
+import { Button } from "@mui/material";
+import React from "react";
import { useNavigate, useParams } from "react-router-dom";
-import { validate as validateUUID } from "uuid";
-import { ServerApi } from "../api/ServerApi";
import { VMApi, VMInfo } from "../api/VMApi";
import { useAlert } from "../hooks/providers/AlertDialogProvider";
import { useSnackbar } from "../hooks/providers/SnackbarProvider";
import { AsyncWidget } from "../widgets/AsyncWidget";
import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer";
-import { CheckboxInput } from "../widgets/forms/CheckboxInput";
-import { SelectInput } from "../widgets/forms/SelectInput";
-import { TextInput } from "../widgets/forms/TextInput";
+import { VMDetails } from "../widgets/vms/VMDetails";
export function CreateVMRoute(): React.ReactElement {
const snackbar = useSnackbar();
@@ -105,7 +101,6 @@ function EditVMInner(p: {
setChanged(true);
forceUpdate();
};
-
return (
}
>
-
- {/* Metadata section */}
-
- {
- p.vm.name = v ?? "";
- valueChanged();
- }}
- size={ServerApi.Config.constraints.name_size}
- />
-
-
-
- {
- p.vm.genid = v;
- valueChanged();
- }}
- checkValue={(v) => validateUUID(v)}
- />
-
- {
- p.vm.title = v;
- valueChanged();
- }}
- size={ServerApi.Config.constraints.title_size}
- />
-
- {
- p.vm.description = v;
- valueChanged();
- }}
- multiline={true}
- />
-
-
- {/* General section */}
-
- {
- p.vm.architecture = v! as any;
- valueChanged();
- }}
- value={p.vm.architecture}
- options={[
- { label: "i686", value: "i686" },
- { label: "x86_64", value: "x86_64" },
- ]}
- />
-
- {
- p.vm.boot_type = v! as any;
- valueChanged();
- }}
- value={p.vm.boot_type}
- options={[
- { label: "UEFI with Secure Boot", value: "UEFISecureBoot" },
- { label: "UEFI", value: "UEFI" },
- ]}
- />
-
- {
- p.vm.memory = Number(v ?? "0");
- valueChanged();
- }}
- checkValue={(v) =>
- Number(v) > ServerApi.Config.constraints.memory_size.min &&
- Number(v) < ServerApi.Config.constraints.memory_size.max
- }
- />
-
- {
- p.vm.vnc_access = v;
- valueChanged();
- }}
- />
-
-
+
);
}
-
-function EditSection(
- p: { title: string } & PropsWithChildren
-): React.ReactElement {
- return (
-
-
-
- {p.title}
-
- {p.children}
-
-
- );
-}
diff --git a/virtweb_frontend/src/routes/VirtualMachinesRoute.tsx b/virtweb_frontend/src/routes/VMListRoute.tsx
similarity index 97%
rename from virtweb_frontend/src/routes/VirtualMachinesRoute.tsx
rename to virtweb_frontend/src/routes/VMListRoute.tsx
index 842c45d..a82452c 100644
--- a/virtweb_frontend/src/routes/VirtualMachinesRoute.tsx
+++ b/virtweb_frontend/src/routes/VMListRoute.tsx
@@ -22,7 +22,7 @@ import { VMStatusWidget } from "../widgets/vms/VMStatusWidget";
import { useSnackbar } from "../hooks/providers/SnackbarProvider";
import { useConfirm } from "../hooks/providers/ConfirmDialogProvider";
-export function VirtualMachinesRoute(): React.ReactElement {
+export function VMListRoute(): React.ReactElement {
const [list, setList] = React.useState();
const loadKey = React.useRef(1);
@@ -119,7 +119,7 @@ function VMListWidget(p: {
{row.description ?? ""}
{filesize(row.memory * 1000 * 1000)}
-
+
diff --git a/virtweb_frontend/src/routes/VMRoute.tsx b/virtweb_frontend/src/routes/VMRoute.tsx
new file mode 100644
index 0000000..4bfb42f
--- /dev/null
+++ b/virtweb_frontend/src/routes/VMRoute.tsx
@@ -0,0 +1,56 @@
+import { useNavigate, useParams } from "react-router-dom";
+import { VMApi, VMInfo, VMState } from "../api/VMApi";
+import React from "react";
+import { AsyncWidget } from "../widgets/AsyncWidget";
+import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer";
+import { VMDetails } from "../widgets/vms/VMDetails";
+import { VMStatusWidget } from "../widgets/vms/VMStatusWidget";
+import { Button } from "@mui/material";
+
+export function VMRoute(): React.ReactElement {
+ const { uuid } = useParams();
+
+ const [vm, setVM] = React.useState();
+
+ const load = async () => {
+ setVM(await VMApi.GetSingle(uuid!));
+ };
+
+ return (
+ }
+ />
+ );
+}
+
+function VMRouteBody(p: { vm: VMInfo }): React.ReactElement {
+ const navigate = useNavigate();
+
+ const [state, setState] = React.useState();
+
+ return (
+
+
+
+ {(state === "Shutdown" || state === "Shutoff") && (
+
+ )}
+
+ }
+ >
+
+
+ );
+}
diff --git a/virtweb_frontend/src/widgets/forms/CheckboxInput.tsx b/virtweb_frontend/src/widgets/forms/CheckboxInput.tsx
index e3b6ecd..b8c27d7 100644
--- a/virtweb_frontend/src/widgets/forms/CheckboxInput.tsx
+++ b/virtweb_frontend/src/widgets/forms/CheckboxInput.tsx
@@ -6,15 +6,16 @@ export function CheckboxInput(p: {
checked: boolean | undefined;
onValueChange: (v: boolean) => void;
}): React.ReactElement {
- if (!p.editable && p.checked)
- return {p.label};
+ //if (!p.editable && p.checked)
+ // return {p.label};
- if (!p.editable) return <>>;
+ //if (!p.editable) return <>>;
return (
p.onValueChange(e.target.checked)}
/>
diff --git a/virtweb_frontend/src/widgets/forms/SelectInput.tsx b/virtweb_frontend/src/widgets/forms/SelectInput.tsx
index f0a9f5a..fc02b2c 100644
--- a/virtweb_frontend/src/widgets/forms/SelectInput.tsx
+++ b/virtweb_frontend/src/widgets/forms/SelectInput.tsx
@@ -8,16 +8,16 @@ export interface SelectOption {
export function SelectInput(p: {
value?: string;
- editing: boolean;
+ editable: boolean;
label: string;
options: SelectOption[];
onValueChange: (o?: string) => void;
}): React.ReactElement {
- if (!p.editing && !p.value) return <>>;
+ if (!p.editable && !p.value) return <>>;
- if (!p.editing) {
+ if (!p.editable) {
const value = p.options.find((o) => o.value === p.value)?.label;
- return ;
+ return ;
}
return (
diff --git a/virtweb_frontend/src/widgets/vms/VMDetails.tsx b/virtweb_frontend/src/widgets/vms/VMDetails.tsx
new file mode 100644
index 0000000..59e6bb4
--- /dev/null
+++ b/virtweb_frontend/src/widgets/vms/VMDetails.tsx
@@ -0,0 +1,138 @@
+import { Grid, Paper, Typography } from "@mui/material";
+import { PropsWithChildren } from "react";
+import { validate as validateUUID } from "uuid";
+import { ServerApi } from "../../api/ServerApi";
+import { VMInfo } from "../../api/VMApi";
+import { CheckboxInput } from "../forms/CheckboxInput";
+import { SelectInput } from "../forms/SelectInput";
+import { TextInput } from "../forms/TextInput";
+
+export function VMDetails(p: {
+ vm: VMInfo;
+ editable: boolean;
+ onChange?: () => void;
+}): React.ReactElement {
+ return (
+
+ {/* Metadata section */}
+
+ {
+ p.vm.name = v ?? "";
+ p.onChange?.();
+ }}
+ size={ServerApi.Config.constraints.name_size}
+ />
+
+
+
+ {
+ p.vm.genid = v;
+ p.onChange?.();
+ }}
+ checkValue={(v) => validateUUID(v)}
+ />
+
+ {
+ p.vm.title = v;
+ p.onChange?.();
+ }}
+ size={ServerApi.Config.constraints.title_size}
+ />
+
+ {
+ p.vm.description = v;
+ p.onChange?.();
+ }}
+ multiline={true}
+ />
+
+
+ {/* General section */}
+
+ {
+ p.vm.architecture = v! as any;
+ p.onChange?.();
+ }}
+ value={p.vm.architecture}
+ options={[
+ { label: "i686", value: "i686" },
+ { label: "x86_64", value: "x86_64" },
+ ]}
+ />
+
+ {
+ p.vm.boot_type = v! as any;
+ p.onChange?.();
+ }}
+ value={p.vm.boot_type}
+ options={[
+ { label: "UEFI with Secure Boot", value: "UEFISecureBoot" },
+ { label: "UEFI", value: "UEFI" },
+ ]}
+ />
+
+ {
+ p.vm.memory = Number(v ?? "0");
+ p.onChange?.();
+ }}
+ checkValue={(v) =>
+ Number(v) > ServerApi.Config.constraints.memory_size.min &&
+ Number(v) < ServerApi.Config.constraints.memory_size.max
+ }
+ />
+
+ {
+ p.vm.vnc_access = v;
+ p.onChange?.();
+ }}
+ />
+
+
+ );
+}
+
+function EditSection(
+ p: { title: string } & PropsWithChildren
+): React.ReactElement {
+ return (
+
+
+
+ {p.title}
+
+ {p.children}
+
+
+ );
+}
diff --git a/virtweb_frontend/src/widgets/vms/VMStatusWidget.tsx b/virtweb_frontend/src/widgets/vms/VMStatusWidget.tsx
index 66c0305..118e833 100644
--- a/virtweb_frontend/src/widgets/vms/VMStatusWidget.tsx
+++ b/virtweb_frontend/src/widgets/vms/VMStatusWidget.tsx
@@ -11,7 +11,7 @@ import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider";
import { useSnackbar } from "../../hooks/providers/SnackbarProvider";
export function VMStatusWidget(p: {
- d: VMInfo;
+ vm: VMInfo;
onChange?: (s: VMState) => void;
}): React.ReactElement {
const snackbar = useSnackbar();
@@ -20,7 +20,7 @@ export function VMStatusWidget(p: {
const refresh = async () => {
try {
- const s = await VMApi.GetState(p.d);
+ const s = await VMApi.GetState(p.vm);
if (s !== state) p.onChange?.(s);
setState(s);
} catch (e) {
@@ -32,6 +32,7 @@ export function VMStatusWidget(p: {
const changedAction = () => setState(undefined);
React.useEffect(() => {
+ refresh();
const i = setInterval(() => refresh(), 3000);
return () => clearInterval(i);
@@ -54,7 +55,7 @@ export function VMStatusWidget(p: {
cond={["Shutdown", "Shutoff", "Crashed"]}
icon={}
tooltip="Start the Virtual Machine"
- performAction={() => VMApi.StartVM(p.d)}
+ performAction={() => VMApi.StartVM(p.vm)}
onExecuted={changedAction}
/>
@@ -64,7 +65,7 @@ export function VMStatusWidget(p: {
cond={["Paused", "PowerManagementSuspended"]}
icon={}
tooltip="Resume the Virtual Machine"
- performAction={() => VMApi.ResumeVM(p.d)}
+ performAction={() => VMApi.ResumeVM(p.vm)}
onExecuted={changedAction}
/>
@@ -75,7 +76,7 @@ export function VMStatusWidget(p: {
icon={}
tooltip="Suspend the Virtual Machine"
confirmMessage="Do you really want to supsend this VM?"
- performAction={() => VMApi.SuspendVM(p.d)}
+ performAction={() => VMApi.SuspendVM(p.vm)}
onExecuted={changedAction}
/>
@@ -86,7 +87,7 @@ export function VMStatusWidget(p: {
icon={}
tooltip="Shutdown the Virtual Machine"
confirmMessage="Do you really want to shutdown this VM?"
- performAction={() => VMApi.ShutdownVM(p.d)}
+ performAction={() => VMApi.ShutdownVM(p.vm)}
onExecuted={changedAction}
/>
@@ -97,7 +98,7 @@ export function VMStatusWidget(p: {
icon={}
tooltip="Kill the Virtual Machine"
confirmMessage="Do you really want to kill this VM? This could lead to data loss / corruption!"
- performAction={() => VMApi.KillVM(p.d)}
+ performAction={() => VMApi.KillVM(p.vm)}
onExecuted={changedAction}
/>
@@ -108,7 +109,7 @@ export function VMStatusWidget(p: {
icon={}
tooltip="Reset the Virtual Machine"
confirmMessage="Do you really want to reset this VM?"
- performAction={() => VMApi.ResetVM(p.d)}
+ performAction={() => VMApi.ResetVM(p.vm)}
onExecuted={changedAction}
/>