Add groups support (#146)
All checks were successful
continuous-integration/drone/push Build is passing

Reviewed-on: #146
This commit is contained in:
2024-12-06 18:06:01 +00:00
parent aa9222bd22
commit 4c6608bf55
14 changed files with 874 additions and 67 deletions

View File

@ -0,0 +1,177 @@
import { Button, Spinner, Toolbar, Tooltip } from "@fluentui/react-components";
import {
ArrowResetRegular,
PauseRegular,
PlayCircleRegular,
PlayFilled,
PowerRegular,
StopRegular,
} from "@fluentui/react-icons";
import React from "react";
import { GroupApi, TreatmentResult } from "../api/GroupApi";
import { VMGroup } from "../api/ServerApi";
import { VMInfo, VMState } from "../api/VMApi";
import { useAlert } from "../hooks/providers/AlertDialogProvider";
import { useConfirm } from "../hooks/providers/ConfirmDialogProvider";
import { useToast } from "../hooks/providers/ToastProvider";
export function GroupVMAction(p: {
group: VMGroup;
state?: VMState;
vm?: VMInfo;
}): React.ReactElement {
return (
<Toolbar>
<GroupVMButton
enabled={p.group.can_start}
icon={<PlayFilled />}
tooltip="Start"
group={p.group}
vm={p.vm}
allowedStates={["Shutdown", "Shutoff", "Crashed"]}
currState={p.state}
needConfirm={false}
action={GroupApi.StartVM}
/>
<GroupVMButton
enabled={p.group.can_suspend}
icon={<PauseRegular />}
tooltip="Suspend"
group={p.group}
vm={p.vm}
allowedStates={["Running"]}
currState={p.state}
needConfirm={true}
action={GroupApi.SuspendVM}
/>
<GroupVMButton
enabled={p.group.can_resume}
icon={<PlayCircleRegular />}
tooltip="Resume"
group={p.group}
vm={p.vm}
allowedStates={["Paused", "PowerManagementSuspended"]}
currState={p.state}
needConfirm={false}
action={GroupApi.ResumeVM}
/>
<GroupVMButton
enabled={p.group.can_shutdown}
icon={<PowerRegular />}
tooltip="Shutdown"
group={p.group}
vm={p.vm}
allowedStates={["Running"]}
currState={p.state}
needConfirm={true}
action={GroupApi.ShutdownVM}
/>
<GroupVMButton
enabled={p.group.can_kill}
icon={<StopRegular />}
tooltip="Kill"
group={p.group}
vm={p.vm}
allowedStates={[
"Running",
"Paused",
"PowerManagementSuspended",
"Blocked",
]}
currState={p.state}
needConfirm={true}
action={GroupApi.KillVM}
/>
<GroupVMButton
enabled={p.group.can_reset}
icon={<ArrowResetRegular />}
tooltip="Reset"
group={p.group}
vm={p.vm}
allowedStates={[
"Running",
"Paused",
"PowerManagementSuspended",
"Blocked",
]}
currState={p.state}
needConfirm={true}
action={GroupApi.ResetVM}
/>
</Toolbar>
);
}
function GroupVMButton(p: {
enabled: boolean;
icon: React.ReactElement;
action: (group: VMGroup, vm?: VMInfo) => Promise<TreatmentResult>;
tooltip: string;
currState?: VMState;
allowedStates: VMState[];
group: VMGroup;
vm?: VMInfo;
needConfirm: boolean;
}): React.ReactElement {
const toast = useToast();
const confirm = useConfirm();
const alert = useAlert();
const [running, setRunning] = React.useState(false);
const target = p.vm
? `the VM ${p.vm.name}`
: `all the VM of the group ${p.group.id}`;
const allowed =
!p.vm || (p.currState && p.allowedStates.includes(p.currState));
const perform = async () => {
if (running || !allowed) return;
try {
if (
(!p.vm || p.needConfirm) &&
!(await confirm(
`Do you want to perform ${p.tooltip} action on ${target}?`,
`Confirmation`,
p.tooltip
))
) {
return;
}
setRunning(true);
const result = await p.action(p.group, p.vm);
toast(
p.tooltip,
`${p.tooltip} action on ${target}: ${result.ok} OK / ${result.failed} Failed`,
"success"
);
} catch (e) {
console.error("Failed to perform group action!", e);
alert(`Failed to perform ${p.tooltip} action on ${target}: ${e}`);
} finally {
setRunning(false);
}
};
if (!p.enabled) return <></>;
return (
<Tooltip
content={`${p.tooltip} ${target}`}
relationship="description"
withArrow
>
<Button
icon={running ? <Spinner size="tiny" /> : p.icon}
onClick={allowed ? perform : undefined}
disabled={!allowed}
appearance="subtle"
/>
</Tooltip>
);
}

View File

@ -0,0 +1,171 @@
import {
Button,
Card,
Dialog,
DialogActions,
DialogBody,
DialogContent,
DialogSurface,
DialogTitle,
DialogTrigger,
Table,
TableBody,
TableCell,
TableCellActions,
TableCellLayout,
TableHeader,
TableHeaderCell,
TableRow,
Title3,
Tooltip,
} from "@fluentui/react-components";
import { Desktop24Regular, ScreenshotRegular } from "@fluentui/react-icons";
import { filesize } from "filesize";
import React from "react";
import { GroupApi, GroupVMState } from "../api/GroupApi";
import { Rights, VMGroup } from "../api/ServerApi";
import { VMInfo } from "../api/VMApi";
import { useToast } from "../hooks/providers/ToastProvider";
import { GroupVMAction } from "./GroupVMAction";
import { VMLiveScreenshot } from "./VMLiveScreenshot";
export function GroupsWidget(p: { rights: Rights }): React.ReactElement {
return (
<>
{p.rights.groups.map((g) => (
<GroupInfo group={g} />
))}
</>
);
}
function GroupInfo(p: { group: VMGroup }): React.ReactElement {
const toast = useToast();
const [state, setState] = React.useState<GroupVMState | undefined>();
const [screenshotVM, setScreenshotVM] = React.useState<VMInfo | undefined>();
const load = async () => {
const newState = await GroupApi.State(p.group);
if (state !== newState) setState(newState);
};
const screenshot = (vm: VMInfo) => {
setScreenshotVM(vm);
};
React.useEffect(() => {
const interval = setInterval(async () => {
try {
if (p.group.can_get_state) await load();
} catch (e) {
console.error(e);
toast(
"Error",
`Failed to refresh group ${p.group.id} VMs status!`,
"error"
);
}
}, 1000);
return () => clearInterval(interval);
});
return (
<>
<Card
style={{
margin: "50px 10px",
display: "flex",
flexDirection: "column",
}}
>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<Title3 style={{ marginLeft: "10px" }}>{p.group.id}</Title3>
<GroupVMAction group={p.group} />
</div>
<Table sortable>
<TableHeader>
<TableRow>
<TableHeaderCell>VM</TableHeaderCell>
<TableHeaderCell>Resources</TableHeaderCell>
<TableHeaderCell>State</TableHeaderCell>
<TableHeaderCell>Actions</TableHeaderCell>
</TableRow>
</TableHeader>
<TableBody>
{p.group.vms.map((item) => (
<TableRow key={item.uuid}>
<TableCell>
<TableCellLayout
media={<Desktop24Regular />}
appearance="primary"
description={item.description}
>
{item.name}
</TableCellLayout>
<TableCellActions>
{state?.[item.uuid] === "Running" && (
<Tooltip
relationship="description"
content={"Take a screenshot of the VM screen"}
withArrow
>
<Button
icon={<ScreenshotRegular />}
appearance="subtle"
aria-label="Edit"
disabled={!p.group.can_screenshot}
onClick={() => screenshot(item)}
/>
</Tooltip>
)}
</TableCellActions>
</TableCell>
<TableCell>
{item.architecture} &bull; RAM :{" "}
{filesize(item.memory * 1000 * 1000)} &bull;{" "}
{item.number_vcpu} vCPU
</TableCell>
<TableCell>{state?.[item.uuid] ?? ""}</TableCell>
<TableCell>
<GroupVMAction
group={p.group}
state={state?.[item.uuid]}
vm={item}
/>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Card>
<Dialog
open={!!screenshotVM}
onOpenChange={(_event, _data) => {
if (!screenshotVM) setScreenshotVM(undefined);
}}
>
<DialogSurface>
<DialogBody>
<DialogTitle>
<em>{screenshotVM?.name}</em> screen
</DialogTitle>
<DialogContent>
<VMLiveScreenshot vm={screenshotVM!} group={p.group} />
</DialogContent>
<DialogActions>
<DialogTrigger disableButtonEnhancement>
<Button
appearance="secondary"
onClick={() => setScreenshotVM(undefined)}
>
Close
</Button>
</DialogTrigger>
</DialogActions>
</DialogBody>
</DialogSurface>
</Dialog>
</>
);
}

View File

@ -1,8 +1,13 @@
import React from "react";
import { GroupApi } from "../api/GroupApi";
import { VMGroup } from "../api/ServerApi";
import { VMApi, VMInfo } from "../api/VMApi";
import { useToast } from "../hooks/providers/ToastProvider";
export function VMLiveScreenshot(p: { vm: VMInfo }): React.ReactElement {
export function VMLiveScreenshot(p: {
vm: VMInfo;
group?: VMGroup;
}): React.ReactElement {
const toast = useToast();
const [screenshotURL, setScreenshotURL] = React.useState<
@ -14,7 +19,9 @@ export function VMLiveScreenshot(p: { vm: VMInfo }): React.ReactElement {
React.useEffect(() => {
const refresh = async () => {
try {
const screenshot = await VMApi.Screenshot(p.vm);
const screenshot = p.group
? await GroupApi.ScreenshotVM(p.group, p.vm)
: await VMApi.Screenshot(p.vm);
const u = URL.createObjectURL(screenshot);
setScreenshotURL(u);
} catch (e) {

View File

@ -22,11 +22,11 @@ import {
import { filesize } from "filesize";
import React from "react";
import { Rights } from "../api/ServerApi";
import { VMApi, VMInfo, VMState } from "../api/VMApi";
import { VMApi, VMInfo, VMInfoAndCaps, VMState } from "../api/VMApi";
import { useConfirm } from "../hooks/providers/ConfirmDialogProvider";
import { useToast } from "../hooks/providers/ToastProvider";
import { VMLiveScreenshot } from "./VMLiveScreenshot";
import { SectionContainer } from "./SectionContainer";
import { VMLiveScreenshot } from "./VMLiveScreenshot";
const useStyles = makeStyles({
body1Stronger: typographyStyles.body1Stronger,
@ -54,7 +54,7 @@ export function VirtualMachinesWidget(p: {
);
}
function VMWidget(p: { vm: VMInfo }): React.ReactElement {
function VMWidget(p: { vm: VMInfoAndCaps }): React.ReactElement {
const toast = useToast();
const [state, setState] = React.useState<VMState | undefined>();
@ -189,7 +189,10 @@ function VMWidget(p: { vm: VMInfo }): React.ReactElement {
);
}
function VMPreview(p: { vm: VMInfo; state?: VMState }): React.ReactElement {
function VMPreview(p: {
vm: VMInfoAndCaps;
state?: VMState;
}): React.ReactElement {
const styles = useStyles();
if (!p.vm.can_screenshot || p.state !== "Running") {
return (