Can enable autostart of VMs
This commit is contained in:
		@@ -308,3 +308,39 @@ impl Handler<ScreenshotDomainReq> for LibVirtActor {
 | 
			
		||||
        Ok(png_out.into_inner())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Message)]
 | 
			
		||||
#[rtype(result = "anyhow::Result<bool>")]
 | 
			
		||||
pub struct IsDomainAutostart(pub DomainXMLUuid);
 | 
			
		||||
 | 
			
		||||
impl Handler<IsDomainAutostart> for LibVirtActor {
 | 
			
		||||
    type Result = anyhow::Result<bool>;
 | 
			
		||||
 | 
			
		||||
    fn handle(&mut self, msg: IsDomainAutostart, _ctx: &mut Self::Context) -> Self::Result {
 | 
			
		||||
        log::debug!(
 | 
			
		||||
            "Check if autostart is enabled for a domain: {}",
 | 
			
		||||
            msg.0.as_string()
 | 
			
		||||
        );
 | 
			
		||||
        let domain = Domain::lookup_by_uuid_string(&self.m, &msg.0.as_string())?;
 | 
			
		||||
        Ok(domain.get_autostart()?)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Message)]
 | 
			
		||||
#[rtype(result = "anyhow::Result<()>")]
 | 
			
		||||
pub struct SetDomainAutostart(pub DomainXMLUuid, pub bool);
 | 
			
		||||
 | 
			
		||||
impl Handler<SetDomainAutostart> for LibVirtActor {
 | 
			
		||||
    type Result = anyhow::Result<()>;
 | 
			
		||||
 | 
			
		||||
    fn handle(&mut self, msg: SetDomainAutostart, _ctx: &mut Self::Context) -> Self::Result {
 | 
			
		||||
        log::debug!(
 | 
			
		||||
            "Set autostart enabled={} for a domain: {}",
 | 
			
		||||
            msg.1,
 | 
			
		||||
            msg.0.as_string()
 | 
			
		||||
        );
 | 
			
		||||
        let domain = Domain::lookup_by_uuid_string(&self.m, &msg.0.as_string())?;
 | 
			
		||||
        domain.set_autostart(msg.1)?;
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -90,6 +90,28 @@ pub async fn update(
 | 
			
		||||
    Ok(HttpResponse::Ok().finish())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize, serde::Deserialize)]
 | 
			
		||||
pub struct VMAutostart {
 | 
			
		||||
    autostart: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Get autostart value of a vm
 | 
			
		||||
pub async fn get_autostart(client: LibVirtReq, id: web::Path<SingleVMUUidReq>) -> HttpResult {
 | 
			
		||||
    Ok(HttpResponse::Ok().json(VMAutostart {
 | 
			
		||||
        autostart: client.is_domain_autostart(id.uid).await?,
 | 
			
		||||
    }))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Configure autostart value for a vm
 | 
			
		||||
pub async fn set_autostart(
 | 
			
		||||
    client: LibVirtReq,
 | 
			
		||||
    id: web::Path<SingleVMUUidReq>,
 | 
			
		||||
    body: web::Json<VMAutostart>,
 | 
			
		||||
) -> HttpResult {
 | 
			
		||||
    client.set_domain_autostart(id.uid, body.autostart).await?;
 | 
			
		||||
    Ok(HttpResponse::Accepted().json("OK"))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize)]
 | 
			
		||||
pub struct DeleteVMQuery {
 | 
			
		||||
    keep_files: bool,
 | 
			
		||||
 
 | 
			
		||||
@@ -79,4 +79,19 @@ impl LibVirtClient {
 | 
			
		||||
    pub async fn screenshot_domain(&self, id: DomainXMLUuid) -> anyhow::Result<Vec<u8>> {
 | 
			
		||||
        self.0.send(libvirt_actor::ScreenshotDomainReq(id)).await?
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Get auto-start status of a domain
 | 
			
		||||
    pub async fn is_domain_autostart(&self, id: DomainXMLUuid) -> anyhow::Result<bool> {
 | 
			
		||||
        self.0.send(libvirt_actor::IsDomainAutostart(id)).await?
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn set_domain_autostart(
 | 
			
		||||
        &self,
 | 
			
		||||
        id: DomainXMLUuid,
 | 
			
		||||
        autostart: bool,
 | 
			
		||||
    ) -> anyhow::Result<()> {
 | 
			
		||||
        self.0
 | 
			
		||||
            .send(libvirt_actor::SetDomainAutostart(id, autostart))
 | 
			
		||||
            .await?
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -77,7 +77,6 @@ pub struct VMInfo {
 | 
			
		||||
    pub iso_file: Option<String>,
 | 
			
		||||
    /// Storage - https://access.redhat.com/documentation/fr-fr/red_hat_enterprise_linux/6/html/virtualization_administration_guide/sect-virtualization-virtualized_block_devices-adding_storage_devices_to_guests#sect-Virtualization-Adding_storage_devices_to_guests-Adding_file_based_storage_to_a_guest
 | 
			
		||||
    pub disks: Vec<Disk>,
 | 
			
		||||
    // TODO : autostart
 | 
			
		||||
    // TODO : network interface
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -144,6 +144,14 @@ async fn main() -> std::io::Result<()> {
 | 
			
		||||
            .route("/api/vm/create", web::post().to(vm_controller::create))
 | 
			
		||||
            .route("/api/vm/list", web::get().to(vm_controller::list_all))
 | 
			
		||||
            .route("/api/vm/{uid}", web::get().to(vm_controller::get_single))
 | 
			
		||||
            .route(
 | 
			
		||||
                "/api/vm/{uid}/autostart",
 | 
			
		||||
                web::get().to(vm_controller::get_autostart),
 | 
			
		||||
            )
 | 
			
		||||
            .route(
 | 
			
		||||
                "/api/vm/{uid}/autostart",
 | 
			
		||||
                web::put().to(vm_controller::set_autostart),
 | 
			
		||||
            )
 | 
			
		||||
            .route("/api/vm/{uid}", web::put().to(vm_controller::update))
 | 
			
		||||
            .route("/api/vm/{uid}", web::delete().to(vm_controller::delete))
 | 
			
		||||
            .route("/api/vm/{uid}/start", web::get().to(vm_controller::start))
 | 
			
		||||
 
 | 
			
		||||
@@ -154,6 +154,29 @@ export class VMApi {
 | 
			
		||||
    return new VMInfo(data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Check if autostart is enabled on a VM
 | 
			
		||||
   */
 | 
			
		||||
  static async IsAutostart(vm: VMInfo): Promise<boolean> {
 | 
			
		||||
    return (
 | 
			
		||||
      await APIClient.exec({
 | 
			
		||||
        uri: `/vm/${vm.uuid}/autostart`,
 | 
			
		||||
        method: "GET",
 | 
			
		||||
      })
 | 
			
		||||
    ).data.autostart;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Set autostart status of a VM
 | 
			
		||||
   */
 | 
			
		||||
  static async SetAutostart(vm: VMInfo, enabled: boolean): Promise<void> {
 | 
			
		||||
    await APIClient.exec({
 | 
			
		||||
      uri: `/vm/${vm.uuid}/autostart`,
 | 
			
		||||
      method: "PUT",
 | 
			
		||||
      jsonData: { autostart: enabled },
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get the state of a VM
 | 
			
		||||
   */
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import { Alert, Box, Button, CircularProgress } from "@mui/material";
 | 
			
		||||
import { useEffect, useRef, useState } from "react";
 | 
			
		||||
import React, { useEffect, useRef, useState } from "react";
 | 
			
		||||
 | 
			
		||||
enum State {
 | 
			
		||||
  Loading,
 | 
			
		||||
@@ -13,6 +13,8 @@ export function AsyncWidget(p: {
 | 
			
		||||
  errMsg: string;
 | 
			
		||||
  build: () => React.ReactElement;
 | 
			
		||||
  ready?: boolean;
 | 
			
		||||
  buildLoading?: () => React.ReactElement;
 | 
			
		||||
  buildError?: (e: string) => React.ReactElement;
 | 
			
		||||
  errAdditionalElement?: () => React.ReactElement;
 | 
			
		||||
}): React.ReactElement {
 | 
			
		||||
  const [state, setState] = useState(State.Loading);
 | 
			
		||||
@@ -39,53 +41,57 @@ export function AsyncWidget(p: {
 | 
			
		||||
 | 
			
		||||
  if (state === State.Error)
 | 
			
		||||
    return (
 | 
			
		||||
      <Box
 | 
			
		||||
        component="div"
 | 
			
		||||
        sx={{
 | 
			
		||||
          display: "flex",
 | 
			
		||||
          justifyContent: "center",
 | 
			
		||||
          alignItems: "center",
 | 
			
		||||
          height: "100%",
 | 
			
		||||
          flex: "1",
 | 
			
		||||
          flexDirection: "column",
 | 
			
		||||
          backgroundColor: (theme) =>
 | 
			
		||||
            theme.palette.mode === "light"
 | 
			
		||||
              ? theme.palette.grey[100]
 | 
			
		||||
              : theme.palette.grey[900],
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <Alert
 | 
			
		||||
          variant="outlined"
 | 
			
		||||
          severity="error"
 | 
			
		||||
          style={{ margin: "0px 15px 15px 15px" }}
 | 
			
		||||
      p.buildError?.(p.errMsg) ?? (
 | 
			
		||||
        <Box
 | 
			
		||||
          component="div"
 | 
			
		||||
          sx={{
 | 
			
		||||
            display: "flex",
 | 
			
		||||
            justifyContent: "center",
 | 
			
		||||
            alignItems: "center",
 | 
			
		||||
            height: "100%",
 | 
			
		||||
            flex: "1",
 | 
			
		||||
            flexDirection: "column",
 | 
			
		||||
            backgroundColor: (theme) =>
 | 
			
		||||
              theme.palette.mode === "light"
 | 
			
		||||
                ? theme.palette.grey[100]
 | 
			
		||||
                : theme.palette.grey[900],
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          {p.errMsg}
 | 
			
		||||
        </Alert>
 | 
			
		||||
          <Alert
 | 
			
		||||
            variant="outlined"
 | 
			
		||||
            severity="error"
 | 
			
		||||
            style={{ margin: "0px 15px 15px 15px" }}
 | 
			
		||||
          >
 | 
			
		||||
            {p.errMsg}
 | 
			
		||||
          </Alert>
 | 
			
		||||
 | 
			
		||||
        <Button onClick={load}>Try again</Button>
 | 
			
		||||
          <Button onClick={load}>Try again</Button>
 | 
			
		||||
 | 
			
		||||
        {p.errAdditionalElement && p.errAdditionalElement()}
 | 
			
		||||
      </Box>
 | 
			
		||||
          {p.errAdditionalElement && p.errAdditionalElement()}
 | 
			
		||||
        </Box>
 | 
			
		||||
      )
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
  if (state === State.Loading || p.ready === false)
 | 
			
		||||
    return (
 | 
			
		||||
      <Box
 | 
			
		||||
        component="div"
 | 
			
		||||
        sx={{
 | 
			
		||||
          display: "flex",
 | 
			
		||||
          justifyContent: "center",
 | 
			
		||||
          alignItems: "center",
 | 
			
		||||
          height: "100%",
 | 
			
		||||
          flex: "1",
 | 
			
		||||
          backgroundColor: (theme) =>
 | 
			
		||||
            theme.palette.mode === "light"
 | 
			
		||||
              ? theme.palette.grey[100]
 | 
			
		||||
              : theme.palette.grey[900],
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <CircularProgress />
 | 
			
		||||
      </Box>
 | 
			
		||||
      p.buildLoading?.() ?? (
 | 
			
		||||
        <Box
 | 
			
		||||
          component="div"
 | 
			
		||||
          sx={{
 | 
			
		||||
            display: "flex",
 | 
			
		||||
            justifyContent: "center",
 | 
			
		||||
            alignItems: "center",
 | 
			
		||||
            height: "100%",
 | 
			
		||||
            flex: "1",
 | 
			
		||||
            backgroundColor: (theme) =>
 | 
			
		||||
              theme.palette.mode === "light"
 | 
			
		||||
                ? theme.palette.grey[100]
 | 
			
		||||
                : theme.palette.grey[900],
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <CircularProgress />
 | 
			
		||||
        </Box>
 | 
			
		||||
      )
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
  return p.build();
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										70
									
								
								virtweb_frontend/src/widgets/forms/VMAutostartInput.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								virtweb_frontend/src/widgets/forms/VMAutostartInput.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,70 @@
 | 
			
		||||
import { Alert, CircularProgress, Typography } from "@mui/material";
 | 
			
		||||
import { VMApi, VMInfo } from "../../api/VMApi";
 | 
			
		||||
import { AsyncWidget } from "../AsyncWidget";
 | 
			
		||||
import React from "react";
 | 
			
		||||
import { CheckboxInput } from "./CheckboxInput";
 | 
			
		||||
import { useAlert } from "../../hooks/providers/AlertDialogProvider";
 | 
			
		||||
import { useSnackbar } from "../../hooks/providers/SnackbarProvider";
 | 
			
		||||
 | 
			
		||||
export function VMAutostartInput(p: {
 | 
			
		||||
  editable: boolean;
 | 
			
		||||
  vm: VMInfo;
 | 
			
		||||
}): React.ReactElement {
 | 
			
		||||
  const alert = useAlert();
 | 
			
		||||
  const snackbar = useSnackbar();
 | 
			
		||||
 | 
			
		||||
  const [enabled, setEnabled] = React.useState<boolean | undefined>();
 | 
			
		||||
 | 
			
		||||
  const load = async () => {
 | 
			
		||||
    setEnabled(await VMApi.IsAutostart(p.vm));
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const update = async (enabled: boolean) => {
 | 
			
		||||
    try {
 | 
			
		||||
      await VMApi.SetAutostart(p.vm, enabled);
 | 
			
		||||
      snackbar("Autostart status successfully updated!");
 | 
			
		||||
      setEnabled(enabled);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      console.error(e);
 | 
			
		||||
      alert("Failed to update autostart status of the VM!");
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <AsyncWidget
 | 
			
		||||
      loadKey={p.vm.uuid}
 | 
			
		||||
      load={load}
 | 
			
		||||
      errMsg="Failed to check autostart status of the VM!"
 | 
			
		||||
      buildLoading={() => (
 | 
			
		||||
        <Typography>
 | 
			
		||||
          <CircularProgress size={"1rem"} /> Checking for autostart
 | 
			
		||||
        </Typography>
 | 
			
		||||
      )}
 | 
			
		||||
      buildError={(e: string) => <Alert severity="error">{e}</Alert>}
 | 
			
		||||
      build={() => (
 | 
			
		||||
        <VMAutostartInputInner
 | 
			
		||||
          editable={p.editable}
 | 
			
		||||
          enabled={enabled!}
 | 
			
		||||
          setEnabled={update}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function VMAutostartInputInner(p: {
 | 
			
		||||
  editable: boolean;
 | 
			
		||||
  enabled: boolean;
 | 
			
		||||
  setEnabled: (b: boolean) => void;
 | 
			
		||||
}): React.ReactElement {
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      <CheckboxInput
 | 
			
		||||
        editable={p.editable}
 | 
			
		||||
        checked={p.enabled}
 | 
			
		||||
        label="Autostart VM"
 | 
			
		||||
        onValueChange={p.setEnabled}
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										74
									
								
								virtweb_frontend/src/widgets/forms/VMSelectIsoInput.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								virtweb_frontend/src/widgets/forms/VMSelectIsoInput.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,74 @@
 | 
			
		||||
import { filesize } from "filesize";
 | 
			
		||||
import { IsoFile } from "../../api/IsoFilesApi";
 | 
			
		||||
import { SelectInput } from "./SelectInput";
 | 
			
		||||
import {
 | 
			
		||||
  Avatar,
 | 
			
		||||
  IconButton,
 | 
			
		||||
  ListItem,
 | 
			
		||||
  ListItemAvatar,
 | 
			
		||||
  ListItemText,
 | 
			
		||||
  Tooltip,
 | 
			
		||||
} from "@mui/material";
 | 
			
		||||
import DeleteIcon from "@mui/icons-material/Delete";
 | 
			
		||||
import { mdiDisc } from "@mdi/js";
 | 
			
		||||
import Icon from "@mdi/react";
 | 
			
		||||
 | 
			
		||||
export function VMSelectIsoInput(p: {
 | 
			
		||||
  editable: boolean;
 | 
			
		||||
  isoList: IsoFile[];
 | 
			
		||||
  value?: string;
 | 
			
		||||
  onChange: (newVal?: string) => void;
 | 
			
		||||
}): React.ReactElement {
 | 
			
		||||
  if (!p.value && !p.editable) return <></>;
 | 
			
		||||
 | 
			
		||||
  if (p.value) {
 | 
			
		||||
    const iso = p.isoList.find((d) => d.filename == p.value);
 | 
			
		||||
    return (
 | 
			
		||||
      <ListItem
 | 
			
		||||
        secondaryAction={
 | 
			
		||||
          p.editable && (
 | 
			
		||||
            <IconButton
 | 
			
		||||
              edge="end"
 | 
			
		||||
              aria-label="detach iso file"
 | 
			
		||||
              onClick={() => {
 | 
			
		||||
                p.onChange(undefined);
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <Tooltip title="Detach ISO file">
 | 
			
		||||
                <DeleteIcon />
 | 
			
		||||
              </Tooltip>
 | 
			
		||||
            </IconButton>
 | 
			
		||||
          )
 | 
			
		||||
        }
 | 
			
		||||
      >
 | 
			
		||||
        <ListItemAvatar>
 | 
			
		||||
          <Avatar>
 | 
			
		||||
            <Icon path={mdiDisc} />
 | 
			
		||||
          </Avatar>
 | 
			
		||||
        </ListItemAvatar>
 | 
			
		||||
        <ListItemText
 | 
			
		||||
          primary={iso?.filename}
 | 
			
		||||
          secondary={filesize(iso?.size ?? 0)}
 | 
			
		||||
        />
 | 
			
		||||
      </ListItem>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <SelectInput
 | 
			
		||||
      label="ISO file"
 | 
			
		||||
      editable={p.editable}
 | 
			
		||||
      value={p.value}
 | 
			
		||||
      onValueChange={p.onChange}
 | 
			
		||||
      options={[
 | 
			
		||||
        { label: "None", value: undefined },
 | 
			
		||||
        ...p.isoList.map((i) => {
 | 
			
		||||
          return {
 | 
			
		||||
            label: `${i.filename} ${filesize(i.size)}`,
 | 
			
		||||
            value: i.filename,
 | 
			
		||||
          };
 | 
			
		||||
        }),
 | 
			
		||||
      ]}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@@ -12,6 +12,8 @@ import { AsyncWidget } from "../AsyncWidget";
 | 
			
		||||
import React from "react";
 | 
			
		||||
import { filesize } from "filesize";
 | 
			
		||||
import { VMDisksList } from "../forms/VMDisksList";
 | 
			
		||||
import { VMSelectIsoInput } from "../forms/VMSelectIsoInput";
 | 
			
		||||
import { VMAutostartInput } from "../forms/VMAutostartInput";
 | 
			
		||||
 | 
			
		||||
interface DetailsProps {
 | 
			
		||||
  vm: VMInfo;
 | 
			
		||||
@@ -32,13 +34,13 @@ export function VMDetails(p: DetailsProps): React.ReactElement {
 | 
			
		||||
      loadKey={"1"}
 | 
			
		||||
      load={load}
 | 
			
		||||
      errMsg="Failed to load the list of ISO files"
 | 
			
		||||
      build={() => <VMDetailsInner iso={list!} {...p} />}
 | 
			
		||||
      build={() => <VMDetailsInner isoList={list!} {...p} />}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function VMDetailsInner(
 | 
			
		||||
  p: DetailsProps & { iso: IsoFile[] }
 | 
			
		||||
  p: DetailsProps & { isoList: IsoFile[] }
 | 
			
		||||
): React.ReactElement {
 | 
			
		||||
  return (
 | 
			
		||||
    <Grid container spacing={2}>
 | 
			
		||||
@@ -154,27 +156,20 @@ function VMDetailsInner(
 | 
			
		||||
            p.onChange?.();
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
 | 
			
		||||
        {p.vm.uuid && <VMAutostartInput editable={p.editable} vm={p.vm} />}
 | 
			
		||||
      </EditSection>
 | 
			
		||||
 | 
			
		||||
      {/* Storage section */}
 | 
			
		||||
      <EditSection title="Storage">
 | 
			
		||||
        <SelectInput
 | 
			
		||||
          label="ISO file"
 | 
			
		||||
        <VMSelectIsoInput
 | 
			
		||||
          editable={p.editable}
 | 
			
		||||
          isoList={p.isoList}
 | 
			
		||||
          value={p.vm.iso_file}
 | 
			
		||||
          onValueChange={(v) => {
 | 
			
		||||
          onChange={(v) => {
 | 
			
		||||
            p.vm.iso_file = v;
 | 
			
		||||
            p.onChange?.();
 | 
			
		||||
          }}
 | 
			
		||||
          options={[
 | 
			
		||||
            { label: "None", value: undefined },
 | 
			
		||||
            ...p.iso.map((i) => {
 | 
			
		||||
              return {
 | 
			
		||||
                label: `${i.filename} ${filesize(i.size)}`,
 | 
			
		||||
                value: i.filename,
 | 
			
		||||
              };
 | 
			
		||||
            }),
 | 
			
		||||
          ]}
 | 
			
		||||
        />
 | 
			
		||||
        <VMDisksList vm={p.vm} editable={p.editable} onChange={p.onChange} />
 | 
			
		||||
      </EditSection>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user