feat(lite/pool/VMs): ability to export selected VMs (#7174)
This commit is contained in:
parent
4351aad312
commit
511908bb7d
@ -5,6 +5,7 @@
|
||||
- Explicit error if users attempt to connect from a slave host (PR [#7110](https://github.com/vatesfr/xen-orchestra/pull/7110))
|
||||
- More compact UI (PR [#7159](https://github.com/vatesfr/xen-orchestra/pull/7159))
|
||||
- Fix dashboard host patches list (PR [#7169](https://github.com/vatesfr/xen-orchestra/pull/7169))
|
||||
- Ability to export selected VMs (PR [#7174](https://github.com/vatesfr/xen-orchestra/pull/7174))
|
||||
|
||||
## **0.1.5** (2023-11-07)
|
||||
|
||||
|
@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<UiModal>
|
||||
<FormModalLayout :icon="faDisplay">
|
||||
<template #title>
|
||||
{{ $t("export-n-vms-manually", { n: labelWithUrl.length }) }}
|
||||
</template>
|
||||
|
||||
<p>
|
||||
{{ $t("export-vms-manually-information") }}
|
||||
</p>
|
||||
<ul class="list">
|
||||
<li v-for="({ url, label }, index) in labelWithUrl" :key="index">
|
||||
<a :href="url.href" target="_blank">
|
||||
{{ label }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<template #buttons>
|
||||
<ModalDeclineButton />
|
||||
</template>
|
||||
</FormModalLayout>
|
||||
</UiModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import FormModalLayout from "@/components/ui/modals/layouts/FormModalLayout.vue";
|
||||
import ModalDeclineButton from "@/components/ui/modals/ModalDeclineButton.vue";
|
||||
import UiModal from "@/components/ui/modals/UiModal.vue";
|
||||
import type { XenApiVm } from "@/libs/xen-api/xen-api.types";
|
||||
import { useVmCollection } from "@/stores/xen-api/vm.store";
|
||||
import { faDisplay } from "@fortawesome/free-solid-svg-icons";
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
blockedUrls: URL[];
|
||||
}>();
|
||||
|
||||
const { getByOpaqueRef } = useVmCollection();
|
||||
|
||||
const labelWithUrl = computed(() =>
|
||||
props.blockedUrls.map((url) => {
|
||||
const ref = url.searchParams.get("ref") as XenApiVm["$ref"];
|
||||
return {
|
||||
url: url,
|
||||
label: getByOpaqueRef(ref)?.name_label ?? ref,
|
||||
};
|
||||
})
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.list {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
</style>
|
65
@xen-orchestra/lite/src/components/modals/VmExportModal.vue
Normal file
65
@xen-orchestra/lite/src/components/modals/VmExportModal.vue
Normal file
@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<UiModal @submit.prevent="handleSubmit">
|
||||
<FormModalLayout :icon="faDisplay">
|
||||
<template #title>
|
||||
{{ $t("export-n-vms", { n: vmRefs.length }) }}
|
||||
</template>
|
||||
|
||||
<FormInputWrapper
|
||||
light
|
||||
learn-more-url="https://xcp-ng.org/blog/2018/12/19/zstd-compression-for-xcp-ng/"
|
||||
:label="$t('select-compression')"
|
||||
>
|
||||
<FormSelect v-model="compressionType">
|
||||
<option
|
||||
v-for="key in Object.keys(VM_COMPRESSION_TYPE)"
|
||||
:key="key"
|
||||
:value="
|
||||
VM_COMPRESSION_TYPE[key as keyof typeof VM_COMPRESSION_TYPE]
|
||||
"
|
||||
>
|
||||
{{ $t(key.toLowerCase()) }}
|
||||
</option>
|
||||
</FormSelect>
|
||||
</FormInputWrapper>
|
||||
|
||||
<template #buttons>
|
||||
<ModalDeclineButton />
|
||||
<ModalApproveButton>
|
||||
{{ $t("export-n-vms", { n: vmRefs.length }) }}
|
||||
</ModalApproveButton>
|
||||
</template>
|
||||
</FormModalLayout>
|
||||
</UiModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { faDisplay } from "@fortawesome/free-solid-svg-icons";
|
||||
import { inject, ref } from "vue";
|
||||
|
||||
import FormInputWrapper from "@/components/form/FormInputWrapper.vue";
|
||||
import FormSelect from "@/components/form/FormSelect.vue";
|
||||
import FormModalLayout from "@/components/ui/modals/layouts/FormModalLayout.vue";
|
||||
import ModalApproveButton from "@/components/ui/modals/ModalApproveButton.vue";
|
||||
import ModalDeclineButton from "@/components/ui/modals/ModalDeclineButton.vue";
|
||||
import UiModal from "@/components/ui/modals/UiModal.vue";
|
||||
import { IK_MODAL } from "@/types/injection-keys";
|
||||
import { useXenApiStore } from "@/stores/xen-api.store";
|
||||
import { VM_COMPRESSION_TYPE } from "@/libs/xen-api/xen-api.enums";
|
||||
|
||||
import type { XenApiVm } from "@/libs/xen-api/xen-api.types";
|
||||
|
||||
const props = defineProps<{
|
||||
vmRefs: XenApiVm["$ref"][];
|
||||
}>();
|
||||
|
||||
const modal = inject(IK_MODAL)!;
|
||||
|
||||
const compressionType = ref(VM_COMPRESSION_TYPE.DISABLED);
|
||||
|
||||
const handleSubmit = () => {
|
||||
const xenApi = useXenApiStore().getXapi();
|
||||
xenApi.vm.export(props.vmRefs, compressionType.value);
|
||||
modal.approve();
|
||||
};
|
||||
</script>
|
@ -1,51 +1,50 @@
|
||||
<template>
|
||||
<MenuItem :icon="faFileExport">
|
||||
{{ $t("export") }}
|
||||
<template #submenu>
|
||||
<MenuItem
|
||||
v-tooltip="{ content: $t('coming-soon'), placement: 'left' }"
|
||||
:icon="faDisplay"
|
||||
>
|
||||
{{ $t("export-vms") }}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
:icon="faCode"
|
||||
@click="
|
||||
exportVmsAsJsonFile(vms, `vms_${new Date().toISOString()}.json`)
|
||||
"
|
||||
>
|
||||
{{ $t("export-table-to", { type: ".json" }) }}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
:icon="faFileCsv"
|
||||
@click="exportVmsAsCsvFile(vms, `vms_${new Date().toISOString()}.csv`)"
|
||||
>
|
||||
{{ $t("export-table-to", { type: ".csv" }) }}
|
||||
</MenuItem>
|
||||
</template>
|
||||
<MenuItem
|
||||
v-tooltip="
|
||||
vmRefs.length > 0 &&
|
||||
!isSomeExportable &&
|
||||
$t('no-selected-vm-can-be-exported')
|
||||
"
|
||||
:icon="faDisplay"
|
||||
:disabled="isDisabled"
|
||||
@click="openModal"
|
||||
>
|
||||
{{ $t("export-vms") }}
|
||||
</MenuItem>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useVmCollection } from "@/stores/xen-api/vm.store";
|
||||
import { computed } from "vue";
|
||||
import { exportVmsAsCsvFile, exportVmsAsJsonFile } from "@/libs/vm";
|
||||
import { faDisplay } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import MenuItem from "@/components/menu/MenuItem.vue";
|
||||
import {
|
||||
faCode,
|
||||
faDisplay,
|
||||
faFileCsv,
|
||||
faFileExport,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { DisabledContext } from "@/context";
|
||||
import { useContext } from "@/composables/context.composable";
|
||||
import { useModal } from "@/composables/modal.composable";
|
||||
import { useVmCollection } from "@/stores/xen-api/vm.store";
|
||||
import { VM_OPERATION } from "@/libs/xen-api/xen-api.enums";
|
||||
import { vTooltip } from "@/directives/tooltip.directive";
|
||||
|
||||
import type { XenApiVm } from "@/libs/xen-api/xen-api.types";
|
||||
|
||||
const props = defineProps<{
|
||||
vmRefs: XenApiVm["$ref"][];
|
||||
}>();
|
||||
const props = defineProps<{ vmRefs: XenApiVm["$ref"][] }>();
|
||||
|
||||
const { getByOpaqueRef: getVm } = useVmCollection();
|
||||
const vms = computed(() =>
|
||||
props.vmRefs.map(getVm).filter((vm): vm is XenApiVm => vm !== undefined)
|
||||
const { getByOpaqueRefs, areSomeOperationAllowed } = useVmCollection();
|
||||
|
||||
const isParentDisabled = useContext(DisabledContext);
|
||||
|
||||
const isSomeExportable = computed(() =>
|
||||
getByOpaqueRefs(props.vmRefs).some((vm) =>
|
||||
areSomeOperationAllowed(vm, VM_OPERATION.EXPORT)
|
||||
)
|
||||
);
|
||||
|
||||
const isDisabled = computed(
|
||||
() => isParentDisabled.value || !isSomeExportable.value
|
||||
);
|
||||
|
||||
const openModal = () => {
|
||||
useModal(() => import("@/components/modals/VmExportModal.vue"), {
|
||||
vmRefs: props.vmRefs,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<MenuItem :icon="faFileExport">
|
||||
{{ $t("export") }}
|
||||
<template #submenu>
|
||||
<VmActionExportItem :vmRefs="vmRefs" />
|
||||
<MenuItem
|
||||
:icon="faCode"
|
||||
@click="
|
||||
exportVmsAsJsonFile(vms, `vms_${new Date().toISOString()}.json`)
|
||||
"
|
||||
>
|
||||
{{ $t("export-table-to", { type: ".json" }) }}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
:icon="faFileCsv"
|
||||
@click="exportVmsAsCsvFile(vms, `vms_${new Date().toISOString()}.csv`)"
|
||||
>
|
||||
{{ $t("export-table-to", { type: ".csv" }) }}
|
||||
</MenuItem>
|
||||
</template>
|
||||
</MenuItem>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useVmCollection } from "@/stores/xen-api/vm.store";
|
||||
import { computed } from "vue";
|
||||
import { exportVmsAsCsvFile, exportVmsAsJsonFile } from "@/libs/vm";
|
||||
import MenuItem from "@/components/menu/MenuItem.vue";
|
||||
import VmActionExportItem from "@/components/vm/VmActionItems/VmActionExportItem.vue";
|
||||
import {
|
||||
faCode,
|
||||
faFileCsv,
|
||||
faFileExport,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import type { XenApiVm } from "@/libs/xen-api/xen-api.types";
|
||||
|
||||
const props = defineProps<{
|
||||
vmRefs: XenApiVm["$ref"][];
|
||||
}>();
|
||||
|
||||
const { getByOpaqueRef: getVm } = useVmCollection();
|
||||
const vms = computed(() =>
|
||||
props.vmRefs.map(getVm).filter((vm): vm is XenApiVm => vm !== undefined)
|
||||
);
|
||||
</script>
|
@ -21,7 +21,7 @@
|
||||
{{ $t("edit-config") }}
|
||||
</MenuItem>
|
||||
<VmActionSnapshotItem :vm-refs="selectedRefs" />
|
||||
<VmActionExportItem :vm-refs="selectedRefs" />
|
||||
<VmActionExportItems :vm-refs="selectedRefs" />
|
||||
<VmActionDeleteItem :vm-refs="selectedRefs" />
|
||||
</AppMenu>
|
||||
</template>
|
||||
@ -32,7 +32,7 @@ import MenuItem from "@/components/menu/MenuItem.vue";
|
||||
import UiButton from "@/components/ui/UiButton.vue";
|
||||
import VmActionCopyItem from "@/components/vm/VmActionItems/VmActionCopyItem.vue";
|
||||
import VmActionDeleteItem from "@/components/vm/VmActionItems/VmActionDeleteItem.vue";
|
||||
import VmActionExportItem from "@/components/vm/VmActionItems/VmActionExportItem.vue";
|
||||
import VmActionExportItems from "@/components/vm/VmActionItems/VmActionExportItems.vue";
|
||||
import VmActionMigrateItem from "@/components/vm/VmActionItems/VmActionMigrateItem.vue";
|
||||
import VmActionPowerStateItems from "@/components/vm/VmActionItems/VmActionPowerStateItems.vue";
|
||||
import VmActionSnapshotItem from "@/components/vm/VmActionItems/VmActionSnapshotItem.vue";
|
||||
|
@ -491,3 +491,9 @@ export enum CERTIFICATE_TYPE {
|
||||
HOST = "host",
|
||||
HOST_INTERNAL = "host_internal",
|
||||
}
|
||||
|
||||
export enum VM_COMPRESSION_TYPE {
|
||||
DISABLED = "false",
|
||||
GZIP = "true",
|
||||
ZSTD = "zstd",
|
||||
}
|
||||
|
@ -18,6 +18,8 @@ import type {
|
||||
import { buildXoObject, typeToRawType } from "@/libs/xen-api/xen-api.utils";
|
||||
import { JSONRPCClient } from "json-rpc-2.0";
|
||||
import { castArray } from "lodash-es";
|
||||
import type { VM_COMPRESSION_TYPE } from "@/libs/xen-api/xen-api.enums";
|
||||
import { useModal } from "@/composables/modal.composable";
|
||||
|
||||
export default class XenApi {
|
||||
private client: JSONRPCClient;
|
||||
@ -27,10 +29,12 @@ export default class XenApi {
|
||||
Set<(...args: any[]) => void>
|
||||
>();
|
||||
private fromToken: string | undefined;
|
||||
private hostUrl: string;
|
||||
|
||||
constructor(hostUrl: string) {
|
||||
this.hostUrl = hostUrl;
|
||||
this.client = new JSONRPCClient(async (request) => {
|
||||
const response = await fetch(`${hostUrl}/jsonrpc`, {
|
||||
const response = await fetch(`${this.hostUrl}/jsonrpc`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(request),
|
||||
@ -380,6 +384,36 @@ export default class XenApi {
|
||||
)
|
||||
);
|
||||
},
|
||||
export: (vmRefs: VmRefs, compression: VM_COMPRESSION_TYPE) => {
|
||||
const blockedUrls: URL[] = [];
|
||||
|
||||
castArray(vmRefs).forEach((vmRef) => {
|
||||
const url = new URL(this.hostUrl);
|
||||
url.pathname = "/export/";
|
||||
url.search = new URLSearchParams({
|
||||
session_id: this.sessionId!,
|
||||
ref: vmRef,
|
||||
use_compression: compression,
|
||||
}).toString();
|
||||
|
||||
const _window = window.open(url.href, "_blank");
|
||||
if (_window === null) {
|
||||
blockedUrls.push(url);
|
||||
} else {
|
||||
URL.revokeObjectURL(url.toString());
|
||||
}
|
||||
});
|
||||
|
||||
if (blockedUrls.length > 0) {
|
||||
const { onClose } = useModal(
|
||||
() => import("@/components/modals/VmExportBlockedUrlsModal.vue"),
|
||||
{ blockedUrls }
|
||||
);
|
||||
onClose(() =>
|
||||
blockedUrls.forEach((url) => URL.revokeObjectURL(url.toString()))
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -43,6 +43,7 @@
|
||||
"delete-vms": "Delete 1 VM | Delete {n} VMs",
|
||||
"descending": "descending",
|
||||
"description": "Description",
|
||||
"disabled": "Disabled",
|
||||
"display": "Display",
|
||||
"do-you-have-needs": "You have needs and/or expectations? Let us know",
|
||||
"documentation": "Documentation",
|
||||
@ -51,8 +52,11 @@
|
||||
"error-no-data": "Error, can't collect data.",
|
||||
"error-occurred": "An error has occurred",
|
||||
"export": "Export",
|
||||
"export-n-vms": "Export 1 VM | Export {n} VMs",
|
||||
"export-n-vms-manually": "Export 1 VM manually | Export {n} VMs manually",
|
||||
"export-table-to": "Export table to {type}",
|
||||
"export-vms": "Export VMs",
|
||||
"export-vms-manually-information": "Some VM exports were not able to start automatically, probably due to your browser settings. To export them, you should click on each one. (Alternatively, copy the link as well.)",
|
||||
"fetching-fresh-data": "Fetching fresh data",
|
||||
"filter": {
|
||||
"comparison": {
|
||||
@ -78,6 +82,7 @@
|
||||
"fullscreen": "Fullscreen",
|
||||
"fullscreen-leave": "Leave fullscreen",
|
||||
"go-back": "Go back",
|
||||
"gzip": "gzip",
|
||||
"here": "Here",
|
||||
"hosts": "Hosts",
|
||||
"keep-me-logged": "Keep me logged in",
|
||||
@ -104,6 +109,7 @@
|
||||
"news": "News",
|
||||
"news-name": "{name} news",
|
||||
"no-alarm-triggered": "No alarm triggered",
|
||||
"no-selected-vm-can-be-exported": "No selected VM can be exported",
|
||||
"no-selected-vm-can-be-migrated": "No selected VM can be migrated",
|
||||
"no-tasks": "No tasks",
|
||||
"not-found": "Not found",
|
||||
@ -139,6 +145,7 @@
|
||||
},
|
||||
"resume": "Resume",
|
||||
"save": "Save",
|
||||
"select-compression": "Select a compression",
|
||||
"select-destination-host": "Select a destination host",
|
||||
"selected-vms-in-execution": "Some selected VMs are running",
|
||||
"send-ctrl-alt-del": "Send Ctrl+Alt+Del",
|
||||
@ -180,5 +187,6 @@
|
||||
"version": "Version",
|
||||
"vm-is-running": "The VM is running",
|
||||
"vms": "VMs",
|
||||
"xo-lite-under-construction": "XOLite is under construction"
|
||||
"xo-lite-under-construction": "XOLite is under construction",
|
||||
"zstd": "zstd"
|
||||
}
|
||||
|
@ -43,6 +43,7 @@
|
||||
"delete-vms": "Supprimer 1 VM | Supprimer {n} VMs",
|
||||
"descending": "descendant",
|
||||
"description": "Description",
|
||||
"disabled": "Désactivé",
|
||||
"display": "Affichage",
|
||||
"do-you-have-needs": "Vous avez des besoins et/ou des attentes ? Faites le nous savoir",
|
||||
"documentation": "Documentation",
|
||||
@ -51,8 +52,11 @@
|
||||
"error-no-data": "Erreur, impossible de collecter les données.",
|
||||
"error-occurred": "Une erreur est survenue",
|
||||
"export": "Exporter",
|
||||
"export-n-vms": "Exporter 1 VM | Exporter {n} VMs",
|
||||
"export-n-vms-manually": "Exporter 1 VM manuellement | Exporter {n} VMs manuellement",
|
||||
"export-table-to": "Exporter le tableau en {type}",
|
||||
"export-vms": "Exporter les VMs",
|
||||
"export-vms-manually-information": "Certaines exportations de VMs n'ont pas pu démarrer automatiquement, peut-être en raison des paramètres du navigateur. Pour les exporter, vous devrez cliquer sur chacune d'entre elles. (Ou copier le lien.)",
|
||||
"fetching-fresh-data": "Récupération de données à jour",
|
||||
"filter": {
|
||||
"comparison": {
|
||||
@ -78,6 +82,7 @@
|
||||
"fullscreen": "Plein écran",
|
||||
"fullscreen-leave": "Quitter plein écran",
|
||||
"go-back": "Revenir en arrière",
|
||||
"gzip": "gzip",
|
||||
"here": "Ici",
|
||||
"hosts": "Hôtes",
|
||||
"keep-me-logged": "Rester connecté",
|
||||
@ -104,6 +109,7 @@
|
||||
"news": "Actualités",
|
||||
"news-name": "Actualités {name}",
|
||||
"no-alarm-triggered": "Aucune alarme déclenchée",
|
||||
"no-selected-vm-can-be-exported": "Aucune VM sélectionnée ne peut être exportée",
|
||||
"no-selected-vm-can-be-migrated": "Aucune VM sélectionnée ne peut être migrée",
|
||||
"no-tasks": "Aucune tâche",
|
||||
"not-found": "Non trouvé",
|
||||
@ -139,6 +145,7 @@
|
||||
},
|
||||
"resume": "Reprendre",
|
||||
"save": "Enregistrer",
|
||||
"select-compression": "Sélectionnez une compression",
|
||||
"select-destination-host": "Sélectionnez un hôte de destination",
|
||||
"selected-vms-in-execution": "Certaines VMs sélectionnées sont en cours d'exécution",
|
||||
"send-ctrl-alt-del": "Envoyer Ctrl+Alt+Suppr",
|
||||
@ -180,5 +187,6 @@
|
||||
"version": "Version",
|
||||
"vm-is-running": "La VM est en cours d'exécution",
|
||||
"vms": "VMs",
|
||||
"xo-lite-under-construction": "XOLite est en construction"
|
||||
"xo-lite-under-construction": "XOLite est en construction",
|
||||
"zstd": "zstd"
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user