Can view from web UI XML definition of domains

This commit is contained in:
2023-12-08 18:14:01 +01:00
parent 74b77be013
commit 82447a0018
13 changed files with 632 additions and 135 deletions

View File

@ -25,6 +25,7 @@ import { OIDCCbRoute } from "./routes/auth/OIDCCbRoute";
import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage";
import { BaseLoginPage } from "./widgets/BaseLoginPage";
import { ViewNetworkRoute } from "./routes/ViewNetworkRoute";
import { VMXMLRoute } from "./routes/VMXMLRoute";
interface AuthContext {
signedIn: boolean;
@ -52,6 +53,7 @@ export function App() {
<Route path="vm/:uuid" element={<VMRoute />} />
<Route path="vm/:uuid/edit" element={<EditVMRoute />} />
<Route path="vm/:uuid/vnc" element={<VNCRoute />} />
<Route path="vm/:uuid/xml" element={<VMXMLRoute />} />
<Route path="net" element={<NetworksListRoute />} />
<Route path="net/new" element={<CreateNetworkRoute />} />

View File

@ -106,7 +106,14 @@ export class APIClient {
// JSON response
if (res.headers.get("content-type") === "application/json")
data = await res.json();
// Binary file
// Text / XML response
else if (
["application/xml", "text/plain"].includes(
res.headers.get("content-type") ?? ""
)
)
data = await res.text();
// Binary file, tracking download progress
else if (res.body !== null && args.downProgress) {
// Track download progress
const contentEncoding = res.headers.get("content-encoding");

View File

@ -113,6 +113,10 @@ export class VMInfo implements VMInfoInterface {
get VNCURL(): string {
return `/vm/${this.uuid}/vnc`;
}
get XMLURL(): string {
return `/vm/${this.uuid}/xml`;
}
}
export class VMApi {
@ -154,6 +158,18 @@ export class VMApi {
return new VMInfo(data);
}
/**
* Get the source XML configuration of a domain for debugging purposes
*/
static async GetSingleXML(uuid: string): Promise<string> {
return (
await APIClient.exec({
uri: `/vm/${uuid}/src`,
method: "GET",
})
).data;
}
/**
* Update the information about a single VM
*/

View File

@ -5,7 +5,10 @@ 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";
import { Button, IconButton } from "@mui/material";
import Icon from "@mdi/react";
import { mdiXml } from "@mdi/js";
import { RouterLink } from "../widgets/RouterLink";
export function VMRoute(): React.ReactElement {
const { uuid } = useParams();
@ -35,9 +38,15 @@ function VMRouteBody(p: { vm: VMInfo }): React.ReactElement {
<VirtWebRouteContainer
label={`VM ${p.vm.name}`}
actions={
<span>
<span style={{ display: "inline-flex", alignItems: "center" }}>
<VMStatusWidget vm={p.vm} onChange={setState} />
<RouterLink to={p.vm.XMLURL}>
<IconButton size="small">
<Icon path={mdiXml} style={{ width: "1rem" }} />
</IconButton>
</RouterLink>
{(state === "Shutdown" || state === "Shutoff") && (
<Button
variant="contained"

View File

@ -0,0 +1,60 @@
import React from "react";
import { useParams } from "react-router-dom";
import { Light as SyntaxHighlighter } from "react-syntax-highlighter";
import xml from "react-syntax-highlighter/dist/esm/languages/hljs/xml";
import { dracula } from "react-syntax-highlighter/dist/esm/styles/hljs";
import xmlFormat from "xml-formatter";
import { VMApi, VMInfo } from "../api/VMApi";
import { AsyncWidget } from "../widgets/AsyncWidget";
import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer";
import { IconButton } from "@mui/material";
import { RouterLink } from "../widgets/RouterLink";
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
SyntaxHighlighter.registerLanguage("xml", xml);
export function VMXMLRoute(): React.ReactElement {
const { uuid } = useParams();
const [vm, setVM] = React.useState<VMInfo | undefined>();
const [src, setSrc] = React.useState<string | undefined>();
const load = async () => {
setVM(await VMApi.GetSingle(uuid!));
setSrc(await VMApi.GetSingleXML(uuid!));
};
return (
<AsyncWidget
loadKey={uuid}
load={load}
errMsg="Failed to load VM information!"
build={() => <XMLRouteInner vm={vm!} src={src!} />}
/>
);
}
function XMLRouteInner(p: { vm: VMInfo; src: string }): React.ReactElement {
const xml = xmlFormat(p.src);
return (
<VirtWebRouteContainer
label={`XML definition of ${p.vm.name}`}
actions={
<RouterLink to={p.vm.ViewURL}>
<IconButton>
<ArrowBackIcon />
</IconButton>
</RouterLink>
}
>
<SyntaxHighlighter
language="xml"
style={dracula}
customStyle={{ fontSize: "120%" }}
>
{xml}
</SyntaxHighlighter>
</VirtWebRouteContainer>
);
}

View File

@ -94,7 +94,7 @@ function VNCInner(p: { vm: VMInfo }): React.ReactElement {
return <p>Please wait, connecting to the machine...</p>;
return (
<div ref={vncRef}>
<div ref={vncRef} style={{ display: "flex" }}>
{/* Controls */}
<div>
<IconButton onClick={goBack}>

View File

@ -1,60 +0,0 @@
import { FormControl, Input, InputLabel } from "@mui/material";
import { TextInput } from "./TextInput";
import { IMaskInput } from "react-imask";
import React from "react";
interface CustomProps {
onChange: (event: { target: { name: string; value: string } }) => void;
name: string;
placeholder: string;
}
const TextMaskCustom = React.forwardRef<HTMLInputElement, CustomProps>(
function TextMaskCustom(props, ref) {
const { onChange, placeholder, ...other } = props;
return (
<IMaskInput
{...other}
mask={placeholder}
definitions={{
"#": /[1-9]/,
}}
inputRef={ref}
onAccept={(value: any) =>
onChange({ target: { name: props.name, value } })
}
overwrite
/>
);
}
);
export function TextMaskInput(p: {
label: string;
editable: boolean;
value?: string;
onValueChange?: (newVal: string | undefined) => void;
mask: string;
}): React.ReactElement {
const id = React.useRef(Math.random());
if (!p.editable) return <TextInput {...p} />;
return (
<FormControl variant="standard" fullWidth>
<InputLabel htmlFor={`mi-${id.current}`}>{p.label}</InputLabel>
<Input
fullWidth
value={p.value ?? ""}
onChange={(c) =>
p.onValueChange?.(
c.target.value.length === 0 ? undefined : c.target.value
)
}
id={`mi-${id.current}`}
inputComponent={TextMaskCustom as any}
placeholder={p.mask}
/>
</FormControl>
);
}