mirror of
https://github.com/vatesfr/xen-orchestra.git
synced 2026-11-09 17:06:13 -06:00
Compare commits
63 Commits
8025af3cc8
...
1667ed2a16
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1667ed2a16 | ||
|
|
55cf10d8bf | ||
|
|
7ec1e941a5 | ||
|
|
f8dc85855b | ||
|
|
8ae1f4e811 | ||
|
|
1c1960a09c | ||
|
|
892fff90ab | ||
|
|
d4c8b7aef4 | ||
|
|
11b3302b62 | ||
|
|
587d273e67 | ||
|
|
3dd6126425 | ||
|
|
92496e7a19 | ||
|
|
d6e7324892 | ||
|
|
13986f2828 | ||
|
|
f7cad58fa3 | ||
|
|
d1e023bdd4 | ||
|
|
710cc1915e | ||
|
|
873644d9e0 | ||
|
|
35082b5d9e | ||
|
|
a8ab6a1f99 | ||
|
|
fb1da9305e | ||
|
|
04d86aca15 | ||
|
|
73207050a0 | ||
|
|
a61182a466 | ||
|
|
6866393ba8 | ||
|
|
d1358444e7 | ||
|
|
75e990343a | ||
|
|
8d25cb6b94 | ||
|
|
c099fabb2b | ||
|
|
b20630c512 | ||
|
|
e63e6a3810 | ||
|
|
8d69b4095e | ||
|
|
43eefcf01f | ||
|
|
67ea6b5a53 | ||
|
|
c1c2673441 | ||
|
|
23ffab2373 | ||
|
|
2a4c19d281 | ||
|
|
ba7059af89 | ||
|
|
a51c206614 | ||
|
|
3532de374d | ||
|
|
f27fdb5c96 | ||
|
|
79d303db55 | ||
|
|
e70963dc74 | ||
|
|
a654570dd1 | ||
|
|
38522688ca | ||
|
|
b5a6a0f2b2 | ||
|
|
9f10b3f942 | ||
|
|
8c14ebbb0c | ||
|
|
6838e0b536 | ||
|
|
4d671f71cd | ||
|
|
dc43545389 | ||
|
|
30a21d98a4 | ||
|
|
bacd60115c | ||
|
|
a27f0cb5ed | ||
|
|
60272ed6cf | ||
|
|
745b6a7012 | ||
|
|
8583fcdd32 | ||
|
|
2a4e45f4dc | ||
|
|
0d7fba2b6a | ||
|
|
9efc4df5e6 | ||
|
|
cdda6f6e2a | ||
|
|
d85415f28e | ||
|
|
890a8aedae |
@@ -32,6 +32,14 @@ export type V2V_VM_OTHER_CONFIG = (typeof V2V_VM_OTHER_CONFIG)[keyof typeof V2V_
|
||||
|
||||
export type XoV2vVmOtherConfig = Partial<Record<V2V_VM_OTHER_CONFIG, string>>
|
||||
|
||||
// The disk of the source chain an imported VDI last received, one of the `chainPaths` of its source disk: the CID
|
||||
// cannot tell a fresh snapshot delta from its parent, the path can
|
||||
export const V2V_VDI_OTHER_CONFIG = {
|
||||
DISK_PATH: 'esxi_diskPath',
|
||||
} as const
|
||||
|
||||
export type V2V_VDI_OTHER_CONFIG = (typeof V2V_VDI_OTHER_CONFIG)[keyof typeof V2V_VDI_OTHER_CONFIG]
|
||||
|
||||
export const V2V_START_BLOCKED_REASON = 'Esxi migration in progress...'
|
||||
|
||||
export const V2V_PARTIAL_IMPORT_TAG = 'xo:no-bak=V2V partial import'
|
||||
@@ -120,6 +128,9 @@ export type XoV2vDisk = {
|
||||
mode: V2V_DISK_MODE
|
||||
isRdm: boolean
|
||||
isEncrypted: boolean
|
||||
// `dir/disk.vmdk` in its datastore (no `[datastore]` prefix) of the base disk, then of each snapshot delta down to
|
||||
// the active disk: the chain the import walks for this vmx node
|
||||
chainPaths: string[]
|
||||
}
|
||||
|
||||
// No port group for opaque (NSX) or missing backings
|
||||
|
||||
13
@xen-orchestra/lite/src/route-map.d.ts
vendored
13
@xen-orchestra/lite/src/route-map.d.ts
vendored
@@ -527,6 +527,13 @@ declare module 'vue-router/auto-routes' {
|
||||
Record<never, never>,
|
||||
| never
|
||||
>,
|
||||
'/story/web-core/tags-input/vts-tags-input': RouteRecordInfo<
|
||||
'/story/web-core/tags-input/vts-tags-input',
|
||||
'/story/web-core/tags-input/vts-tags-input',
|
||||
Record<never, never>,
|
||||
Record<never, never>,
|
||||
| never
|
||||
>,
|
||||
'/story/web-core/tree/vts-tree-item': RouteRecordInfo<
|
||||
'/story/web-core/tree/vts-tree-item',
|
||||
'/story/web-core/tree/vts-tree-item',
|
||||
@@ -1572,6 +1579,12 @@ declare module 'vue-router/auto-routes' {
|
||||
views:
|
||||
| never
|
||||
}
|
||||
'src/stories/web-core/tags-input/vts-tags-input.story.vue': {
|
||||
routes:
|
||||
| '/story/web-core/tags-input/vts-tags-input'
|
||||
views:
|
||||
| never
|
||||
}
|
||||
'src/stories/web-core/tree/vts-tree-item.story.vue': {
|
||||
routes:
|
||||
| '/story/web-core/tree/vts-tree-item'
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# Usage
|
||||
|
||||
Enter or comma adds the typed value: it is trimmed, and an empty value or a value already in the list is ignored. A
|
||||
pasted comma-separated draft adds each of its values. Enter while an IME composition is in progress adds nothing.
|
||||
|
||||
Removing a chip updates the model with the new array.
|
||||
|
||||
The label comes from the surrounding `VtsInputWrapper`.
|
||||
|
||||
```vue-template
|
||||
<VtsInputWrapper label="Values">
|
||||
<VtsTagsInput v-model="values" accent="brand" placeholder="Add a value" />
|
||||
</VtsInputWrapper>
|
||||
```
|
||||
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<ComponentStory
|
||||
v-slot="{ properties }"
|
||||
:params="[
|
||||
model().type('string[]').preset(['alpha', 'beta']).help('Enter or comma adds the typed value'),
|
||||
prop('accent').enum('brand', 'warning', 'danger').required().preset('brand').widget(),
|
||||
prop('placeholder').str().preset('Add a value').widget(),
|
||||
]"
|
||||
>
|
||||
<VtsInputWrapper label="Values">
|
||||
<VtsTagsInput v-bind="properties" />
|
||||
</VtsInputWrapper>
|
||||
</ComponentStory>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ComponentStory from '@/components/component-story/ComponentStory.vue'
|
||||
import { model, prop } from '@/libs/story/story-param.ts'
|
||||
import VtsInputWrapper from '@core/components/input-wrapper/VtsInputWrapper.vue'
|
||||
import VtsTagsInput from '@core/components/tags-input/VtsTagsInput.vue'
|
||||
</script>
|
||||
@@ -2,7 +2,7 @@
|
||||
<template>
|
||||
<div class="vts-side-panel-sticky" :class="{ mobile: uiStore.isSmall }">
|
||||
<VtsPanel
|
||||
:closable="hasSelection"
|
||||
:closable="isClosable"
|
||||
:close-icon="uiStore.isSmall ? 'fa:angle-left' : 'action:close-cancel-clear'"
|
||||
class="vts-side-panel"
|
||||
:class="{
|
||||
@@ -47,11 +47,12 @@ import UiButtonIcon from '@core/components/ui/button-icon/UiButtonIcon.vue'
|
||||
import { vTooltip } from '@core/directives/tooltip.directive.ts'
|
||||
import { usePanelStore } from '@core/stores/panel.store.ts'
|
||||
import { useUiStore } from '@core/stores/ui.store.ts'
|
||||
import { watch } from 'vue'
|
||||
import { computed, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { hasSelection } = defineProps<{
|
||||
const { hasSelection, persistent } = defineProps<{
|
||||
hasSelection?: boolean
|
||||
persistent?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -70,6 +71,9 @@ const uiStore = useUiStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// A persistent panel holds no selection to clear: it only closes where closing collapses it
|
||||
const isClosable = computed(() => hasSelection && (!persistent || panelStore.syncsOpenStateWithSelection))
|
||||
|
||||
watch(
|
||||
[() => hasSelection, () => panelStore.isLocked, () => uiStore.isSmall],
|
||||
([hasSelection]) => {
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<div class="vts-tags-input">
|
||||
<UiInput v-model="draft" :accent :placeholder @keydown="(event: KeyboardEvent) => handleKeydown(event)" />
|
||||
<div v-if="tags.length > 0" class="chips">
|
||||
<UiChip v-for="(tag, index) of tags" :key="index" accent="info" @remove="removeTag(index)">
|
||||
{{ tag }}
|
||||
</UiChip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import UiChip from '@core/components/ui/chip/UiChip.vue'
|
||||
import UiInput, { type InputAccent } from '@core/components/ui/input/UiInput.vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const { accent, placeholder } = defineProps<{
|
||||
accent: InputAccent
|
||||
placeholder?: string
|
||||
}>()
|
||||
|
||||
const tags = defineModel<string[]>({ required: true })
|
||||
|
||||
const draft = ref('')
|
||||
|
||||
const TAG_SEPARATOR_KEYS = ['Enter', ',']
|
||||
|
||||
// A pasted draft may hold several comma-separated tags
|
||||
function addDraftTags() {
|
||||
const draftTags = new Set(draft.value.split(',').map(tag => tag.trim()))
|
||||
const newTags = [...draftTags].filter(tag => tag !== '' && !tags.value.includes(tag))
|
||||
|
||||
draft.value = ''
|
||||
|
||||
if (newTags.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
tags.value = [...tags.value, ...newTags]
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
// Enter also validates an IME composition, which must not add a half-typed tag
|
||||
const isSeparator = !event.isComposing && TAG_SEPARATOR_KEYS.includes(event.key)
|
||||
|
||||
if (!isSeparator) {
|
||||
return
|
||||
}
|
||||
|
||||
// Prevents a form submit on Enter and keeps the comma out of the draft
|
||||
event.preventDefault()
|
||||
addDraftTags()
|
||||
}
|
||||
|
||||
function removeTag(index: number) {
|
||||
tags.value = tags.value.filter((_tag, tagIndex) => tagIndex !== index)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.vts-tags-input {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.8rem;
|
||||
|
||||
.chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -30,6 +30,7 @@ The selection is stored by item ID, so it is kept when changing page or filter.
|
||||
- "Select all" selects every item matching the filter
|
||||
- "Clear selection" unselects every item, including the ones hidden by the filter
|
||||
- IDs of items that are no longer in `items` are not part of `selectedIds`
|
||||
- A row checkbox can be disabled with the optional `disabled` binding of the `checkbox` body cell; the header checkbox and "Select all" still include that row, so keep it out of `items` if it must never be selected
|
||||
|
||||
## Usage
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
"action:back": "Back",
|
||||
"action:boot-vm": "Boot VM",
|
||||
"action:change-state": "Change state",
|
||||
"action:clear": "Clear",
|
||||
"action:clear-filter": "Clear filter",
|
||||
"action:clear-selection": "clear selection",
|
||||
"action:close": "Close",
|
||||
"action:configure": "Configure",
|
||||
@@ -86,7 +88,9 @@
|
||||
"action:edit-br": "@:action:edit BR",
|
||||
"action:edit-config": "Edit config",
|
||||
"action:edit-n-brs": "@:action:edit {n} BRs",
|
||||
"action:edit-settings": "@:action:edit @.lower:settings",
|
||||
"action:edit-traffic-rule": "@:action:edit @.lower:traffic-rule",
|
||||
"action:edit-vm-settings": "@:action:edit {name} @.lower:settings",
|
||||
"action:emergency-shutdown": "Emergency shutdown",
|
||||
"action:emergency-shutdown-host": "@:action:emergency-shutdown host",
|
||||
"action:enable": "Enable",
|
||||
@@ -126,6 +130,8 @@
|
||||
"action:migrate-n-vms": "Migrate 1 VM | Migrate {n} VMs",
|
||||
"action:migrate-vdi-on-sr": "Migrate on SR",
|
||||
"action:move-filters-to-parent-group": "Move filters to parent group",
|
||||
"action:next-mapping": "Next: mapping",
|
||||
"action:next-review": "Next: review",
|
||||
"action:open": "Open",
|
||||
"action:open-app": "@:action:open {name}",
|
||||
"action:open-console-in-new-tab": "Open console in new tab",
|
||||
@@ -137,6 +143,9 @@
|
||||
"action:reboot": "Reboot",
|
||||
"action:reboot-host": "@:action:reboot host",
|
||||
"action:reformat": "Reformat",
|
||||
"action:remove": "Remove",
|
||||
"action:remove-from-list": "Remove from list",
|
||||
"action:remove-name-from-list": "Remove {name} from the list",
|
||||
"action:reset-all-networks": "Reset all networks",
|
||||
"action:reset-all-srs": "Reset all SRs",
|
||||
"action:reset-to-default": "Reset to default",
|
||||
@@ -186,6 +195,8 @@
|
||||
"action:update": "Update",
|
||||
"action:use-query-builder": "Use query builder",
|
||||
"action:visit-pool-dashboard": "Visit pool dashboard",
|
||||
"active-mapping": "Active mapping",
|
||||
"adapter": "Adapter",
|
||||
"add-or": "+OR",
|
||||
"admin-login": "Admin login",
|
||||
"admin-password": "Admin password",
|
||||
@@ -209,6 +220,7 @@
|
||||
"all-done!": "All done!",
|
||||
"all-good!": "All good!",
|
||||
"all-quiet-launchpad": "All quiet on the launchpad",
|
||||
"all-vms": "All VMs",
|
||||
"all-srs-shared-storage-migration-optional": "All SRs are shared, storage migration is optional.",
|
||||
"all-vifs-follow-vm": "All VIFs follow the VM to its new host: within the pool, their networks don't change.",
|
||||
"allocated-space": "Allocated space",
|
||||
@@ -217,6 +229,7 @@
|
||||
"allow-unauthorized": "Allow unauthorized",
|
||||
"allowed-ips": "Allowed IPs",
|
||||
"allowed-ips-example": "For example: 192.168.0.1; 2001:db8::1",
|
||||
"also-on": "Also on",
|
||||
"and": "And",
|
||||
"any-property": "Any property",
|
||||
"api-error-details": "API Error details",
|
||||
@@ -303,6 +316,7 @@
|
||||
"character-limit": "{count}/{max} character | {count}/{max} characters",
|
||||
"check-errors:": "Check out the errors:",
|
||||
"check-summing": "TX checksumming",
|
||||
"checks": "Checks",
|
||||
"click-test-br-speed": "Click to test BR speed",
|
||||
"click-to-display-alarms:": "Click to display alarms:",
|
||||
"click-to-return-default-pool": "Click here to return to the default pool",
|
||||
@@ -374,6 +388,8 @@
|
||||
"dark-mode:disable": "Disable dark mode",
|
||||
"dark-mode:enable": "Enable dark mode",
|
||||
"dashboard": "Dashboard",
|
||||
"datastore": "Datastore",
|
||||
"datastores": "Datastores",
|
||||
"date": "Date",
|
||||
"default-behavior": "Default behavior",
|
||||
"default-locking-mode": "Default locking mode",
|
||||
@@ -381,6 +397,7 @@
|
||||
"default-storage-repository": "Default storage repository",
|
||||
"default-template": "Default template",
|
||||
"deletion-blocked": "Deletion Blocked",
|
||||
"delta-transfer": "Delta transfer",
|
||||
"deploy-xoa-available-on-desktop": "XOA deployment is available on your desktop interface",
|
||||
"deploy-xoa-custom-ntp-servers": "Custom NTP servers (separated by spaces)",
|
||||
"deploy-xoa-status:configuring": "Configuring XOA…",
|
||||
@@ -397,6 +414,7 @@
|
||||
"destination-network-required": "Destination network is required.",
|
||||
"destination-pool": "Destination pool",
|
||||
"destination-sr": "Destination SR",
|
||||
"destination-vm": "Destination VM",
|
||||
"destination-sr-required": "Destination SR is required.",
|
||||
"details": "Details",
|
||||
"device": "Device",
|
||||
@@ -416,8 +434,10 @@
|
||||
"disconnected-from-physical-device": "Disconnected from physical device",
|
||||
"disconnected-pbd-number": "Disconnected PBD #{n}",
|
||||
"disconnecting": "Disconnecting",
|
||||
"disk": "Disk",
|
||||
"disk-name": "Disk name",
|
||||
"disk-space": "Disk space",
|
||||
"disks": "Disks",
|
||||
"dns": "DNS",
|
||||
"do-you-have-needs": "You have needs and/or expectations? Let us know",
|
||||
"do-you-really-want-to-leave-this-page?": "Some changes are still being deployed. Do you really want to leave this page?",
|
||||
@@ -491,10 +511,12 @@
|
||||
"from-vm": "From VM",
|
||||
"full": "Full",
|
||||
"full-copy": "Full copy",
|
||||
"full-transfer": "Full transfer",
|
||||
"fullscreen": "Fullscreen",
|
||||
"gateway": "Gateway",
|
||||
"general": "General",
|
||||
"general-information": "General information",
|
||||
"global-default": "Global default",
|
||||
"global-defaults-for-name": "Global defaults for {name}",
|
||||
"gpu": "GPU",
|
||||
"gpus": "GPUs",
|
||||
@@ -534,8 +556,10 @@
|
||||
"https": "HTTPS",
|
||||
"hyper-threading": "Hyper threading (SMT)",
|
||||
"id": "ID",
|
||||
"import-in-progress": "Import in progress",
|
||||
"in": "in",
|
||||
"in-last-three-runs": "In their last 3 runs",
|
||||
"in-list": "In list",
|
||||
"in-progress": "In progress",
|
||||
"included": "Included",
|
||||
"incremental": "Incremental",
|
||||
@@ -779,10 +803,12 @@
|
||||
"json-schema:numbered-item": "{label} {n}",
|
||||
"json-schema:show-examples-n": "Show example ({n}) | Show examples ({n})",
|
||||
"json-schema:unsupported-field": "The \"{label}\" field cannot be edited from this interface",
|
||||
"keep-mac-addresses": "Keep MAC addresses",
|
||||
"keep-me-logged": "Keep me logged in",
|
||||
"keep-page-open": "Do not refresh or quit tab before end of deployment.",
|
||||
"key": "Key",
|
||||
"keywords": "Keywords",
|
||||
"label-with-count": "{label} {count}",
|
||||
"language": "Language",
|
||||
"language-preferences": "Language preferences",
|
||||
"last": "Last",
|
||||
@@ -793,6 +819,7 @@
|
||||
"last-revert": "Last revert",
|
||||
"last-run-number": "Last run #{n}",
|
||||
"last-snapshot": "Last snapshot",
|
||||
"last-sync": "Last sync",
|
||||
"last-week": "Last week",
|
||||
"ldap-auth-providers": "LDAP/Auth providers",
|
||||
"learn-more": "Learn more",
|
||||
@@ -807,6 +834,7 @@
|
||||
"loaded": "Loaded",
|
||||
"loading": "Loading",
|
||||
"local": "Local",
|
||||
"location": "Location",
|
||||
"locked": "Locked",
|
||||
"locking-mode": "Locking mode",
|
||||
"locking-mode-default": "Default locking mode",
|
||||
@@ -835,6 +863,7 @@
|
||||
"merge-backups-synchronously": "Merge backups synchronously",
|
||||
"message": "Message",
|
||||
"migrate-vms": "Migrate VMs",
|
||||
"migration": "Migration",
|
||||
"migration-blocked": "Migration Blocked",
|
||||
"migration-compression": "Migration compression",
|
||||
"migration-network": "Migration network",
|
||||
@@ -895,21 +924,27 @@
|
||||
"multi-pathing": "Multi-pathing",
|
||||
"multiple-schedules": "Multiple schedules",
|
||||
"multiple-schedules-advice": "We advise splitting a backup job with multiple schedules into multiple backup jobs with one schedule each.",
|
||||
"n-blocking": "1 blocking | {n} blocking",
|
||||
"n-bonded-networks": "1 bonded network | {n} bonded networks",
|
||||
"n-brs": "1 BR | {n} BRs",
|
||||
"n-disks": "1 disk | {n} disks",
|
||||
"n-gb": "{n} GB",
|
||||
"n-gb-left": "{n} GB left",
|
||||
"n-gb-required": "{n} GB required",
|
||||
"n-hexadecimal-characters": "{n} hexadecimal characters.",
|
||||
"n-hosts": "1 host | {n} hosts",
|
||||
"n-hosts-awaiting-patch": "{n} host is awaiting this patch | {n} hosts are awaiting this patch",
|
||||
"n-in-progress": "1 in progress | {n} in progress",
|
||||
"n-internal-networks": "1 internal network | {n} internal networks",
|
||||
"n-migrated": "1 migrated | {n} migrated",
|
||||
"n-missing": "{n} missing",
|
||||
"n-networks": "1 network | {n} networks",
|
||||
"n-nics": "1 NIC | {n} NICs",
|
||||
"n-not-migrated": "1 not migrated | {n} not migrated",
|
||||
"n-partially-migrated": "1 partially migrated | {n} partially migrated",
|
||||
"n-percent": "{n}%",
|
||||
"n-pifs": "1 PIF | {n} PIFs",
|
||||
"n-ready": "1 ready | {n} ready",
|
||||
"n-snapshots": "1 snapshot | {n} snapshots",
|
||||
"n-srs": "1 SR | {n} SRs",
|
||||
"n-subtasks": "1 subtask | {n} subtasks",
|
||||
@@ -919,6 +954,7 @@
|
||||
"n-vdis": "1 VDI | {n} VDIs",
|
||||
"n-vifs": "1 VIF | {n} VIFs",
|
||||
"n-vms": "1 VM | {n} VMs",
|
||||
"n-warnings": "1 warning | {n} warnings",
|
||||
"name": "Name",
|
||||
"name-required": "Name is required.",
|
||||
"native": "Native",
|
||||
@@ -929,6 +965,7 @@
|
||||
"nested-virtualization": "Nested virtualization",
|
||||
"netmask": "Netmask",
|
||||
"network": "Network",
|
||||
"network-adapters": "Network adapters",
|
||||
"network-block-device": "Network block device",
|
||||
"network-config": "Network config",
|
||||
"network-create:warning:mtu-out-of-range": "MTU should be between {min} and {max}",
|
||||
@@ -1030,6 +1067,8 @@
|
||||
"normal": "Normal",
|
||||
"not-found": "Not found",
|
||||
"not-included": "Not included",
|
||||
"not-migrated": "Not migrated",
|
||||
"not-ready": "Not ready",
|
||||
"not-resident-on": "Not resident on",
|
||||
"not-running": "Not running",
|
||||
"not-yet-available": "Information is not yet available",
|
||||
@@ -1071,6 +1110,7 @@
|
||||
"other": "Other",
|
||||
"other-properties": "Other properties",
|
||||
"ova": "OVA",
|
||||
"overridden": "Overridden",
|
||||
"page-not-found": "This page is not to be found…",
|
||||
"page-please-wait": "Hold tight, astronaut! The page is getting ready to take off.{newline}We're just aligning the thrusters and calibrating the systems. It won't be long now, stay tuned!",
|
||||
"pagination:all": "@:all",
|
||||
@@ -1122,6 +1162,7 @@
|
||||
"pool-forget-confirm-info": "This will permanently remove this host from XO. To reconnect, you will need to add it again.",
|
||||
"pool-forget-confirm-title": "You are about to remove {name} host",
|
||||
"pool-management": "Pool management",
|
||||
"pool-named": "Pool {name}",
|
||||
"pool-ram-usage": "Pool RAM Usage",
|
||||
"pool-required": "Pool is required.",
|
||||
"pool:status:connected": "@:status:connected",
|
||||
@@ -1137,12 +1178,14 @@
|
||||
"popup-first-connection-introduction": "We’re excited to introduce the first official release of Xen Orchestra 6.",
|
||||
"popup-first-connection-upcoming-release": "In the upcoming releases, we’ll be adding actions on all objects, user management, backup features, and much more. We hope you’ll enjoy exploring this brand-new interface!",
|
||||
"port": "Port",
|
||||
"port-group": "Port group",
|
||||
"port-invalid": "Port format is invalid.",
|
||||
"port-required": "Port is required.",
|
||||
"power-on-host-for-console": "Power on your host to access its console",
|
||||
"power-on-mode": "Power on mode",
|
||||
"power-on-vm-for-console": "Power on your VM to access its console",
|
||||
"power-state": "Power state",
|
||||
"power-state-on-host": "{power} on {host}",
|
||||
"pro-support": "{name} pro support",
|
||||
"professional-support": "Professional support",
|
||||
"progress": "Progress",
|
||||
@@ -1196,6 +1239,7 @@
|
||||
"read": "Read",
|
||||
"read-only": "Read-only",
|
||||
"reading-speed": "Reading speed",
|
||||
"ready": "Ready",
|
||||
"reboot-blocked": "Reboot Blocked",
|
||||
"reboot-vm-internal-shutdown": "Reboot VM on internal shutdown",
|
||||
"receive": "Receive",
|
||||
@@ -1224,6 +1268,7 @@
|
||||
"resources": "Resources",
|
||||
"resources-overview": "Resources overview",
|
||||
"rest-api": "REST API",
|
||||
"resume": "Resume",
|
||||
"retry": "Retry",
|
||||
"review": "Review",
|
||||
"review-and-confirm": "Review & confirm",
|
||||
@@ -1250,7 +1295,9 @@
|
||||
"select-pool": "Select a pool",
|
||||
"select-preset": "Select a preset",
|
||||
"select-to-see-details": "Click on {icon} to see details",
|
||||
"selected-vms": "Selected VMs",
|
||||
"selected-vms-in-execution": "Some selected VMs are running",
|
||||
"selected-vms-n": "Selected VMs ({n})",
|
||||
"self-signed-certificates": "Self-signed certificates",
|
||||
"send-us-feedback": "Send us feedback",
|
||||
"settings": "Settings",
|
||||
@@ -1261,6 +1308,7 @@
|
||||
"size": "Size",
|
||||
"size-and-speed": "@:size & @.lower:speed",
|
||||
"size-on-disk": "Size on disk",
|
||||
"size-on-this-datastore": "Size on this datastore",
|
||||
"skip-ssl-check": "Skip SSL check (self-signed certificate)",
|
||||
"skip-ssl-check-info": "The server certificate is not verified. Use it for self-signed certificates.",
|
||||
"skipped": "Skipped",
|
||||
@@ -1345,6 +1393,7 @@
|
||||
"success": "Success",
|
||||
"summary": "Summary",
|
||||
"support": "Support",
|
||||
"supported": "Supported",
|
||||
"supported-image-formats": "Supported image formats",
|
||||
"suspend-blocked": "Suspend Blocked",
|
||||
"suspend-storage-repository": "Suspend storage repository",
|
||||
@@ -1360,6 +1409,8 @@
|
||||
"table-selection:watch-out!": "Watch out!",
|
||||
"tags": "Tags",
|
||||
"target": "Target",
|
||||
"target-network": "Target network",
|
||||
"target-sr": "Target SR",
|
||||
"task": "Task",
|
||||
"task-type": "Task type",
|
||||
"task:ended": "@:ended",
|
||||
@@ -1448,6 +1499,7 @@
|
||||
"unnamed": "Unnamed",
|
||||
"unreachable-hosts": "Unreachable hosts",
|
||||
"unreachable-hosts-reload-page": "Done, reload the page",
|
||||
"unsupported": "Unsupported",
|
||||
"untitled": "Untitled",
|
||||
"untranslated-text-helper": "By default, untranslated texts will be presented in english.",
|
||||
"up-to-date": "Up-to-date",
|
||||
@@ -1466,17 +1518,47 @@
|
||||
"users": "Users",
|
||||
"uuid": "UUID",
|
||||
"v2v": "V2V",
|
||||
"v2v:action:add-to-migration-list": "Add to migration list",
|
||||
"v2v:already-migrated-confirm-at-launch": "Already migrated, confirmation at launch",
|
||||
"v2v:check:mapping-incomplete": "Mapping incomplete",
|
||||
"v2v:check:n-snapshots": "1 snapshot on the source VM | {n} snapshots on the source VM",
|
||||
"v2v:check:no-snapshot": "No snapshot on the source VM",
|
||||
"v2v:check:no-unsupported-disk": "No unsupported disk (RDM, independent, encrypted)",
|
||||
"v2v:choose-vmware-source": "Choose the VMware source",
|
||||
"v2v:connect-new-vmware-source": "Connect a new VMware source",
|
||||
"v2v:credentials-never-stored": "Credentials are never stored by XO: they stay in this browser and are sent with each request that needs VMware. Migration status is shared by everyone who connects to the same vCenter.",
|
||||
"v2v:delta-from-last-sync": "Delta from last sync",
|
||||
"v2v:destination-pool-info": "A migration goes from one source to one pool. Each source and pool pair has its own list.",
|
||||
"v2v:existing-list-info": "This browser already holds a migration list for {migration} ({vms}). Continuing reopens it, with the current migration state of every VM.",
|
||||
"v2v:filter-vms-info": "filter by datastore, status or name with the query builder",
|
||||
"v2v:global-defaults-info": "Applied to every VM, disk and network adapter. Each VM can override them in step 4 (Mapping). The SR and network are prefilled with the pool default SR and the management network of the pool master.",
|
||||
"v2v:list-pair-info": "One source and one destination per list: choosing another source or pool opens its own list.",
|
||||
"v2v:locked-since-first-sync": "Locked since the first sync",
|
||||
"v2v:mac-kept": "MAC kept",
|
||||
"v2v:mapping-locked": "mapping locked",
|
||||
"v2v:name-from-migration-list": "{name} from the migration list",
|
||||
"v2v:next-run-reminder": "The longer since the last sync, the larger the final delta. Run a delta sync shortly before the cutover to keep the downtime short.",
|
||||
"v2v:no-bak-info": "Excluded from backups: {tag} (removed at cutover)",
|
||||
"v2v:no-vm-in-list": "No VM in the migration list",
|
||||
"v2v:not-found-in-source": "Not found in the source",
|
||||
"v2v:partial-import-on-pool": "Partial import on pool {pool}",
|
||||
"v2v:prerequisite-checking": "Prerequisite check: checking vectura…",
|
||||
"v2v:prerequisite-error": "Prerequisite check: vectura cannot be used, VMs cannot be migrated until it is fixed",
|
||||
"v2v:prerequisite-error-detail": "Prerequisite check: vectura cannot be used ({error}), VMs cannot be migrated until it is fixed",
|
||||
"v2v:prerequisite-ok": "Prerequisite check: vectura OK",
|
||||
"v2v:remove-from-list-info": "Only the migration list changes: nothing is deleted on the source nor in XO.",
|
||||
"v2v:saved-in-this-browser": "Saved in this browser",
|
||||
"v2v:sent-to-xo-at-launch": "sent to XO at launch",
|
||||
"v2v:source-snapshot-changed": "Source snapshot changed since the last sync",
|
||||
"v2v:start-blocked-partial": "Start blocked while the import is partial",
|
||||
"v2v:status:cold": "Cutover done",
|
||||
"v2v:status:warm": "Warm synced",
|
||||
"v2v:tags-backup-info": "Use tags to pick up the VM in smart backup jobs. Until the cutover, the VM also carries {tag}, so backup jobs skip it; the tag is removed at cutover.",
|
||||
"v2v:unsupported-encrypted-disk": "Unsupported: encrypted disk",
|
||||
"v2v:unsupported-independent-disk": "Unsupported: independent disk",
|
||||
"v2v:unsupported-rdm-disk": "Unsupported: RDM disk",
|
||||
"value-by-default": "{value} by default",
|
||||
"value-from": "Value from",
|
||||
"vcpu": "vCPU",
|
||||
"vcpu-at-startup": "vCPU at startup",
|
||||
"vcpus": "vCPUs",
|
||||
@@ -1525,6 +1607,7 @@
|
||||
"virtualization-mode": "Virtualization mode",
|
||||
"virtualization-type": "Virtualization type",
|
||||
"vlan": "VLAN",
|
||||
"vlan-id": "VLAN {id}",
|
||||
"vlan-required": "VLAN is required.",
|
||||
"vm": "VM",
|
||||
"vm-backup-failure-number-of-retries": "Number of retries if VM backup fails",
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
"action:back": "Retour",
|
||||
"action:boot-vm": "Démarrer la VM",
|
||||
"action:change-state": "Changer l'état",
|
||||
"action:clear": "Vider",
|
||||
"action:clear-filter": "Effacer le filtre",
|
||||
"action:clear-selection": "vider la sélection",
|
||||
"action:close": "Fermer",
|
||||
"action:configure": "Configurer",
|
||||
@@ -86,7 +88,9 @@
|
||||
"action:edit-br": "@:action:edit le BR",
|
||||
"action:edit-config": "Modifier config",
|
||||
"action:edit-n-brs": "@:action:edit {n} BRs",
|
||||
"action:edit-settings": "@:action:edit les @.lower:settings",
|
||||
"action:edit-traffic-rule": "@:action:edit la @.lower:traffic-rule",
|
||||
"action:edit-vm-settings": "@:action:edit les @.lower:settings de {name}",
|
||||
"action:emergency-shutdown": "Arrêt d'urgence",
|
||||
"action:emergency-shutdown-host": "@:action:emergency-shutdown de l'hôte",
|
||||
"action:enable": "Activer",
|
||||
@@ -126,6 +130,8 @@
|
||||
"action:migrate-n-vms": "Migrer 1 VM | Migrer {n} VMs",
|
||||
"action:migrate-vdi-on-sr": "Migrer sur le SR",
|
||||
"action:move-filters-to-parent-group": "Déplacer les filtres vers le groupe parent",
|
||||
"action:next-mapping": "Suivant : correspondance",
|
||||
"action:next-review": "Suivant : récapitulatif",
|
||||
"action:open": "Ouvrir",
|
||||
"action:open-app": "@:action:open {name}",
|
||||
"action:open-console-in-new-tab": "Ouvrir la console dans un nouvel onglet",
|
||||
@@ -137,6 +143,9 @@
|
||||
"action:reboot": "Redémarrer",
|
||||
"action:reboot-host": "@:action:reboot l'hôte",
|
||||
"action:reformat": "Reformater",
|
||||
"action:remove": "Retirer",
|
||||
"action:remove-from-list": "Retirer de la liste",
|
||||
"action:remove-name-from-list": "Retirer {name} de la liste",
|
||||
"action:reset-all-networks": "Réinitialiser tous les réseaux",
|
||||
"action:reset-all-srs": "Réinitialiser tous les SRs",
|
||||
"action:reset-to-default": "Rétablir la valeur par défaut",
|
||||
@@ -186,6 +195,8 @@
|
||||
"action:update": "Mettre à jour",
|
||||
"action:use-query-builder": "Constructeur de requête",
|
||||
"action:visit-pool-dashboard": "Visiter le tableau de bord du pool",
|
||||
"active-mapping": "Correspondance active",
|
||||
"adapter": "Carte",
|
||||
"add-or": "+OU",
|
||||
"admin-login": "Nom d'utilisateur administrateur",
|
||||
"admin-password": "Mot de passe administrateur",
|
||||
@@ -209,6 +220,7 @@
|
||||
"all-done!": "Tout est terminé !",
|
||||
"all-good!": "Tout va bien !",
|
||||
"all-quiet-launchpad": "Tout est calme sur la rampe de lancement",
|
||||
"all-vms": "Toutes les VMs",
|
||||
"all-srs-shared-storage-migration-optional": "Tous les SRs sont partagés, la migration du stockage est facultative.",
|
||||
"all-vifs-follow-vm": "Toutes les VIFs suivent la VM vers son nouvel hôte : au sein du pool, leurs réseaux ne changent pas.",
|
||||
"allocated-space": "Espace alloué",
|
||||
@@ -217,6 +229,7 @@
|
||||
"allow-unauthorized": "Autoriser les connexions non vérifiées",
|
||||
"allowed-ips": "IPs autorisées",
|
||||
"allowed-ips-example": "Par exemple : 192.168.0.1; 2001:db8::1",
|
||||
"also-on": "Aussi sur",
|
||||
"and": "Et",
|
||||
"any-property": "N'importe quelle propriété",
|
||||
"api-error-details": "Détails de l'erreur API",
|
||||
@@ -303,6 +316,7 @@
|
||||
"character-limit": "{count}/{max} caractère | {count}/{max} caractères",
|
||||
"check-errors:": "Consultez les erreurs :",
|
||||
"check-summing": "TX checksumming",
|
||||
"checks": "Vérifications",
|
||||
"click-test-br-speed": "Cliquez pour tester la vitesse du BR",
|
||||
"click-to-display-alarms:": "Cliquer pour afficher les alarmes :",
|
||||
"click-to-return-default-pool": "Cliquer ici pour revenir au pool par défaut",
|
||||
@@ -374,6 +388,8 @@
|
||||
"dark-mode:disable": "Désactiver le mode sombre",
|
||||
"dark-mode:enable": "Activer le mode sombre",
|
||||
"dashboard": "Tableau de bord",
|
||||
"datastore": "Banque de données",
|
||||
"datastores": "Banques de données",
|
||||
"date": "Date",
|
||||
"default-behavior": "Comportement par défaut",
|
||||
"default-locking-mode": "Mode de verrouillage par défaut",
|
||||
@@ -381,6 +397,7 @@
|
||||
"default-storage-repository": "Dépôt de stockage par défaut",
|
||||
"default-template": "Modèle par défaut",
|
||||
"deletion-blocked": "Suppression Bloquée",
|
||||
"delta-transfer": "Transfert delta",
|
||||
"deploy-xoa-available-on-desktop": "Le déploiement de la XOA est disponible sur ordinateur",
|
||||
"deploy-xoa-custom-ntp-servers": "Serveurs NTP (séparés par des espaces)",
|
||||
"deploy-xoa-status:configuring": "Configuration de la XOA…",
|
||||
@@ -397,6 +414,7 @@
|
||||
"destination-network-required": "Le réseau de destination est requis.",
|
||||
"destination-pool": "Pool de destination",
|
||||
"destination-sr": "SR de destination",
|
||||
"destination-vm": "VM de destination",
|
||||
"destination-sr-required": "Le SR de destination est requis.",
|
||||
"details": "Détails",
|
||||
"device": "Appareil",
|
||||
@@ -416,8 +434,10 @@
|
||||
"disconnected-from-physical-device": "Déconnecté de l'appareil physique",
|
||||
"disconnected-pbd-number": "PBD déconnecté #{n}",
|
||||
"disconnecting": "Déconnexion",
|
||||
"disk": "Disque",
|
||||
"disk-name": "Nom du disque",
|
||||
"disk-space": "Espace disque",
|
||||
"disks": "Disques",
|
||||
"dns": "DNS",
|
||||
"do-you-have-needs": "Vous avez des besoins et/ou des attentes ? Faites-le-nous savoir",
|
||||
"do-you-really-want-to-leave-this-page?": "Des modifications sont encore en cours de déploiement. Souhaitez-vous vraiment quitter cette page ?",
|
||||
@@ -491,10 +511,12 @@
|
||||
"from-vm": "Depuis la VM",
|
||||
"full": "Complet",
|
||||
"full-copy": "Copie complète",
|
||||
"full-transfer": "Transfert complet",
|
||||
"fullscreen": "Plein écran",
|
||||
"gateway": "Passerelle",
|
||||
"general": "Général",
|
||||
"general-information": "Informations générales",
|
||||
"global-default": "Défaut global",
|
||||
"global-defaults-for-name": "Valeurs par défaut globales pour {name}",
|
||||
"gpu": "GPU",
|
||||
"gpus": "GPUs",
|
||||
@@ -534,8 +556,10 @@
|
||||
"https": "HTTPS",
|
||||
"hyper-threading": "Hyper-threading (SMT)",
|
||||
"id": "ID",
|
||||
"import-in-progress": "Import en cours",
|
||||
"in": "entrant sur",
|
||||
"in-last-three-runs": "Dans leurs trois derniers runs",
|
||||
"in-list": "Dans la liste",
|
||||
"in-progress": "En cours",
|
||||
"included": "Inclus",
|
||||
"incremental": "Incrémental",
|
||||
@@ -779,10 +803,12 @@
|
||||
"json-schema:numbered-item": "{label} {n}",
|
||||
"json-schema:show-examples-n": "Afficher l'exemple ({n}) | Afficher les exemples ({n})",
|
||||
"json-schema:unsupported-field": "Le champ « {label} » ne peut pas être modifié depuis cette interface",
|
||||
"keep-mac-addresses": "Conserver les adresses MAC",
|
||||
"keep-me-logged": "Rester connecté",
|
||||
"keep-page-open": "Ne pas rafraichir ou quitter cette page avant la fin du déploiement.",
|
||||
"key": "Clé",
|
||||
"keywords": "Mots-clés",
|
||||
"label-with-count": "{label} {count}",
|
||||
"language": "Langue",
|
||||
"language-preferences": "Choix de la langue",
|
||||
"last": "Dernier",
|
||||
@@ -793,6 +819,7 @@
|
||||
"last-revert": "Dernière restauration",
|
||||
"last-run-number": "Dernier run #{n}",
|
||||
"last-snapshot": "Dernier instantané",
|
||||
"last-sync": "Dernière synchronisation",
|
||||
"last-week": "Semaine dernière",
|
||||
"ldap-auth-providers": "LDAP/Fournisseurs d'authentification",
|
||||
"learn-more": "En savoir plus",
|
||||
@@ -807,6 +834,7 @@
|
||||
"loaded": "Chargé",
|
||||
"loading": "Chargement",
|
||||
"local": "Local",
|
||||
"location": "Emplacement",
|
||||
"locked": "Verrouillé",
|
||||
"locking-mode": "Mode de verrouillage",
|
||||
"locking-mode-default": "Mode de verrouillage par défaut",
|
||||
@@ -835,6 +863,7 @@
|
||||
"merge-backups-synchronously": "Fusionner les sauvegardes de manière synchrone",
|
||||
"message": "Message",
|
||||
"migrate-vms": "Migrer des VMs",
|
||||
"migration": "Migration",
|
||||
"migration-blocked": "Migration bloquée",
|
||||
"migration-compression": "Compression des migrations",
|
||||
"migration-network": "Réseau de migration",
|
||||
@@ -895,21 +924,27 @@
|
||||
"multi-pathing": "Multi-pathing",
|
||||
"multiple-schedules": "Plusieurs planifications",
|
||||
"multiple-schedules-advice": "Nous vous conseillons de diviser un job de sauvegarde à plusieurs planifications en plusieurs jobs ayant chacun une seule planification.",
|
||||
"n-blocking": "0 bloquante | 1 bloquante | {n} bloquantes",
|
||||
"n-bonded-networks": "1 réseau lié | {n} réseaux liés",
|
||||
"n-brs": "1 BR | {n} BRs",
|
||||
"n-disks": "0 disque | 1 disque | {n} disques",
|
||||
"n-gb": "{n} Go",
|
||||
"n-gb-left": "{n} Go libres",
|
||||
"n-gb-required": "{n} Go requis",
|
||||
"n-hexadecimal-characters": "{n} caractères hexadécimaux.",
|
||||
"n-hosts": "0 hôte | 1 hôte | {n} hôtes",
|
||||
"n-hosts-awaiting-patch": "{n} hôte attend ce patch | {n} hôtes attendent ce patch",
|
||||
"n-in-progress": "0 en cours | 1 en cours | {n} en cours",
|
||||
"n-internal-networks": "1 réseau interne | {n} réseaux internes",
|
||||
"n-migrated": "0 migrée | 1 migrée | {n} migrées",
|
||||
"n-missing": "{n} manquant | {n} manquants",
|
||||
"n-networks": "0 réseau | 1 réseau | {n} réseaux",
|
||||
"n-nics": "0 carte réseau | 1 carte réseau | {n} cartes réseau",
|
||||
"n-not-migrated": "0 non migrée | 1 non migrée | {n} non migrées",
|
||||
"n-partially-migrated": "0 partiellement migrée | 1 partiellement migrée | {n} partiellement migrées",
|
||||
"n-percent": "{n} %",
|
||||
"n-pifs": "1 PIF | {n} PIFs",
|
||||
"n-ready": "0 prête | 1 prête | {n} prêtes",
|
||||
"n-snapshots": "0 instantané | 1 instantané | {n} instantanés",
|
||||
"n-srs": "0 SR | 1 SR | {n} SRs",
|
||||
"n-subtasks": "0 sous-tâche | 1 sous-tâche | {n} sous-tâches",
|
||||
@@ -919,6 +954,7 @@
|
||||
"n-vdis": "0 VDI | 1 VDI | {n} VDIs",
|
||||
"n-vifs": "0 VIF | 1 VIF | {n} VIFs",
|
||||
"n-vms": "0 VM | 1 VM | {n} VMs",
|
||||
"n-warnings": "0 avertissement | 1 avertissement | {n} avertissements",
|
||||
"name": "Nom",
|
||||
"name-required": "Le nom est requis.",
|
||||
"native": "Native",
|
||||
@@ -929,6 +965,7 @@
|
||||
"nested-virtualization": "Virtualisation imbriquée",
|
||||
"netmask": "Masque réseau",
|
||||
"network": "Réseau",
|
||||
"network-adapters": "Cartes réseau",
|
||||
"network-block-device": "Protocole NBD (Network Block Device)",
|
||||
"network-config": "Configuration réseau",
|
||||
"network-create:warning:mtu-out-of-range": "Le MTU doit être compris entre {min} et {max}",
|
||||
@@ -1030,6 +1067,8 @@
|
||||
"normal": "Normal",
|
||||
"not-found": "Non trouvé",
|
||||
"not-included": "Non inclus",
|
||||
"not-migrated": "Non migrée",
|
||||
"not-ready": "Non prête",
|
||||
"not-resident-on": "Non résident sur",
|
||||
"not-running": "Pas en cours d'exécution",
|
||||
"not-yet-available": "Information pas encore disponible",
|
||||
@@ -1071,6 +1110,7 @@
|
||||
"other": "Autre",
|
||||
"other-properties": "Autres propriétés",
|
||||
"ova": "OVA",
|
||||
"overridden": "Personnalisé",
|
||||
"page-not-found": "Cette page est introuvable…",
|
||||
"page-please-wait": "Accrochez-vous, astronaute ! La page est sur le point de décoller.{newline}Nous alignons les propulseurs et calibrons les systèmes. Plus que quelques instants, restez à l'écoute !",
|
||||
"pagination:all": "Tous",
|
||||
@@ -1122,6 +1162,7 @@
|
||||
"pool-forget-confirm-info": "Cela supprimera définitivement cet hôte de XO. Pour vous reconnecter, vous devrez l'ajouter à nouveau.",
|
||||
"pool-forget-confirm-title": "Vous êtes sur le point de supprimer l'hôte {name}",
|
||||
"pool-management": "Gestion du Pool",
|
||||
"pool-named": "Pool {name}",
|
||||
"pool-ram-usage": "Utilisation RAM du Pool",
|
||||
"pool-required": "Le pool est requis.",
|
||||
"pool:status:connected": "@:status:connected",
|
||||
@@ -1137,12 +1178,14 @@
|
||||
"popup-first-connection-introduction": "Nous sommes ravis de vous présenter la première version officielle de Xen Orchestra 6.",
|
||||
"popup-first-connection-upcoming-release": "Dans les prochaines versions, nous ajouterons des actions sur tous les objets, la gestion des utilisateurs, des fonctionnalités de sauvegarde et bien plus encore. Nous espérons que vous apprécierez découvrir cette toute nouvelle interface !",
|
||||
"port": "Port",
|
||||
"port-group": "Groupe de ports",
|
||||
"port-invalid": "Le format du port est invalide.",
|
||||
"port-required": "Le port est requis.",
|
||||
"power-on-host-for-console": "Allumez votre hôte pour accéder à sa console",
|
||||
"power-on-mode": "Mode d'alimentation",
|
||||
"power-on-vm-for-console": "Allumez votre VM pour accéder à sa console",
|
||||
"power-state": "État d'alimentation",
|
||||
"power-state-on-host": "{power} sur {host}",
|
||||
"pro-support": "Support pro {name}",
|
||||
"professional-support": "Support professionnel",
|
||||
"progress": "Progression",
|
||||
@@ -1196,6 +1239,7 @@
|
||||
"read": "Lecture",
|
||||
"read-only": "Lecture seule",
|
||||
"reading-speed": "Vitesse de lecture",
|
||||
"ready": "Prête",
|
||||
"reboot-blocked": "Redémarrage bloqué",
|
||||
"reboot-vm-internal-shutdown": "Redémarrer la VM lors d'un arrêt interne",
|
||||
"receive": "Recevoir",
|
||||
@@ -1224,6 +1268,7 @@
|
||||
"resources": "Ressources",
|
||||
"resources-overview": "Vue d'ensemble des ressources",
|
||||
"rest-api": "API REST",
|
||||
"resume": "Reprise",
|
||||
"retry": "Réessayer",
|
||||
"review": "Revue",
|
||||
"review-and-confirm": "Vérifier et confirmer",
|
||||
@@ -1250,7 +1295,9 @@
|
||||
"select-pool": "Sélectionnez un pool",
|
||||
"select-preset": "Sélectionner une configuration prédéfinie",
|
||||
"select-to-see-details": "Cliquez sur {icon} pour voir les détails",
|
||||
"selected-vms": "VMs sélectionnées",
|
||||
"selected-vms-in-execution": "Certaines VMs sélectionnées sont en cours d'exécution",
|
||||
"selected-vms-n": "VMs sélectionnées ({n})",
|
||||
"self-signed-certificates": "Certificats auto-signés",
|
||||
"send-us-feedback": "Envoyez-nous vos commentaires",
|
||||
"settings": "Paramètres",
|
||||
@@ -1261,6 +1308,7 @@
|
||||
"size": "Taille",
|
||||
"size-and-speed": "@:size et @.lower:speed",
|
||||
"size-on-disk": "Taille sur le disque",
|
||||
"size-on-this-datastore": "Taille sur cette banque de données",
|
||||
"skip-ssl-check": "Ignorer la vérification SSL (certificat auto-signé)",
|
||||
"skip-ssl-check-info": "Le certificat du serveur n'est pas vérifié. À utiliser pour les certificats auto-signés.",
|
||||
"skipped": "Ignoré",
|
||||
@@ -1345,6 +1393,7 @@
|
||||
"success": "Succès",
|
||||
"summary": "Résumé",
|
||||
"support": "Support",
|
||||
"supported": "Prise en charge",
|
||||
"supported-image-formats": "Formats d'image supportés",
|
||||
"suspend-blocked": "Mise en veille bloquée",
|
||||
"suspend-storage-repository": "Espace de stockage en attente",
|
||||
@@ -1360,6 +1409,8 @@
|
||||
"table-selection:watch-out!": "Attention !",
|
||||
"tags": "Tags",
|
||||
"target": "Cible",
|
||||
"target-network": "Réseau cible",
|
||||
"target-sr": "SR cible",
|
||||
"task": "Tâche",
|
||||
"task-type": "Type de tâche",
|
||||
"task:ended": "Terminée",
|
||||
@@ -1448,6 +1499,7 @@
|
||||
"unnamed": "Sans nom",
|
||||
"unreachable-hosts": "Hôtes inaccessibles",
|
||||
"unreachable-hosts-reload-page": "C'est fait. Rafraîchir la page",
|
||||
"unsupported": "Non prise en charge",
|
||||
"untitled": "Sans titre",
|
||||
"untranslated-text-helper": "Par défaut, les textes non traduits seront affichés en anglais.",
|
||||
"up-to-date": "À jour",
|
||||
@@ -1466,17 +1518,47 @@
|
||||
"users": "Utilisateurs",
|
||||
"uuid": "UUID",
|
||||
"v2v": "V2V",
|
||||
"v2v:action:add-to-migration-list": "Ajouter à la liste de migration",
|
||||
"v2v:already-migrated-confirm-at-launch": "Déjà migrée, confirmation au lancement",
|
||||
"v2v:check:mapping-incomplete": "Correspondance incomplète",
|
||||
"v2v:check:n-snapshots": "0 instantané sur la VM source | 1 instantané sur la VM source | {n} instantanés sur la VM source",
|
||||
"v2v:check:no-snapshot": "Aucun instantané sur la VM source",
|
||||
"v2v:check:no-unsupported-disk": "Aucun disque non pris en charge (RDM, indépendant, chiffré)",
|
||||
"v2v:choose-vmware-source": "Choisir la source VMware",
|
||||
"v2v:connect-new-vmware-source": "Connecter une nouvelle source VMware",
|
||||
"v2v:credentials-never-stored": "Les identifiants ne sont jamais stockés par XO : ils restent dans ce navigateur et sont envoyés avec chaque requête qui nécessite VMware. L'état des migrations est partagé par tous ceux qui se connectent au même vCenter.",
|
||||
"v2v:delta-from-last-sync": "Delta depuis la dernière synchronisation",
|
||||
"v2v:destination-pool-info": "Une migration va d’une source vers un pool. Chaque couple source et pool a sa propre liste.",
|
||||
"v2v:existing-list-info": "Ce navigateur contient déjà une liste de migration pour {migration} ({vms}). Continuer la rouvre, avec l’état de migration actuel de chaque VM.",
|
||||
"v2v:filter-vms-info": "filtrer par banque de données, statut ou nom avec le constructeur de requête",
|
||||
"v2v:global-defaults-info": "Appliquées à chaque VM, disque et carte réseau. Chaque VM peut les remplacer à l’étape 4 (Correspondance). Le SR et le réseau sont préremplis avec le SR par défaut du pool et le réseau de management du maître du pool.",
|
||||
"v2v:list-pair-info": "Une source et une destination par liste : choisir une autre source ou un autre pool ouvre sa propre liste.",
|
||||
"v2v:locked-since-first-sync": "Verrouillée depuis la première synchronisation",
|
||||
"v2v:mac-kept": "MAC conservée",
|
||||
"v2v:mapping-locked": "correspondance verrouillée",
|
||||
"v2v:name-from-migration-list": "{name} de la liste de migration",
|
||||
"v2v:next-run-reminder": "Plus la dernière synchronisation est ancienne, plus le delta final est important. Lancez une synchronisation delta peu avant la bascule pour réduire l’interruption.",
|
||||
"v2v:no-bak-info": "Exclue des sauvegardes : {tag} (retiré à la bascule)",
|
||||
"v2v:no-vm-in-list": "Aucune VM dans la liste de migration",
|
||||
"v2v:not-found-in-source": "Introuvable dans la source",
|
||||
"v2v:partial-import-on-pool": "Import partiel sur le pool {pool}",
|
||||
"v2v:prerequisite-checking": "Vérification des prérequis : vérification de vectura…",
|
||||
"v2v:prerequisite-error": "Vérification des prérequis : vectura ne peut pas être utilisé, aucune VM ne peut être migrée tant que ce n'est pas corrigé",
|
||||
"v2v:prerequisite-error-detail": "Vérification des prérequis : vectura ne peut pas être utilisé ({error}), aucune VM ne peut être migrée tant que ce n'est pas corrigé",
|
||||
"v2v:prerequisite-ok": "Vérification des prérequis : vectura OK",
|
||||
"v2v:remove-from-list-info": "Seule la liste de migration change : rien n’est supprimé sur la source ni dans XO.",
|
||||
"v2v:saved-in-this-browser": "Enregistrée dans ce navigateur",
|
||||
"v2v:sent-to-xo-at-launch": "envoyée à XO au lancement",
|
||||
"v2v:source-snapshot-changed": "Le snapshot source a changé depuis la dernière synchronisation",
|
||||
"v2v:start-blocked-partial": "Démarrage bloqué tant que l’import est partiel",
|
||||
"v2v:status:cold": "Bascule effectuée",
|
||||
"v2v:status:warm": "Synchronisée à chaud",
|
||||
"v2v:tags-backup-info": "Utilisez les tags pour retrouver la VM dans les jobs de sauvegarde en mode intelligent. Jusqu’à la bascule, la VM porte aussi {tag}, pour que les jobs de sauvegarde l’ignorent ; ce tag est retiré à la bascule.",
|
||||
"v2v:unsupported-encrypted-disk": "Non prise en charge : disque chiffré",
|
||||
"v2v:unsupported-independent-disk": "Non prise en charge : disque indépendant",
|
||||
"v2v:unsupported-rdm-disk": "Non prise en charge : disque RDM",
|
||||
"value-by-default": "{value} par défaut",
|
||||
"value-from": "Valeur issue de",
|
||||
"vcpu": "vCPU",
|
||||
"vcpu-at-startup": "vCPU au démarrage",
|
||||
"vcpus": "vCPUs",
|
||||
@@ -1525,6 +1607,7 @@
|
||||
"virtualization-mode": "Mode de virtualisation",
|
||||
"virtualization-type": "Type de virtualisation",
|
||||
"vlan": "VLAN",
|
||||
"vlan-id": "VLAN {id}",
|
||||
"vlan-required": "Le VLAN est requis.",
|
||||
"vm": "VM",
|
||||
"vm-backup-failure-number-of-retries": "Nombre de tentatives en cas d'échec de la sauvegarde de la VM",
|
||||
|
||||
@@ -8,6 +8,7 @@ const COLUMN_WIDTH = '4rem'
|
||||
|
||||
export type CheckboxCellBindings = {
|
||||
selected: boolean
|
||||
disabled?: boolean
|
||||
onToggle: () => void
|
||||
}
|
||||
|
||||
@@ -21,9 +22,10 @@ function renderHeadCheckbox(model: Ref<boolean | undefined>) {
|
||||
})
|
||||
}
|
||||
|
||||
function renderBodyCheckbox({ selected, onToggle }: CheckboxCellBindings) {
|
||||
function renderBodyCheckbox({ selected, disabled, onToggle }: CheckboxCellBindings) {
|
||||
return h(UiCheckbox, {
|
||||
accent: 'brand',
|
||||
disabled,
|
||||
modelValue: selected,
|
||||
'onUpdate:modelValue': onToggle,
|
||||
})
|
||||
|
||||
@@ -1,15 +1,142 @@
|
||||
import type { XoV2vInventory, XoV2vVm } from '@vates/types'
|
||||
import { createV2vInventory, createV2vVm } from '../../src/test/create-v2v-inventory.ts'
|
||||
import {
|
||||
V2V_DISK_MODE,
|
||||
V2V_FIRMWARE,
|
||||
V2V_VM_POWER_STATE,
|
||||
type XoV2vDatacenter,
|
||||
type XoV2vDatastore,
|
||||
type XoV2vDisk,
|
||||
type XoV2vHost,
|
||||
type XoV2vInventory,
|
||||
type XoV2vNic,
|
||||
type XoV2vPortGroup,
|
||||
type XoV2vVm,
|
||||
} from '@vates/types'
|
||||
import {
|
||||
createV2vDatastore,
|
||||
createV2vDisk,
|
||||
createV2vInventory,
|
||||
createV2vNic,
|
||||
createV2vVm,
|
||||
} from '../../src/test/create-v2v-inventory.ts'
|
||||
|
||||
const VCENTER_VM_COUNT = 148
|
||||
|
||||
const ESXI_VM_COUNT = 9
|
||||
|
||||
const SNAPSHOTS_VM_INDEX = 4
|
||||
|
||||
// Warm/cold come from real XO VMs, the inventory cannot mock them: to see `app-008` resumed, turn an XO VM of the
|
||||
// destination pool with 2 VDIs and 2 VIFs into its partial import (`sourceId` is `<host>:<port>` for an ESXi
|
||||
// source), one VDI paired on a snapshot delta, the other on the base disk:
|
||||
// xe vm-param-set uuid=<vm> other-config:xo:v2v:sourceId=mock-vcenter-instance-uuid other-config:sourceVmId=vm-107 \
|
||||
// other-config:sourceSnapshotId=2 blocked-operations:start='Esxi migration in progress...'
|
||||
// xe vdi-param-set uuid=<1st vdi> other-config:esxi_diskPath=app-008/app-008-000001.vmdk
|
||||
// xe vdi-param-set uuid=<2nd vdi> other-config:esxi_diskPath=app-008/app-008_1.vmdk
|
||||
const WARM_VM_INDEX = 7
|
||||
|
||||
// One VM per disk case the VM selection step tells apart: each unsupported disk, and disks on two datastores. The
|
||||
// last three are past the ESXi VMs, so only the vCenter shows them
|
||||
const RDM_VM_INDEX = 5
|
||||
|
||||
const TWO_DATASTORES_VM_INDEX = 10
|
||||
|
||||
const INDEPENDENT_DISK_VM_INDEX = 11
|
||||
|
||||
const ENCRYPTED_DISK_VM_INDEX = 12
|
||||
|
||||
const DATACENTER_ID = 'datacenter-1' as XoV2vDatacenter['id']
|
||||
|
||||
const HOST_ID = 'host-1' as XoV2vHost['id']
|
||||
|
||||
const PORT_GROUP_ID = 'network-1' as XoV2vPortGroup['id']
|
||||
|
||||
const DATASTORES = [
|
||||
{ id: 'datastore-11', name: 'ds-prod-01', capacity: 4 * 1024 ** 4, free: 1.1 * 1024 ** 4 },
|
||||
{ id: 'datastore-12', name: 'ds-prod-02', capacity: 4 * 1024 ** 4, free: 1.1 * 1024 ** 4 },
|
||||
{ id: 'datastore-13', name: 'ds-local-esx03', capacity: 1024 ** 4, free: 512 * 1024 ** 3 },
|
||||
].map(({ id, ...datastore }) =>
|
||||
createV2vDatastore({ ...datastore, id: id as XoV2vDatastore['id'], hostIds: [HOST_ID] })
|
||||
)
|
||||
|
||||
const SNAPSHOT_COUNTS: Record<number, number> = { [SNAPSHOTS_VM_INDEX]: 1, [WARM_VM_INDEX]: 2 }
|
||||
|
||||
// VMware names the deltas of `dir/disk.vmdk` `dir/disk-000001.vmdk`, `dir/disk-000002.vmdk`… one per snapshot
|
||||
function createChainPaths(directory: string, fileName: string, snapshotCount: number): string[] {
|
||||
const deltaPaths = Array.from(
|
||||
{ length: snapshotCount },
|
||||
(_, deltaIndex) => `${directory}/${fileName}-${String(deltaIndex + 1).padStart(6, '0')}.vmdk`
|
||||
)
|
||||
|
||||
return [`${directory}/${fileName}.vmdk`, ...deltaPaths]
|
||||
}
|
||||
|
||||
function createDisks(index: number, name: string, snapshotCount: number): XoV2vDisk[] {
|
||||
const datastoreId = DATASTORES[index % DATASTORES.length].id
|
||||
|
||||
const disk = createV2vDisk({
|
||||
datastoreId,
|
||||
capacity: (40 + (index % 5) * 20) * 1024 ** 3,
|
||||
mode: index === INDEPENDENT_DISK_VM_INDEX ? V2V_DISK_MODE.INDEPENDENT_PERSISTENT : V2V_DISK_MODE.PERSISTENT,
|
||||
isRdm: index === RDM_VM_INDEX,
|
||||
isEncrypted: index === ENCRYPTED_DISK_VM_INDEX,
|
||||
chainPaths: createChainPaths(name, name, snapshotCount),
|
||||
})
|
||||
|
||||
const hasSecondDisk = index === TWO_DATASTORES_VM_INDEX || index === WARM_VM_INDEX
|
||||
|
||||
if (!hasSecondDisk) {
|
||||
return [disk]
|
||||
}
|
||||
|
||||
const secondDatastoreId =
|
||||
index === TWO_DATASTORES_VM_INDEX ? DATASTORES[(index + 1) % DATASTORES.length].id : datastoreId
|
||||
|
||||
return [
|
||||
disk,
|
||||
createV2vDisk({
|
||||
key: 'scsi0:1' as XoV2vDisk['key'],
|
||||
label: 'Hard disk 2',
|
||||
datastoreId: secondDatastoreId,
|
||||
capacity: 200 * 1024 ** 3,
|
||||
chainPaths: createChainPaths(name, `${name}_1`, snapshotCount),
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
function createNics(index: number): XoV2vNic[] {
|
||||
const nicCount = index === WARM_VM_INDEX ? 2 : 1
|
||||
|
||||
return Array.from({ length: nicCount }, (_, nicIndex) =>
|
||||
createV2vNic({
|
||||
key: `ethernet${nicIndex}` as XoV2vNic['key'],
|
||||
label: `Network adapter ${nicIndex + 1}`,
|
||||
portGroupId: PORT_GROUP_ID,
|
||||
macAddress: `00:50:56:9a:0${nicIndex}:${index.toString(16).padStart(2, '0')}`,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
// Varied on purpose: the list must show every power state, firmware and size combination
|
||||
function createVm(index: number): XoV2vVm {
|
||||
const name = `app-${String(index + 1).padStart(3, '0')}`
|
||||
const snapshotCount = SNAPSHOT_COUNTS[index] ?? 0
|
||||
const disks = createDisks(index, name, snapshotCount)
|
||||
|
||||
return createV2vVm({
|
||||
id: `vm-${index + 100}` as XoV2vVm['id'],
|
||||
instanceUuid: `vm-instance-${index + 100}`,
|
||||
name: `app-${String(index + 1).padStart(3, '0')}`,
|
||||
name,
|
||||
powerState: index % 3 === 0 ? V2V_VM_POWER_STATE.POWERED_OFF : V2V_VM_POWER_STATE.POWERED_ON,
|
||||
hostId: HOST_ID,
|
||||
nCpus: 2 + (index % 4),
|
||||
memory: (4 + (index % 4) * 4) * 1024 ** 3,
|
||||
firmware: index % 2 === 0 ? V2V_FIRMWARE.UEFI : V2V_FIRMWARE.BIOS,
|
||||
datastoreIds: [...new Set(disks.map(disk => disk.datastoreId))],
|
||||
snapshotCount,
|
||||
// The vmsd numbers snapshots from 1, the last taken is the current one
|
||||
currentSnapshotId: snapshotCount > 0 ? String(snapshotCount) : undefined,
|
||||
disks,
|
||||
nics: createNics(index),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -20,6 +147,10 @@ export function createMockInventory(host: string): XoV2vInventory {
|
||||
product: isVcenter ? 'VMware vCenter Server' : 'VMware ESXi',
|
||||
version: isVcenter ? '8.0.2' : '7.0.3',
|
||||
instanceUuid: isVcenter ? 'mock-vcenter-instance-uuid' : undefined,
|
||||
datacenters: [{ id: DATACENTER_ID, name: 'DC-Paris' }],
|
||||
hosts: [{ id: HOST_ID, name: 'esx01.lab.local', datacenterId: DATACENTER_ID }],
|
||||
datastores: DATASTORES,
|
||||
portGroups: [{ id: PORT_GROUP_ID, name: 'PG-Prod', vlan: 10 }],
|
||||
vms: Array.from({ length: isVcenter ? VCENTER_VM_COUNT : ESXI_VM_COUNT }, (_, index) => createVm(index)),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -14,6 +14,10 @@ A fixed priority, not a preference:
|
||||
|
||||
Testing a page is a different job from testing a component, and the component is the more valuable one. A component should not carry much complex logic — when it does, extract that into a composable or utility and test it there.
|
||||
|
||||
### Only the app's own behaviour
|
||||
|
||||
An XO 6 test covers what the app decides, never what `@xen-orchestra/web-core` does on its own: a side panel opening on a selection, a store re-expanding on a resize, a select opening its options all belong to the web-core tests. A web-core component may be rendered beside the code under test when the app's rule depends on it, but the assertion must be about that rule: a test that still passes once the app's code is removed is testing web-core and is deleted, not written. A state the app sets on a web-core component (`busy`, `disabled`, `selected`, a table `state`…) is asserted through that component's props (`getUiButton(wrapper, label).props('busy')`), never through the DOM it renders for it; a user action clicks the real `<button>` (`getButton(wrapper, label)`). A composable member that only forwards to a web-core store gets no test of its own. Selects: see [Testing components](./tests/testing-components.md#selects).
|
||||
|
||||
## Getting started
|
||||
|
||||
- [Assert behaviour, not implementation](./tests/assert-behaviour.md) — the one rule that outranks the others
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -35,7 +35,21 @@ beforeEach(() => {
|
||||
})
|
||||
```
|
||||
|
||||
Collections always read together share one mock module exporting each of their composables: `mock-xo-collections.ts` stands for the pool, host, SR, network, PBD, PIF and VM template collections. Each `vi.mock` of these modules points to it (`vi.mock` is hoisted, it cannot be wrapped in a helper), and `xoCollectionsMock.reset()` resets them all. Its pool collection also answers `hasPoolById` and `useGetPoolById` from `pools`, its host collection `getHostById` from `hosts`, its PBD collection `pbdsBySr` from `pbds`, and `arePoolsReady` (true after a reset) lets a test hold the pools as still loading.
|
||||
`vmCollectionMock.areVmsReady` is true after a reset; set it to false to hold the collection as still loading.
|
||||
|
||||
Collections always read together share one mock module exporting each of their composables: `mock-xo-collections.ts` stands for the pool, host, SR, network, PBD, PIF and VM template collections. Each `vi.mock` of these modules points to it (`vi.mock` is hoisted, it cannot be wrapped in a helper), and `xoCollectionsMock.reset()` resets them all. Its pool collection also answers `getPoolById`, `hasPoolById` and `useGetPoolById` from `pools`, its host collection `getHostById` from `hosts`, its SR and network collections `getSrById` and `getNetworkById`, its PBD collection `pbdsBySr` from `pbds`, and `arePoolsReady`, `areSrsReady`, `areNetworksReady` and `areTemplatesReady` (true after a reset) let a test hold that collection as still loading. `seedV2vListPool` (`seed-v2v-list-pool.ts`) seeds the pool of a V2V list and what it offers.
|
||||
|
||||
The same goes for `mock-xo-v2v-vm-rows-collections.ts`, the VM, task and VM snapshot collections the V2V VM rows are built from: `v2vVmRowsCollectionsMock` holds `vms`, `tasks`, `snapshots` and their `are…Ready` flags (true after a reset; set one to false to hold that collection as still loading).
|
||||
|
||||
`mock-delete-modal.ts` stands for `useDeleteModal`, whose overlay a unit test cannot answer: `deleteModalMock.answer('onConfirm' | 'onCancel')` sets how the next confirmation is answered, `deleteModalMock.confirm()` runs the `onConfirm` event of the last confirmation for a caller that passes its events instead of awaiting the answer, `deleteModalMock.open` is the spy to read the props it was opened with, and `deleteModalMock.reset()` goes in `beforeEach`.
|
||||
|
||||
`mock-overlay.ts` stands for `useOverlay`, whose overlays a unit test cannot open: each call is recorded in order, as the options it was given (its `component`, its `events`) and the `open` spy it got back; `overlayMock.getOverlay<TOptions>(index = 0)` / `overlayMock.getOverlays<TOptions>()` read them back, `TOptions` typing the events for the payload they take, and `overlayMock.reset()` goes in `beforeEach`.
|
||||
|
||||
`mock-v2v-vm-actions.ts` stands for `use-v2v-vm-actions.composable.ts`, whose composables open the settings drawer or delete an import VM: each `v2vVmActionsMock.useV2v*VmActions` spy returns fresh fake actions of its kind (read them back from `mock.results`), and `v2vVmActionsMock.reset()` goes in `beforeEach`.
|
||||
|
||||
`mock-xo-vm-device-collections.ts` stands for the VBD, VDI and VIF collections: `vmDeviceCollectionsMock` holds `vbds`, `vdis`, `vifs` and their `are…Ready` flags (true after a reset), and its collections answer `getVbdsByIds` and `getVifsByIds`.
|
||||
|
||||
`mock-xo-routes.ts` stands for `useXoRoutes`, the XO 5 routes the app fetches: an XO 5 link reads `https://xo5.example.com/#<path>`.
|
||||
|
||||
## Network
|
||||
|
||||
|
||||
@@ -26,7 +26,11 @@ The exception is a **reactivity** test: it must keep reading through the mounted
|
||||
|
||||
## Unmounting
|
||||
|
||||
`src/test/setup.ts` calls `enableAutoUnmount(afterEach)`, so every wrapper — `mount` or `mountComposable` — is unmounted after the test that created it. Never unmount by hand, and never mount in `beforeAll`: that wrapper would be torn down after the first test.
|
||||
`src/test/setup.ts` calls `enableAutoUnmount(afterEach)`, so every wrapper — `mount` or `mountComposable` — is unmounted after the test that created it. Never unmount by hand for cleanup, and never mount in `beforeAll`: that wrapper would be torn down after the first test. The one `wrapper.unmount()` allowed is the act of a test asserting what `onUnmounted` does.
|
||||
|
||||
## Local storage
|
||||
|
||||
`src/test/setup.ts` clears `localStorage` before each test, so every test starts with an empty storage. Never clear it again in a test file: store what a test needs in its own setup (e.g. `seedV2vMigrationList`).
|
||||
|
||||
## Shared helpers: `use<X>EnhancedData`
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ expect(labelledValues).toEqual({ [t('vga')]: t('disabled'), [t('video-ram')]: '8
|
||||
|
||||
Prefer this shape: one assertion covering every row a user sees, and it fails loudly when a value lands under the wrong label. Assert against **class names and semantic elements**, never a positional chain of child indexes.
|
||||
|
||||
Only the classes the markup already carries count: a web-core component's own (`.ui-head-bar .status`) or one a style rule of the component uses. Never add a class only for a test to find an element; query the component (`getComponent`) or the web-core class around it instead.
|
||||
The class must exist for the component's own CSS: **never add a class only for a test**, and remove one whose CSS is gone. Without a styled class, query the component (`getComponent`), a styled class of a child component, or the text the user reads.
|
||||
|
||||
`src/test/find-rendered-values.ts` does exactly that reduction, so a card test does not re-roll it:
|
||||
|
||||
@@ -133,6 +133,25 @@ Where the line falls:
|
||||
| data reaching the component (a name, an id, an IP, a tag) | the literal — it is the test's own input |
|
||||
| a number the component formats (`'8 B'`, `'4 GiB'`, `'2'`) | the literal — deriving it would copy the template |
|
||||
|
||||
## Tables
|
||||
|
||||
A table is read **by header label**, never by column position: `src/test/find-table-cells.ts` looks the label up in `thead th`, so a column added, moved or hidden does not shift the assertions.
|
||||
|
||||
| Helper | Reads |
|
||||
| -------------------------------------------------------- | --------------------------------------------------------------------- |
|
||||
| `findTableHeaders(wrapper)` | the header labels, in order (`''` for a checkbox column) |
|
||||
| `findTableColumn(wrapper, label)` | the text of each cell of that column, one per body row |
|
||||
| `findTableColumnCells(wrapper, label)` | the cells of that column, to query a link, a tag or a checkbox inside |
|
||||
| `findTableRows(wrapper)`, from `find-rendered-values.ts` | the cell texts of each body row, for a table read row by row |
|
||||
|
||||
```typescript
|
||||
expect(findTableHeaders(wrapper)).toEqual(['', t('vm'), t('size')])
|
||||
expect(findTableColumn(wrapper, t('size'))).toEqual(['80GiB', '120GiB'])
|
||||
expect(findTableColumnCells(wrapper, t('vm'))[0].getComponent(UiLink).attributes('href')).toBe('/vm/vm-42/dashboard')
|
||||
```
|
||||
|
||||
A label missing from the header throws, naming the label, rather than reading an empty column.
|
||||
|
||||
## Chart cards
|
||||
|
||||
A dashboard chart card loads its `VtsLinearChart` asynchronously, and the real chart cannot mount here: ECharts needs a canvas and a `ResizeObserver`. `src/test/linear-chart-stub.ts` stands in for it, with the props declared so they stay readable. `mountChartCard` from `src/test/mount-chart-card.ts` wires that stub in and defaults `loading` to `false`, so a card test only names its component and its stats type:
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import BackupJobsTable from '@/modules/backup-job/components/list/BackupJobsTable.vue'
|
||||
import type { FrontAnyXoBackupJob } from '@/modules/backup-job/remote-resources/use-xo-backup-job-collection.ts'
|
||||
import type { useXoBackupLogCollection } from '@/modules/backup-log/remote-resources/use-xo-backup-log-collection.ts'
|
||||
import type { useXoRoutes } from '@/shared/remote-resources/use-xo-routes.ts'
|
||||
import { createMetadataBackupJob, createMirrorBackupJob, createVmBackupJob } from '@/test/create-backup-job.ts'
|
||||
import { createBackupLog } from '@/test/create-backup-log.ts'
|
||||
import { createTestRouter } from '@/test/create-test-router.ts'
|
||||
@@ -21,11 +20,7 @@ const { areBackupLogsReady, getLastNBackupLogsByJobId } = vi.hoisted(() => ({
|
||||
getLastNBackupLogsByJobId: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock(import('@/shared/remote-resources/use-xo-routes.ts'), () => ({
|
||||
useXoRoutes: (() => ({
|
||||
buildXo5Route: (path: string) => `https://xo5.example.com/#${path}`,
|
||||
})) as unknown as typeof useXoRoutes,
|
||||
}))
|
||||
vi.mock(import('@/shared/remote-resources/use-xo-routes.ts'), () => import('@/test/mock-xo-routes.ts'))
|
||||
|
||||
vi.mock(import('@/modules/backup-log/remote-resources/use-xo-backup-log-collection.ts'), () => ({
|
||||
useXoBackupLogCollection: (() => ({
|
||||
|
||||
@@ -21,7 +21,6 @@ import type {
|
||||
} from '@/modules/schedule/remote-resources/use-xo-schedule-collection.ts'
|
||||
import type { useXoSrCollection } from '@/modules/storage-repository/remote-resources/use-xo-sr-collection.ts'
|
||||
import type { useXoVmCollection } from '@/modules/vm/remote-resources/use-xo-vm-collection.ts'
|
||||
import type { useXoRoutes } from '@/shared/remote-resources/use-xo-routes.ts'
|
||||
import { createMetadataBackupJob, createVmBackupJob } from '@/test/create-backup-job.ts'
|
||||
import { createBackupLog } from '@/test/create-backup-log.ts'
|
||||
import { createSchedule } from '@/test/create-schedule.ts'
|
||||
@@ -36,11 +35,7 @@ const { schedulesByJobId, backupLogsByJobId } = vi.hoisted(() => ({
|
||||
backupLogsByJobId: new Map<string, FrontXoBackupLog[]>(),
|
||||
}))
|
||||
|
||||
vi.mock(import('@/shared/remote-resources/use-xo-routes.ts'), () => ({
|
||||
useXoRoutes: (() => ({
|
||||
buildXo5Route: (path: string) => `https://xo5.example.com/#${path}`,
|
||||
})) as unknown as typeof useXoRoutes,
|
||||
}))
|
||||
vi.mock(import('@/shared/remote-resources/use-xo-routes.ts'), () => import('@/test/mock-xo-routes.ts'))
|
||||
|
||||
vi.mock(import('@/modules/schedule/remote-resources/use-xo-schedule-collection.ts'), async () => {
|
||||
const { computed: vueComputed, ref: vueRef } = await import('vue')
|
||||
|
||||
@@ -4,7 +4,6 @@ import type {
|
||||
FrontXoSchedule,
|
||||
useXoScheduleCollection,
|
||||
} from '@/modules/schedule/remote-resources/use-xo-schedule-collection.ts'
|
||||
import type { useXoRoutes } from '@/shared/remote-resources/use-xo-routes.ts'
|
||||
import { createBackupLog, createBackupLogTask } from '@/test/create-backup-log.ts'
|
||||
import { createSchedule } from '@/test/create-schedule.ts'
|
||||
import { findCardLabelledValues } from '@/test/find-rendered-values.ts'
|
||||
@@ -20,11 +19,7 @@ const { schedules } = vi.hoisted(() => ({
|
||||
schedules: [] as FrontXoSchedule[],
|
||||
}))
|
||||
|
||||
vi.mock(import('@/shared/remote-resources/use-xo-routes.ts'), () => ({
|
||||
useXoRoutes: (() => ({
|
||||
buildXo5Route: (path: string) => `https://xo5.example.com/#${path}`,
|
||||
})) as unknown as typeof useXoRoutes,
|
||||
}))
|
||||
vi.mock(import('@/shared/remote-resources/use-xo-routes.ts'), () => import('@/test/mock-xo-routes.ts'))
|
||||
|
||||
vi.mock(import('@/modules/schedule/remote-resources/use-xo-schedule-collection.ts'), () => ({
|
||||
useXoScheduleCollection: (() => ({
|
||||
|
||||
@@ -6,7 +6,6 @@ import type {
|
||||
FrontXoSr,
|
||||
useXoSrCollection,
|
||||
} from '@/modules/storage-repository/remote-resources/use-xo-sr-collection.ts'
|
||||
import type { useXoRoutes } from '@/shared/remote-resources/use-xo-routes.ts'
|
||||
import { createMetadataBackupJob, createVmBackupJob } from '@/test/create-backup-job.ts'
|
||||
import { createSr } from '@/test/create-sr.ts'
|
||||
import { findCardLabelledValues } from '@/test/find-rendered-values.ts'
|
||||
@@ -24,11 +23,7 @@ const { srs } = vi.hoisted(() => ({
|
||||
srs: [] as FrontXoSr[],
|
||||
}))
|
||||
|
||||
vi.mock(import('@/shared/remote-resources/use-xo-routes.ts'), () => ({
|
||||
useXoRoutes: (() => ({
|
||||
buildXo5Route: (path: string) => `https://xo5.example.com/#${path}`,
|
||||
})) as unknown as typeof useXoRoutes,
|
||||
}))
|
||||
vi.mock(import('@/shared/remote-resources/use-xo-routes.ts'), () => import('@/test/mock-xo-routes.ts'))
|
||||
|
||||
vi.mock(import('@/modules/storage-repository/remote-resources/use-xo-sr-collection.ts'), () => ({
|
||||
useXoSrCollection: (() => ({
|
||||
|
||||
@@ -2,7 +2,6 @@ import BackupJobRunItem from '@/modules/backup-job/components/list/panel/card-it
|
||||
import BackupJobLogsCard from '@/modules/backup-job/components/list/panel/cards/BackupJobLogsCard.vue'
|
||||
import type { FrontXoBackupLog } from '@/modules/backup-log/remote-resources/use-xo-backup-log-collection.ts'
|
||||
import type { useXoScheduleCollection } from '@/modules/schedule/remote-resources/use-xo-schedule-collection.ts'
|
||||
import type { useXoRoutes } from '@/shared/remote-resources/use-xo-routes.ts'
|
||||
import { createBackupLog } from '@/test/create-backup-log.ts'
|
||||
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
|
||||
import { t } from '@/test/i18n.ts'
|
||||
@@ -10,11 +9,7 @@ import UiPanelCardTitle from '@core/components/ui/panel-card-title/UiPanelCardTi
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { computed } from 'vue'
|
||||
|
||||
vi.mock(import('@/shared/remote-resources/use-xo-routes.ts'), () => ({
|
||||
useXoRoutes: (() => ({
|
||||
buildXo5Route: (path: string) => `https://xo5.example.com/#${path}`,
|
||||
})) as unknown as typeof useXoRoutes,
|
||||
}))
|
||||
vi.mock(import('@/shared/remote-resources/use-xo-routes.ts'), () => import('@/test/mock-xo-routes.ts'))
|
||||
|
||||
vi.mock(import('@/modules/schedule/remote-resources/use-xo-schedule-collection.ts'), () => ({
|
||||
useXoScheduleCollection: (() => ({
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import BackupJobSchedulesCard from '@/modules/backup-job/components/list/panel/cards/BackupJobSchedulesCard.vue'
|
||||
import type { FrontXoSchedule } from '@/modules/schedule/remote-resources/use-xo-schedule-collection.ts'
|
||||
import { getScheduleNextRun } from '@/modules/schedule/utils/xo-schedule.util.ts'
|
||||
import type { useXoRoutes } from '@/shared/remote-resources/use-xo-routes.ts'
|
||||
import { createSchedule } from '@/test/create-schedule.ts'
|
||||
import { findCardLabelledValues } from '@/test/find-rendered-values.ts'
|
||||
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
|
||||
@@ -12,11 +11,7 @@ import UiPanelCardTitle from '@core/components/ui/panel-card-title/UiPanelCardTi
|
||||
import { formatDateTime } from '@core/utils/time.util.ts'
|
||||
import { mount } from '@vue/test-utils'
|
||||
|
||||
vi.mock(import('@/shared/remote-resources/use-xo-routes.ts'), () => ({
|
||||
useXoRoutes: (() => ({
|
||||
buildXo5Route: (path: string) => `https://xo5.example.com/#${path}`,
|
||||
})) as unknown as typeof useXoRoutes,
|
||||
}))
|
||||
vi.mock(import('@/shared/remote-resources/use-xo-routes.ts'), () => import('@/test/mock-xo-routes.ts'))
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
|
||||
@@ -5,9 +5,9 @@ import type { useXoHostCollection } from '@/modules/host/remote-resources/use-xo
|
||||
import type { useXoPbdCollection } from '@/modules/pbd/remote-resources/use-xo-pbd-collection.ts'
|
||||
import type { useXoSrCollection } from '@/modules/storage-repository/remote-resources/use-xo-sr-collection.ts'
|
||||
import type { FrontXoVm, useXoVmCollection } from '@/modules/vm/remote-resources/use-xo-vm-collection.ts'
|
||||
import type { useXoRoutes } from '@/shared/remote-resources/use-xo-routes.ts'
|
||||
import { createBackupLog, createBackupLogVmTask } from '@/test/create-backup-log.ts'
|
||||
import { createVm } from '@/test/create-vm.ts'
|
||||
import { findTableColumnCells } from '@/test/find-table-cells.ts'
|
||||
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
|
||||
import { t } from '@/test/i18n.ts'
|
||||
import UiLink from '@core/components/ui/link/UiLink.vue'
|
||||
@@ -20,11 +20,7 @@ const { getBackupJobById, getVmById } = vi.hoisted(() => ({
|
||||
getVmById: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock(import('@/shared/remote-resources/use-xo-routes.ts'), () => ({
|
||||
useXoRoutes: (() => ({
|
||||
buildXo5Route: (path: string) => `https://xo5.example.com/#${path}`,
|
||||
})) as unknown as typeof useXoRoutes,
|
||||
}))
|
||||
vi.mock(import('@/shared/remote-resources/use-xo-routes.ts'), () => import('@/test/mock-xo-routes.ts'))
|
||||
|
||||
vi.mock(import('@/modules/backup-job/remote-resources/use-xo-backup-job-collection.ts'), () => ({
|
||||
useXoBackupJobCollection: (() => ({ getBackupJobById })) as unknown as typeof useXoBackupJobCollection,
|
||||
@@ -73,9 +69,7 @@ function mountTable(backupLog: FrontXoBackupLog) {
|
||||
}
|
||||
|
||||
function findVmCell(wrapper: ReturnType<typeof mountTable>) {
|
||||
const headers = wrapper.findAll('thead th').map(header => header.text())
|
||||
|
||||
return wrapper.findAll('tbody tr td')[headers.indexOf(t('vm'))]
|
||||
return findTableColumnCells(wrapper, t('vm'))[0]
|
||||
}
|
||||
|
||||
describe('VM', () => {
|
||||
|
||||
@@ -49,7 +49,8 @@ const emit = defineEmits<{
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { general, details, detailsTitle, hasMixedTypes, validateAndBuildPayloads } = useBulkEditBackupRepositoriesForm(brs)
|
||||
const { general, details, detailsTitle, hasMixedTypes, validateAndBuildPayloads } =
|
||||
useBulkEditBackupRepositoriesForm(brs)
|
||||
|
||||
async function handleConfirm() {
|
||||
const payloads = await validateAndBuildPayloads()
|
||||
|
||||
@@ -51,7 +51,10 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { MASKED_SECRET, splitBackupRepositoryPath } from '@/modules/backup-repository/utils/xo-backup-repository.util.ts'
|
||||
import {
|
||||
MASKED_SECRET,
|
||||
splitBackupRepositoryPath,
|
||||
} from '@/modules/backup-repository/utils/xo-backup-repository.util.ts'
|
||||
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
|
||||
import VtsCopyButton from '@core/components/copy-button/VtsCopyButton.vue'
|
||||
import VtsStatus from '@core/components/status/VtsStatus.vue'
|
||||
|
||||
@@ -65,7 +65,10 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { MASKED_SECRET, splitBackupRepositoryPath } from '@/modules/backup-repository/utils/xo-backup-repository.util.ts'
|
||||
import {
|
||||
MASKED_SECRET,
|
||||
splitBackupRepositoryPath,
|
||||
} from '@/modules/backup-repository/utils/xo-backup-repository.util.ts'
|
||||
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
|
||||
import VtsCopyButton from '@core/components/copy-button/VtsCopyButton.vue'
|
||||
import VtsStatus from '@core/components/status/VtsStatus.vue'
|
||||
|
||||
@@ -4,9 +4,9 @@ import type { FrontXoBackupRepository } from '@/modules/backup-repository/remote
|
||||
import { createBr } from '@/test/create-br.ts'
|
||||
import { createTestRouter } from '@/test/create-test-router.ts'
|
||||
import { t } from '@/test/i18n.ts'
|
||||
import { overlayMock } from '@/test/mock-overlay.ts'
|
||||
import { mountComposable } from '@/test/mount-composable.ts'
|
||||
import type { useRouteQuery } from '@core/composables/route-query.composable.ts'
|
||||
import type { useOverlay } from '@core/packages/overlay/use-overlay.ts'
|
||||
import { type MaybeRefOrGetter, ref } from 'vue'
|
||||
import type { Router } from 'vue-router'
|
||||
|
||||
@@ -16,8 +16,7 @@ type ForgetResults = PromiseSettledResult<void>[]
|
||||
|
||||
type MockedOverlay = { open: ReturnType<typeof vi.fn>; events: OverlayEvents }
|
||||
|
||||
const { overlays, run, selectedBrId } = vi.hoisted(() => ({
|
||||
overlays: [] as MockedOverlay[],
|
||||
const { run, selectedBrId } = vi.hoisted(() => ({
|
||||
run: vi.fn(),
|
||||
selectedBrId: { value: '' },
|
||||
}))
|
||||
@@ -36,21 +35,13 @@ vi.mock(import('@core/composables/route-query.composable.ts'), () => ({
|
||||
useRouteQuery: (() => selectedBrId) as unknown as typeof useRouteQuery,
|
||||
}))
|
||||
|
||||
vi.mock(import('@core/packages/overlay/use-overlay.ts'), () => ({
|
||||
useOverlay: (({ events }: Pick<MockedOverlay, 'events'>) => {
|
||||
const overlay = { open: vi.fn(), events }
|
||||
|
||||
overlays.push(overlay)
|
||||
|
||||
return { open: overlay.open }
|
||||
}) as unknown as typeof useOverlay,
|
||||
}))
|
||||
vi.mock(import('@core/packages/overlay/use-overlay.ts'), () => import('@/test/mock-overlay.ts'))
|
||||
|
||||
let successHandled: Promise<void> | undefined
|
||||
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
overlays.length = 0
|
||||
overlayMock.reset()
|
||||
run.mockReset()
|
||||
selectedBrId.value = ''
|
||||
successHandled = undefined
|
||||
@@ -71,7 +62,7 @@ function mountForget(brs: MaybeRefOrGetter<FrontXoBackupRepository[]>, { router
|
||||
const result = mountComposable(() => useBackupRepositoryForget(brs), { router }).wrapper.vm
|
||||
|
||||
// The composable declares the single repository modal first, then the type to confirm one
|
||||
const [forgetModal, typeToConfirmModal] = overlays
|
||||
const [forgetModal, typeToConfirmModal] = overlayMock.getOverlays<MockedOverlay>()
|
||||
|
||||
return { result, forgetModal, typeToConfirmModal }
|
||||
}
|
||||
@@ -82,7 +73,7 @@ async function forgetAndConfirm(brs: MaybeRefOrGetter<FrontXoBackupRepository[]>
|
||||
|
||||
result.forgetBackupRepositories()
|
||||
|
||||
const openedModal = overlays.find(overlay => overlay.open.mock.calls.length > 0)
|
||||
const openedModal = overlayMock.getOverlays<MockedOverlay>().find(overlay => overlay.open.mock.calls.length > 0)
|
||||
|
||||
// The events can be declared with the modal, or given when opening it
|
||||
const openEvents: OverlayEvents | undefined = openedModal?.open.mock.lastCall?.[0]?.events
|
||||
|
||||
@@ -6,9 +6,9 @@ import type {
|
||||
import type { EditBackupRepositoryPayload } from '@/modules/backup-repository/jobs/xo-backup-repository-edit.job.ts'
|
||||
import type { FrontXoBackupRepository } from '@/modules/backup-repository/remote-resources/use-xo-backup-repository-collection.ts'
|
||||
import { createBr } from '@/test/create-br.ts'
|
||||
import { overlayMock } from '@/test/mock-overlay.ts'
|
||||
import { mountComposable } from '@/test/mount-composable.ts'
|
||||
import { silenceConsoleError } from '@/test/silence-console-error.ts'
|
||||
import type { useOverlay } from '@core/packages/overlay/use-overlay.ts'
|
||||
import { KEEP_OVERLAY_OPEN } from '@core/packages/overlay/symbols.ts'
|
||||
import { ref, toValue } from 'vue'
|
||||
|
||||
@@ -17,8 +17,7 @@ type MockedOverlay = {
|
||||
events: { onConfirm: (payload: never) => Promise<unknown> }
|
||||
}
|
||||
|
||||
const { overlays, useBulkEditJob, run } = vi.hoisted(() => ({
|
||||
overlays: [] as MockedOverlay[],
|
||||
const { useBulkEditJob, run } = vi.hoisted(() => ({
|
||||
useBulkEditJob: vi.fn(),
|
||||
run: vi.fn(),
|
||||
}))
|
||||
@@ -28,19 +27,11 @@ vi.mock(import('@/modules/backup-repository/jobs/xo-backup-repository-bulk-edit.
|
||||
useXoBackupRepositoryBulkEditJob: useBulkEditJob as unknown as typeof useXoBackupRepositoryBulkEditJob,
|
||||
}))
|
||||
|
||||
vi.mock(import('@core/packages/overlay/use-overlay.ts'), () => ({
|
||||
useOverlay: (({ events }: Pick<MockedOverlay, 'events'>) => {
|
||||
const overlay = { open: vi.fn(), events }
|
||||
|
||||
overlays.push(overlay)
|
||||
|
||||
return { open: overlay.open }
|
||||
}) as unknown as typeof useOverlay,
|
||||
}))
|
||||
vi.mock(import('@core/packages/overlay/use-overlay.ts'), () => import('@/test/mock-overlay.ts'))
|
||||
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
overlays.length = 0
|
||||
overlayMock.reset()
|
||||
useBulkEditJob.mockReset()
|
||||
run.mockReset()
|
||||
|
||||
@@ -71,7 +62,7 @@ function mountBulkEdit(brs: FrontXoBackupRepository[]) {
|
||||
const result = mountComposable(() => useBulkEditBackupRepositories(() => brs)).wrapper.vm
|
||||
|
||||
// The composable declares the edit drawer of a single repository first, then the bulk edit one
|
||||
const [editDrawer, bulkEditDrawer] = overlays
|
||||
const [editDrawer, bulkEditDrawer] = overlayMock.getOverlays<MockedOverlay>()
|
||||
|
||||
return { result, editDrawer, bulkEditDrawer }
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@ import type {
|
||||
} from '@/modules/backup-repository/jobs/xo-backup-repository-edit.job.ts'
|
||||
import type { FrontXoBackupRepository } from '@/modules/backup-repository/remote-resources/use-xo-backup-repository-collection.ts'
|
||||
import { createBr } from '@/test/create-br.ts'
|
||||
import { overlayMock } from '@/test/mock-overlay.ts'
|
||||
import { mountComposable } from '@/test/mount-composable.ts'
|
||||
import { silenceConsoleError } from '@/test/silence-console-error.ts'
|
||||
import type { useOverlay } from '@core/packages/overlay/use-overlay.ts'
|
||||
import { KEEP_OVERLAY_OPEN } from '@core/packages/overlay/symbols.ts'
|
||||
import { type Ref, ref } from 'vue'
|
||||
|
||||
@@ -18,20 +18,16 @@ type OverlayOptions = {
|
||||
}
|
||||
}
|
||||
|
||||
const { useEditJob, run, useOverlayMock, open } = vi.hoisted(() => ({
|
||||
const { useEditJob, run } = vi.hoisted(() => ({
|
||||
useEditJob: vi.fn(),
|
||||
run: vi.fn(),
|
||||
useOverlayMock: vi.fn(),
|
||||
open: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock(import('@/modules/backup-repository/jobs/xo-backup-repository-edit.job.ts'), () => ({
|
||||
useXoBackupRepositoryEditJob: useEditJob as unknown as typeof useXoBackupRepositoryEditJob,
|
||||
}))
|
||||
|
||||
vi.mock(import('@core/packages/overlay/use-overlay.ts'), () => ({
|
||||
useOverlay: useOverlayMock as unknown as typeof useOverlay,
|
||||
}))
|
||||
vi.mock(import('@core/packages/overlay/use-overlay.ts'), () => import('@/test/mock-overlay.ts'))
|
||||
|
||||
const PAYLOAD: EditBackupRepositoryPayload = {
|
||||
name: 'Weekly backups',
|
||||
@@ -43,8 +39,7 @@ const PAYLOAD: EditBackupRepositoryPayload = {
|
||||
beforeEach(() => {
|
||||
useEditJob.mockReset()
|
||||
run.mockReset()
|
||||
useOverlayMock.mockReset()
|
||||
open.mockReset()
|
||||
overlayMock.reset()
|
||||
|
||||
silenceConsoleError()
|
||||
|
||||
@@ -54,8 +49,6 @@ beforeEach(() => {
|
||||
isRunning: ref(false),
|
||||
errorMessage: ref(undefined),
|
||||
})
|
||||
|
||||
useOverlayMock.mockReturnValue({ open, abort: vi.fn() })
|
||||
})
|
||||
|
||||
function mountEditBackupRepository(br: FrontXoBackupRepository = createBr()) {
|
||||
@@ -63,7 +56,7 @@ function mountEditBackupRepository(br: FrontXoBackupRepository = createBr()) {
|
||||
}
|
||||
|
||||
function getOverlayOptions(): OverlayOptions {
|
||||
return useOverlayMock.mock.calls[0][0]
|
||||
return overlayMock.getOverlay<OverlayOptions>()
|
||||
}
|
||||
|
||||
function getJobArgs() {
|
||||
@@ -108,7 +101,7 @@ describe('editBackupRepository', () => {
|
||||
|
||||
mountEditBackupRepository(br).editBackupRepository()
|
||||
|
||||
expect(open).toHaveBeenCalledWith({ props: expect.objectContaining({ br }) })
|
||||
expect(overlayMock.getOverlay().open).toHaveBeenCalledWith({ props: expect.objectContaining({ br }) })
|
||||
})
|
||||
|
||||
it('closes the drawer when it is cancelled', () => {
|
||||
|
||||
@@ -3,9 +3,9 @@ import type {
|
||||
NewBackupRepositoryPayload,
|
||||
useXoBackupRepositoryCreateJob,
|
||||
} from '@/modules/backup-repository/jobs/xo-backup-repository-create.job.ts'
|
||||
import { overlayMock } from '@/test/mock-overlay.ts'
|
||||
import { mountComposable } from '@/test/mount-composable.ts'
|
||||
import { silenceConsoleError } from '@/test/silence-console-error.ts'
|
||||
import type { useOverlay } from '@core/packages/overlay/use-overlay.ts'
|
||||
import { KEEP_OVERLAY_OPEN } from '@core/packages/overlay/symbols.ts'
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
@@ -16,20 +16,16 @@ type OverlayOptions = {
|
||||
}
|
||||
}
|
||||
|
||||
const { useCreateJob, run, useOverlayMock, open } = vi.hoisted(() => ({
|
||||
const { useCreateJob, run } = vi.hoisted(() => ({
|
||||
useCreateJob: vi.fn(),
|
||||
run: vi.fn(),
|
||||
useOverlayMock: vi.fn(),
|
||||
open: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock(import('@/modules/backup-repository/jobs/xo-backup-repository-create.job.ts'), () => ({
|
||||
useXoBackupRepositoryCreateJob: useCreateJob as unknown as typeof useXoBackupRepositoryCreateJob,
|
||||
}))
|
||||
|
||||
vi.mock(import('@core/packages/overlay/use-overlay.ts'), () => ({
|
||||
useOverlay: useOverlayMock as unknown as typeof useOverlay,
|
||||
}))
|
||||
vi.mock(import('@core/packages/overlay/use-overlay.ts'), () => import('@/test/mock-overlay.ts'))
|
||||
|
||||
const PAYLOAD: NewBackupRepositoryPayload = {
|
||||
name: 'Nightly backups',
|
||||
@@ -39,13 +35,11 @@ const PAYLOAD: NewBackupRepositoryPayload = {
|
||||
beforeEach(() => {
|
||||
useCreateJob.mockReset()
|
||||
run.mockReset()
|
||||
useOverlayMock.mockReset()
|
||||
open.mockReset()
|
||||
overlayMock.reset()
|
||||
|
||||
silenceConsoleError()
|
||||
|
||||
useCreateJob.mockReturnValue({ run })
|
||||
useOverlayMock.mockReturnValue({ open, abort: vi.fn() })
|
||||
})
|
||||
|
||||
function mountNewBackupRepository() {
|
||||
@@ -53,7 +47,7 @@ function mountNewBackupRepository() {
|
||||
}
|
||||
|
||||
function getOverlayOptions(): OverlayOptions {
|
||||
return useOverlayMock.mock.calls[0][0]
|
||||
return overlayMock.getOverlay<OverlayOptions>()
|
||||
}
|
||||
|
||||
function getJobPayload() {
|
||||
@@ -69,7 +63,7 @@ it('has no payload before the drawer is confirmed', () => {
|
||||
it('opens the drawer', () => {
|
||||
mountNewBackupRepository().openNewBackupRepositoryDrawer()
|
||||
|
||||
expect(open).toHaveBeenCalledOnce()
|
||||
expect(overlayMock.getOverlay().open).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('closes the drawer when it is cancelled', () => {
|
||||
|
||||
@@ -119,7 +119,8 @@ export function useBulkEditBackupRepositoriesForm(brs: FrontXoBackupRepository[]
|
||||
return undefined
|
||||
}
|
||||
|
||||
const detailsChanges = detailsForm === undefined ? undefined : omitMixedValues(detailsForm.formData, t('mixed-value'))
|
||||
const detailsChanges =
|
||||
detailsForm === undefined ? undefined : omitMixedValues(detailsForm.formData, t('mixed-value'))
|
||||
|
||||
return new Map(initialBrs.map(initialBr => [initialBr.br.id, buildPayload(initialBr, detailsChanges)]))
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import PluginActions from '@/modules/plugin/components/actions/PluginActions.vue
|
||||
import PluginHeader from '@/modules/plugin/components/PluginHeader.vue'
|
||||
import type { FrontXoPlugin } from '@/modules/plugin/remote-resources/use-xo-plugin-collection.ts'
|
||||
import { createPlugin, PLUGIN_CONFIGURATION_SCHEMA } from '@/test/create-plugin.ts'
|
||||
import { findAccentedTags } from '@/test/find-accented-labels.ts'
|
||||
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
|
||||
import { t } from '@/test/i18n.ts'
|
||||
import { mount } from '@vue/test-utils'
|
||||
@@ -13,16 +14,6 @@ function mountHeader(plugin: FrontXoPlugin = createPlugin()) {
|
||||
})
|
||||
}
|
||||
|
||||
function findTags(wrapper: ReturnType<typeof mountHeader>) {
|
||||
return wrapper.findAll('.ui-head-bar .ui-tag').map(tag => ({
|
||||
text: tag.text(),
|
||||
accent: tag
|
||||
.classes()
|
||||
.find(className => className.startsWith('accent--'))
|
||||
?.replace('accent--', ''),
|
||||
}))
|
||||
}
|
||||
|
||||
it('shows the name of the plugin in the head bar', () => {
|
||||
const wrapper = mountHeader(createPlugin())
|
||||
|
||||
@@ -46,28 +37,28 @@ it('shows the version and the configuration status of a configured plugin', () =
|
||||
createPlugin({ version: '2.1.0', configurationSchema: PLUGIN_CONFIGURATION_SCHEMA, configured: true })
|
||||
)
|
||||
|
||||
expect(findTags(wrapper)).toEqual([
|
||||
{ text: t('version-number', { version: '2.1.0' }), accent: 'neutral' },
|
||||
{ text: t('status:configured'), accent: 'success' },
|
||||
expect(findAccentedTags(wrapper)).toEqual([
|
||||
{ label: t('version-number', { version: '2.1.0' }), accent: 'neutral' },
|
||||
{ label: t('status:configured'), accent: 'success' },
|
||||
])
|
||||
})
|
||||
|
||||
it('warns about a plugin which is not configured', () => {
|
||||
const wrapper = mountHeader(createPlugin({ configurationSchema: PLUGIN_CONFIGURATION_SCHEMA, configured: false }))
|
||||
|
||||
expect(findTags(wrapper).at(-1)).toEqual({ text: t('status:not-configured'), accent: 'warning' })
|
||||
expect(findAccentedTags(wrapper).at(-1)).toEqual({ label: t('status:not-configured'), accent: 'warning' })
|
||||
})
|
||||
|
||||
it('shows no configuration status for a plugin which is not configurable', () => {
|
||||
const wrapper = mountHeader(createPlugin({ version: '2.1.0', configurationSchema: undefined }))
|
||||
|
||||
expect(findTags(wrapper)).toEqual([{ text: t('version-number', { version: '2.1.0' }), accent: 'neutral' }])
|
||||
expect(findAccentedTags(wrapper)).toEqual([{ label: t('version-number', { version: '2.1.0' }), accent: 'neutral' }])
|
||||
})
|
||||
|
||||
it('shows no version for a plugin without version', () => {
|
||||
const wrapper = mountHeader(createPlugin({ version: undefined, configurationSchema: undefined }))
|
||||
|
||||
expect(findTags(wrapper)).toEqual([])
|
||||
expect(findAccentedTags(wrapper)).toEqual([])
|
||||
})
|
||||
|
||||
it('displays the actions of the plugin', () => {
|
||||
|
||||
@@ -3,9 +3,9 @@ import type { useXoPluginConfigureJob } from '@/modules/plugin/jobs/xo-plugin-co
|
||||
import type { FrontXoPlugin } from '@/modules/plugin/remote-resources/use-xo-plugin-collection.ts'
|
||||
import { createJobResult } from '@/test/create-job-result.ts'
|
||||
import { createPlugin } from '@/test/create-plugin.ts'
|
||||
import { overlayMock } from '@/test/mock-overlay.ts'
|
||||
import { mountComposable } from '@/test/mount-composable.ts'
|
||||
import { silenceConsoleError } from '@/test/silence-console-error.ts'
|
||||
import type { useOverlay } from '@core/packages/overlay/use-overlay.ts'
|
||||
import { KEEP_OVERLAY_OPEN } from '@core/packages/overlay/symbols.ts'
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
@@ -17,30 +17,24 @@ type OverlayOptions = {
|
||||
}
|
||||
}
|
||||
|
||||
const { useJob, run, useOverlayMock, open, drawer } = vi.hoisted(() => ({
|
||||
const { useJob, run, drawer } = vi.hoisted(() => ({
|
||||
useJob: vi.fn(),
|
||||
run: vi.fn(),
|
||||
useOverlayMock: vi.fn(),
|
||||
open: vi.fn(),
|
||||
drawer: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock(import('@core/packages/overlay/use-overlay.ts'), () => ({
|
||||
useOverlay: useOverlayMock as unknown as typeof useOverlay,
|
||||
}))
|
||||
vi.mock(import('@core/packages/overlay/use-overlay.ts'), () => import('@/test/mock-overlay.ts'))
|
||||
|
||||
const PAYLOAD = { to: 'admin@example.org' }
|
||||
|
||||
beforeEach(() => {
|
||||
useJob.mockReset()
|
||||
run.mockReset()
|
||||
useOverlayMock.mockReset()
|
||||
open.mockReset()
|
||||
overlayMock.reset()
|
||||
|
||||
silenceConsoleError()
|
||||
|
||||
useJob.mockReturnValue(createJobResult(run))
|
||||
useOverlayMock.mockReturnValue({ open, abort: vi.fn() })
|
||||
})
|
||||
|
||||
function mountDrawerJob(plugin: FrontXoPlugin = createPlugin()) {
|
||||
@@ -50,7 +44,7 @@ function mountDrawerJob(plugin: FrontXoPlugin = createPlugin()) {
|
||||
}
|
||||
|
||||
function getOverlayOptions(): OverlayOptions {
|
||||
return useOverlayMock.mock.calls[0][0]
|
||||
return overlayMock.getOverlay<OverlayOptions>()
|
||||
}
|
||||
|
||||
function getJobArgs() {
|
||||
@@ -87,7 +81,7 @@ describe('openDrawer', () => {
|
||||
mountDrawerJob(plugin).openDrawer()
|
||||
|
||||
expect(getOverlayOptions().component).toBe(drawer)
|
||||
expect(open).toHaveBeenCalledWith({ props: expect.objectContaining({ plugin }) })
|
||||
expect(overlayMock.getOverlay().open).toHaveBeenCalledWith({ props: expect.objectContaining({ plugin }) })
|
||||
})
|
||||
|
||||
it('closes the drawer when it is cancelled', () => {
|
||||
|
||||
@@ -30,17 +30,19 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useXoServerCollection } from '@/modules/server/remote-resources/use-xo-server-collection.ts'
|
||||
import {
|
||||
type FrontXoServer,
|
||||
useXoServerCollection,
|
||||
} from '@/modules/server/remote-resources/use-xo-server-collection.ts'
|
||||
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
|
||||
import UiAlert from '@core/components/ui/alert/UiAlert.vue'
|
||||
import UiButton from '@core/components/ui/button/UiButton.vue'
|
||||
import UiLink from '@core/components/ui/link/UiLink.vue'
|
||||
import { useUiStore } from '@core/stores/ui.store.ts'
|
||||
import type { XoServer } from '@vates/types'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { serverId } = defineProps<{
|
||||
serverId: XoServer['id']
|
||||
serverId: FrontXoServer['id']
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { FrontXoPool } from '@/modules/pool/remote-resources/use-xo-pool-collection.ts'
|
||||
import type { FrontXoSr } from '@/modules/storage-repository/remote-resources/use-xo-sr-collection.ts'
|
||||
import { SR_SCOPE_TYPE, type SrScope } from '@core/types/storage-repository.type.ts'
|
||||
import type { RouteLocationRaw } from 'vue-router'
|
||||
@@ -6,6 +7,10 @@ export function isSrWritable(sr: FrontXoSr) {
|
||||
return sr.content_type !== 'iso' && sr.size > 0
|
||||
}
|
||||
|
||||
export function isWritableSrOfPool(sr: FrontXoSr, poolId: FrontXoPool['id'] | undefined) {
|
||||
return sr.$pool === poolId && isSrWritable(sr)
|
||||
}
|
||||
|
||||
export function getSrPageLocation(sr: FrontXoSr, scope: SrScope): RouteLocationRaw {
|
||||
return {
|
||||
name: '/sr/[id]',
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { isWritableSrOfPool } from '@/modules/storage-repository/utils/xo-sr.util.ts'
|
||||
import { OTHER_POOL_ID } from '@/test/create-pool.ts'
|
||||
import { createSr } from '@/test/create-sr.ts'
|
||||
|
||||
describe('isWritableSrOfPool', () => {
|
||||
const sr = createSr()
|
||||
|
||||
it('accepts a writable SR of the pool', () => {
|
||||
expect(isWritableSrOfPool(sr, sr.$pool)).toBe(true)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['an ISO SR', createSr({ content_type: 'iso' }), sr.$pool],
|
||||
['an SR without size', createSr({ size: 0 }), sr.$pool],
|
||||
['an SR of another pool', sr, OTHER_POOL_ID],
|
||||
['any SR while the pool is unknown', sr, undefined],
|
||||
])('rejects %s', (_, candidate, poolId) => {
|
||||
expect(isWritableSrOfPool(candidate, poolId)).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -1,10 +1,16 @@
|
||||
import V2vDestinationPoolCard from '@/modules/v2v/components/destination/V2vDestinationPoolCard.vue'
|
||||
import V2vPoolOption from '@/modules/v2v/components/destination/V2vPoolOption.vue'
|
||||
import { createPool } from '@/test/create-pool.ts'
|
||||
import { createSelectBindings } from '@/test/create-select-bindings.ts'
|
||||
import { findCardTitle } from '@/test/find-card-title.ts'
|
||||
import { findSelectStubLabels } from '@/test/find-select-stub-labels.ts'
|
||||
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
|
||||
import { t } from '@/test/i18n.ts'
|
||||
import { mountWithSelect } from '@/test/mount-with-select.ts'
|
||||
import { openSelect } from '@/test/select.ts'
|
||||
import { useFormSelect } from '@core/packages/form-select'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { h } from 'vue'
|
||||
|
||||
const POOL_SELECT_BINDINGS = createSelectBindings('Destination pool')
|
||||
|
||||
@@ -22,3 +28,17 @@ it('shows its title and that a migration goes to one pool', () => {
|
||||
it('lets the user choose the pool', () => {
|
||||
expect(findSelectStubLabels(mountCard())).toEqual([POOL_SELECT_BINDINGS.label])
|
||||
})
|
||||
|
||||
it('shows each pool option with its name and resource counts', async () => {
|
||||
const pool = createPool()
|
||||
const counts = { hosts: 1, srs: 4, networks: 6 }
|
||||
const wrapper = mountWithSelect(() => {
|
||||
const { id } = useFormSelect([pool], { option: { label: 'name_label', properties: () => ({ counts }) } })
|
||||
|
||||
return () => h(V2vDestinationPoolCard, { poolSelectBindings: { ...POOL_SELECT_BINDINGS, id } })
|
||||
})
|
||||
|
||||
await openSelect(wrapper)
|
||||
|
||||
expect(wrapper.getComponent(V2vPoolOption).props()).toEqual({ label: pool.name_label, counts })
|
||||
})
|
||||
|
||||
@@ -9,14 +9,11 @@
|
||||
:network-select-bindings="form.networkSelectBindings"
|
||||
:template-select-bindings="form.templateSelectBindings"
|
||||
/>
|
||||
<div class="actions">
|
||||
<UiButton accent="brand" variant="secondary" size="medium" @click="back()">
|
||||
{{ t('action:back') }}
|
||||
</UiButton>
|
||||
<V2vWizardStepActions @back="back()">
|
||||
<UiButton type="submit" accent="brand" variant="primary" size="medium">
|
||||
{{ existingList !== undefined ? t('action:continue-with-existing-list') : t('action:continue') }}
|
||||
</UiButton>
|
||||
</div>
|
||||
</V2vWizardStepActions>
|
||||
</VtsForm>
|
||||
</template>
|
||||
|
||||
@@ -24,6 +21,7 @@
|
||||
import V2vDestinationDefaultsCard from '@/modules/v2v/components/destination/V2vDestinationDefaultsCard.vue'
|
||||
import V2vDestinationPoolCard from '@/modules/v2v/components/destination/V2vDestinationPoolCard.vue'
|
||||
import V2vExistingListInfo from '@/modules/v2v/components/destination/V2vExistingListInfo.vue'
|
||||
import V2vWizardStepActions from '@/modules/v2v/components/wizard/V2vWizardStepActions.vue'
|
||||
import { useV2vDestinationStep } from '@/modules/v2v/composables/use-v2v-destination-step.composable.ts'
|
||||
import VtsForm from '@core/components/form/VtsForm.vue'
|
||||
import UiButton from '@core/components/ui/button/UiButton.vue'
|
||||
@@ -39,11 +37,5 @@ const { form, existingList, submit, back } = useV2vDestinationStep()
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.6rem;
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import V2vPoolOption from '@/modules/v2v/components/destination/V2vPoolOption.vue'
|
||||
import type { V2vPoolResourceCounts } from '@/modules/v2v/utils/v2v-destination.util.ts'
|
||||
import { findParts } from '@/test/find-parts.ts'
|
||||
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
|
||||
import { t } from '@/test/i18n.ts'
|
||||
import { mount } from '@vue/test-utils'
|
||||
|
||||
function mountPoolOption(counts?: V2vPoolResourceCounts) {
|
||||
return mount(V2vPoolOption, { props: { label: 'diskor', counts }, global: createGlobalTestConfig() })
|
||||
}
|
||||
|
||||
it('shows the pool name with its host, SR and network counts', () => {
|
||||
const wrapper = mount(V2vPoolOption, {
|
||||
props: { label: 'diskor', counts: { hosts: 1, srs: 4, networks: 6 } },
|
||||
global: createGlobalTestConfig(),
|
||||
})
|
||||
const wrapper = mountPoolOption({ hosts: 1, srs: 4, networks: 6 })
|
||||
|
||||
expect(wrapper.get('.label').text()).toBe('diskor')
|
||||
expect(findParts(wrapper, '.counts')).toEqual([
|
||||
@@ -19,7 +21,7 @@ it('shows the pool name with its host, SR and network counts', () => {
|
||||
})
|
||||
|
||||
it('shows only the pool name while its counts are unknown', () => {
|
||||
const wrapper = mount(V2vPoolOption, { props: { label: 'diskor' }, global: createGlobalTestConfig() })
|
||||
const wrapper = mountPoolOption()
|
||||
|
||||
expect(wrapper.find('.counts').exists()).toBe(false)
|
||||
})
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
<template>
|
||||
<span class="v2v-pool-option">
|
||||
<span class="label">{{ label }}</span>
|
||||
<span v-if="counts !== undefined" class="typo-body-regular-small counts interpunct-list">
|
||||
<InlineParts v-if="counts !== undefined" class="typo-body-regular-small counts">
|
||||
<span>{{ t('n-hosts', { n: counts.hosts }) }}</span>
|
||||
<span>{{ t('n-srs', { n: counts.srs }) }}</span>
|
||||
<span>{{ t('n-networks', { n: counts.networks }) }}</span>
|
||||
</span>
|
||||
</InlineParts>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { V2vPoolResourceCounts } from '@/modules/v2v/utils/v2v-destination.util.ts'
|
||||
import InlineParts from '@/shared/components/InlineParts.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
defineProps<{
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
import V2vVmSettingsDrawer from '@/modules/v2v/components/drawer/V2vVmSettingsDrawer.vue'
|
||||
import V2vDiskMappingTable from '@/modules/v2v/components/mapping/V2vDiskMappingTable.vue'
|
||||
import V2vNicMappingTable from '@/modules/v2v/components/mapping/V2vNicMappingTable.vue'
|
||||
import V2vVmOptionsFields from '@/modules/v2v/components/mapping/V2vVmOptionsFields.vue'
|
||||
import type { V2vMigrationListVm } from '@/modules/v2v/types/v2v-migration-list.type.ts'
|
||||
import { cancelDrawer, findDrawerButtonLabels, submitDrawer } from '@/test/drawer.ts'
|
||||
import { findTitles } from '@/test/find-titles.ts'
|
||||
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
|
||||
import { t } from '@/test/i18n.ts'
|
||||
import { xoCollectionsMock } from '@/test/mock-xo-collections.ts'
|
||||
import {
|
||||
mountV2vVmMappingComponent,
|
||||
V2V_FULLY_OVERRIDDEN_LIST_VM,
|
||||
V2V_LIST_VM_OVERRIDES,
|
||||
} from '@/test/mount-v2v-vm-mapping.ts'
|
||||
import {
|
||||
V2V_DATA_DISK,
|
||||
V2V_DEBIAN_TEMPLATE,
|
||||
V2V_DISK_OVERRIDES,
|
||||
V2V_DISK_ROWS,
|
||||
V2V_LAN_NIC,
|
||||
V2V_MAPPED_VM,
|
||||
V2V_NIC_OVERRIDES,
|
||||
V2V_NIC_ROWS,
|
||||
V2V_POOL_OFFERS,
|
||||
V2V_SSD_SR,
|
||||
V2V_SYSTEM_DISK,
|
||||
V2V_TARGET_OPTIONS,
|
||||
V2V_VLAN_NETWORK,
|
||||
createV2vOfferedTarget,
|
||||
} from '@/test/v2v-mapping-fixtures.ts'
|
||||
import VtsOverlayConfirmButton from '@core/components/overlay/VtsOverlayConfirmButton.vue'
|
||||
import UiDrawer from '@core/components/ui/drawer/UiDrawer.vue'
|
||||
import type { XoV2vVm } from '@vates/types'
|
||||
import { mount, type VueWrapper } from '@vue/test-utils'
|
||||
|
||||
vi.mock(
|
||||
import('@/modules/pool/remote-resources/use-xo-pool-collection.ts'),
|
||||
() => import('@/test/mock-xo-collections.ts')
|
||||
)
|
||||
vi.mock(
|
||||
import('@/modules/host/remote-resources/use-xo-host-collection.ts'),
|
||||
() => import('@/test/mock-xo-collections.ts')
|
||||
)
|
||||
vi.mock(
|
||||
import('@/modules/storage-repository/remote-resources/use-xo-sr-collection.ts'),
|
||||
() => import('@/test/mock-xo-collections.ts')
|
||||
)
|
||||
vi.mock(
|
||||
import('@/modules/network/remote-resources/use-xo-network-collection.ts'),
|
||||
() => import('@/test/mock-xo-collections.ts')
|
||||
)
|
||||
vi.mock(
|
||||
import('@/modules/pbd/remote-resources/use-xo-pbd-collection.ts'),
|
||||
() => import('@/test/mock-xo-collections.ts')
|
||||
)
|
||||
vi.mock(
|
||||
import('@/modules/vm/remote-resources/use-xo-vm-template-collection.ts'),
|
||||
() => import('@/test/mock-xo-collections.ts')
|
||||
)
|
||||
|
||||
// The tables and the fields render what the drawer hands them, each covered by its own test: here only the wiring
|
||||
// matters
|
||||
function mountDrawer({ vm = V2V_MAPPED_VM, listVm }: { vm?: XoV2vVm; listVm?: V2vMigrationListVm } = {}) {
|
||||
return mountV2vVmMappingComponent(
|
||||
(list, router, provide) =>
|
||||
mount(V2vVmSettingsDrawer, {
|
||||
props: { vm, list },
|
||||
global: createGlobalTestConfig({
|
||||
router,
|
||||
provide,
|
||||
stubs: { V2vDiskMappingTable: true, V2vNicMappingTable: true, V2vVmOptionsFields: true },
|
||||
}),
|
||||
attachTo: document.body,
|
||||
}),
|
||||
{ listVm }
|
||||
)
|
||||
}
|
||||
|
||||
function getOptionsFields(wrapper: VueWrapper) {
|
||||
return wrapper.getComponent(V2vVmOptionsFields)
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
xoCollectionsMock.reset()
|
||||
})
|
||||
|
||||
it('names the VM in its title', async () => {
|
||||
const wrapper = await mountDrawer()
|
||||
|
||||
expect(wrapper.text()).toContain(t('action:edit-vm-settings', { name: V2V_MAPPED_VM.name }))
|
||||
})
|
||||
|
||||
it('maps the disks, then the network adapters, then sets the options', async () => {
|
||||
const wrapper = await mountDrawer()
|
||||
|
||||
expect(findTitles(wrapper)).toEqual([t('disks'), t('network-adapters'), t('options')])
|
||||
})
|
||||
|
||||
it.each([
|
||||
['disks', { ...V2V_MAPPED_VM, disks: [] }, [t('network-adapters'), t('options')]],
|
||||
['network adapters', { ...V2V_MAPPED_VM, nics: [] }, [t('disks'), t('options')]],
|
||||
])('leaves out the %s of a VM without any', async (_, vm, titles) => {
|
||||
expect(findTitles(await mountDrawer({ vm }))).toEqual(titles)
|
||||
})
|
||||
|
||||
it('maps the disks and the NICs of the VM on the targets of the pool of the list', async () => {
|
||||
const wrapper = await mountDrawer()
|
||||
const diskTable = wrapper.getComponent(V2vDiskMappingTable)
|
||||
|
||||
expect(diskTable.props()).toMatchObject({ rows: V2V_DISK_ROWS, loading: false })
|
||||
expect(diskTable.props('srOptions').map(option => option.id)).toEqual(V2V_POOL_OFFERS.srs.map(sr => sr.id))
|
||||
expect(wrapper.getComponent(V2vNicMappingTable).props()).toEqual({
|
||||
rows: V2V_NIC_ROWS,
|
||||
networkOptions: V2V_TARGET_OPTIONS.networks,
|
||||
loading: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('shows the options of the VM', async () => {
|
||||
const wrapper = await mountDrawer({ listVm: V2V_FULLY_OVERRIDDEN_LIST_VM })
|
||||
|
||||
expect(getOptionsFields(wrapper).props()).toEqual({
|
||||
keepMac: false,
|
||||
template: createV2vOfferedTarget(V2V_DEBIAN_TEMPLATE.id, 'overridden'),
|
||||
templateOptions: V2V_TARGET_OPTIONS.templates,
|
||||
tags: ['web'],
|
||||
loading: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('offers to cancel or to save', async () => {
|
||||
const wrapper = await mountDrawer()
|
||||
|
||||
expect(findDrawerButtonLabels(wrapper)).toEqual([t('cancel'), t('action:save')])
|
||||
})
|
||||
|
||||
it('shows the targets loading and cannot be saved while the pool offers load', async () => {
|
||||
xoCollectionsMock.areSrsReady.value = false
|
||||
|
||||
const wrapper = await mountDrawer()
|
||||
|
||||
expect([
|
||||
wrapper.getComponent(V2vDiskMappingTable).props('loading'),
|
||||
wrapper.getComponent(V2vNicMappingTable).props('loading'),
|
||||
getOptionsFields(wrapper).props('loading'),
|
||||
wrapper.getComponent(VtsOverlayConfirmButton).props('disabled'),
|
||||
]).toEqual([true, true, true, true])
|
||||
})
|
||||
|
||||
it('can be saved once the pool offers are loaded', async () => {
|
||||
const wrapper = await mountDrawer()
|
||||
|
||||
expect(wrapper.getComponent(VtsOverlayConfirmButton).props('disabled')).toBe(false)
|
||||
})
|
||||
|
||||
it.each([
|
||||
[
|
||||
'the SR of a disk',
|
||||
(wrapper: VueWrapper) =>
|
||||
wrapper.getComponent(V2vDiskMappingTable).vm.$emit('changeSr', V2V_SYSTEM_DISK.key, V2V_SSD_SR.id),
|
||||
{ ...V2V_LIST_VM_OVERRIDES, disks: { ...V2V_DISK_OVERRIDES, [V2V_SYSTEM_DISK.key]: V2V_SSD_SR.id } },
|
||||
],
|
||||
[
|
||||
'the network of a NIC',
|
||||
(wrapper: VueWrapper) =>
|
||||
wrapper.getComponent(V2vNicMappingTable).vm.$emit('changeNetwork', V2V_LAN_NIC.key, V2V_VLAN_NETWORK.id),
|
||||
{ ...V2V_LIST_VM_OVERRIDES, nics: { ...V2V_NIC_OVERRIDES, [V2V_LAN_NIC.key]: V2V_VLAN_NETWORK.id } },
|
||||
],
|
||||
[
|
||||
'the MAC addresses kept',
|
||||
(wrapper: VueWrapper) => getOptionsFields(wrapper).vm.$emit('changeKeepMac', false),
|
||||
{ ...V2V_LIST_VM_OVERRIDES, keepMac: false },
|
||||
],
|
||||
[
|
||||
'the template',
|
||||
(wrapper: VueWrapper) => getOptionsFields(wrapper).vm.$emit('changeTemplate', V2V_DEBIAN_TEMPLATE.id),
|
||||
{ ...V2V_LIST_VM_OVERRIDES, template: V2V_DEBIAN_TEMPLATE.id },
|
||||
],
|
||||
[
|
||||
'the tags',
|
||||
(wrapper: VueWrapper) => getOptionsFields(wrapper).vm.$emit('changeTags', ['prod']),
|
||||
{ ...V2V_LIST_VM_OVERRIDES, tags: ['prod'] },
|
||||
],
|
||||
[
|
||||
'the SR of a disk reset',
|
||||
(wrapper: VueWrapper) =>
|
||||
wrapper.getComponent(V2vDiskMappingTable).vm.$emit('changeSr', V2V_DATA_DISK.key, undefined),
|
||||
{ nics: V2V_NIC_OVERRIDES },
|
||||
],
|
||||
])('confirms the overrides of the VM once saved after a change of %s', async (_, change, overrides) => {
|
||||
const wrapper = await mountDrawer()
|
||||
|
||||
change(wrapper)
|
||||
await submitDrawer(wrapper)
|
||||
|
||||
expect(wrapper.emitted('confirm')).toStrictEqual([[overrides]])
|
||||
})
|
||||
|
||||
it.each([
|
||||
['from the cancel button', cancelDrawer],
|
||||
['once dismissed', (wrapper: VueWrapper) => wrapper.getComponent(UiDrawer).vm.$emit('dismiss')],
|
||||
])('cancels %s', async (_, cancel) => {
|
||||
const wrapper = await mountDrawer()
|
||||
|
||||
await cancel(wrapper)
|
||||
|
||||
expect(wrapper.emitted('cancel')).toHaveLength(1)
|
||||
expect(wrapper.emitted('confirm')).toBeUndefined()
|
||||
})
|
||||
@@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<UiDrawer class="v2v-vm-settings-drawer" @confirm="emit('confirm', toOverrides())" @dismiss="emit('cancel')">
|
||||
<template #title>
|
||||
{{ t('action:edit-vm-settings', { name: vm.name }) }}
|
||||
</template>
|
||||
|
||||
<template #content>
|
||||
<div class="sections">
|
||||
<section v-if="diskRows.length > 0" class="section">
|
||||
<UiTitle>{{ t('disks') }}</UiTitle>
|
||||
<V2vDiskMappingTable
|
||||
:rows="diskRows"
|
||||
:sr-options
|
||||
:loading="!areOffersReady"
|
||||
@change-sr="(key, srId) => setDiskSr(key, srId)"
|
||||
/>
|
||||
</section>
|
||||
<section v-if="nicRows.length > 0" class="section">
|
||||
<UiTitle>{{ t('network-adapters') }}</UiTitle>
|
||||
<V2vNicMappingTable
|
||||
:rows="nicRows"
|
||||
:network-options
|
||||
:loading="!areOffersReady"
|
||||
@change-network="(key, networkId) => setNicNetwork(key, networkId)"
|
||||
/>
|
||||
</section>
|
||||
<section class="section">
|
||||
<UiTitle>{{ t('options') }}</UiTitle>
|
||||
<V2vVmOptionsFields
|
||||
:keep-mac
|
||||
:template
|
||||
:template-options
|
||||
:tags
|
||||
:loading="!areOffersReady"
|
||||
@change-keep-mac="value => setKeepMac(value)"
|
||||
@change-template="templateId => setTemplate(templateId)"
|
||||
@change-tags="value => setTags(value)"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #buttons>
|
||||
<VtsOverlayCancelButton @click="emit('cancel')" />
|
||||
<VtsOverlayConfirmButton :disabled="!areOffersReady">
|
||||
{{ t('action:save') }}
|
||||
</VtsOverlayConfirmButton>
|
||||
</template>
|
||||
</UiDrawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import V2vDiskMappingTable from '@/modules/v2v/components/mapping/V2vDiskMappingTable.vue'
|
||||
import V2vNicMappingTable from '@/modules/v2v/components/mapping/V2vNicMappingTable.vue'
|
||||
import V2vVmOptionsFields from '@/modules/v2v/components/mapping/V2vVmOptionsFields.vue'
|
||||
import { useV2vVmMappingDraft } from '@/modules/v2v/composables/use-v2v-vm-mapping-draft.composable.ts'
|
||||
import type { V2vVmOverrides } from '@/modules/v2v/composables/use-v2v-vm-mapping.composable.ts'
|
||||
import type { V2vMigrationList } from '@/modules/v2v/types/v2v-migration-list.type.ts'
|
||||
import VtsOverlayCancelButton from '@core/components/overlay/VtsOverlayCancelButton.vue'
|
||||
import VtsOverlayConfirmButton from '@core/components/overlay/VtsOverlayConfirmButton.vue'
|
||||
import UiDrawer from '@core/components/ui/drawer/UiDrawer.vue'
|
||||
import UiTitle from '@core/components/ui/title/UiTitle.vue'
|
||||
import type { XoV2vVm } from '@vates/types'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { vm, list } = defineProps<{
|
||||
vm: XoV2vVm
|
||||
list: V2vMigrationList
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
confirm: [overrides: V2vVmOverrides]
|
||||
cancel: []
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const {
|
||||
diskRows,
|
||||
nicRows,
|
||||
template,
|
||||
srOptions,
|
||||
networkOptions,
|
||||
templateOptions,
|
||||
areOffersReady,
|
||||
keepMac,
|
||||
tags,
|
||||
setDiskSr,
|
||||
setNicNetwork,
|
||||
setTemplate,
|
||||
setKeepMac,
|
||||
setTags,
|
||||
toOverrides,
|
||||
} = useV2vVmMappingDraft(() => vm, { list: () => list })
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.v2v-vm-settings-drawer {
|
||||
.sections {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4.8rem;
|
||||
}
|
||||
|
||||
.section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.6rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,51 @@
|
||||
import V2vActiveMappingCard from '@/modules/v2v/components/mapping/V2vActiveMappingCard.vue'
|
||||
import { DISKOR_POOL_NAME } from '@/test/create-pool.ts'
|
||||
import { findAccentedInfos } from '@/test/find-accented-labels.ts'
|
||||
import { findParts } from '@/test/find-parts.ts'
|
||||
import { findCardLabelledValues } from '@/test/find-rendered-values.ts'
|
||||
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
|
||||
import { t } from '@/test/i18n.ts'
|
||||
import { describeV2vMappingCardLayout } from '@/test/v2v-mapping-card.ts'
|
||||
import { V2V_ACTIVE_DISK_ROWS, V2V_ACTIVE_NIC_ROWS } from '@/test/v2v-mapping-fixtures.ts'
|
||||
import UiPanelCardTitle from '@core/components/ui/panel-card-title/UiPanelCardTitle.vue'
|
||||
import { mount, type VueWrapper } from '@vue/test-utils'
|
||||
|
||||
function mountCard({ diskRows = V2V_ACTIVE_DISK_ROWS, nicRows = V2V_ACTIVE_NIC_ROWS, loading = false } = {}) {
|
||||
return mount(V2vActiveMappingCard, {
|
||||
props: { poolName: DISKOR_POOL_NAME, diskRows, nicRows, loading },
|
||||
global: createGlobalTestConfig(),
|
||||
})
|
||||
}
|
||||
|
||||
describeV2vMappingCardLayout({ mountCard, title: t('active-mapping') })
|
||||
|
||||
// Each device: its source parts as its key, its destination as its value
|
||||
function findDevices(wrapper: VueWrapper) {
|
||||
return wrapper.findAll('.v2v-mapping-card-section .vts-card-row-key-value').map(row => ({
|
||||
sourceParts: findParts(row, '.key .inline-parts'),
|
||||
destination: row.get('.value').text(),
|
||||
}))
|
||||
}
|
||||
|
||||
it('shows its title, locked', () => {
|
||||
const title = mountCard().getComponent(UiPanelCardTitle)
|
||||
|
||||
expect([title.props('label'), title.props('icon')]).toEqual([t('active-mapping'), 'fa:lock'])
|
||||
})
|
||||
|
||||
it('tells the mapping is locked since the first sync', () => {
|
||||
expect(findAccentedInfos(mountCard())).toEqual([{ label: t('v2v:locked-since-first-sync'), accent: 'info' }])
|
||||
})
|
||||
|
||||
it('names the pool the VM was synced to', () => {
|
||||
expect(findCardLabelledValues(mountCard())[t('pool')]).toBe(DISKOR_POOL_NAME)
|
||||
})
|
||||
|
||||
it('shows each source device with its details, then its destination, empty for an unpaired one', () => {
|
||||
expect(findDevices(mountCard())).toEqual(
|
||||
[...V2V_ACTIVE_DISK_ROWS, ...V2V_ACTIVE_NIC_ROWS].map(row => ({
|
||||
sourceParts: row.sourceParts,
|
||||
destination: row.destination ?? '',
|
||||
}))
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<UiPanelCard class="v2v-active-mapping-card">
|
||||
<UiPanelCardTitle size="medium" icon="fa:lock" :label="t('active-mapping')" />
|
||||
<UiInfo accent="info" wrap>{{ t('v2v:locked-since-first-sync') }}</UiInfo>
|
||||
<VtsCardRowKeyValue>
|
||||
<template #key>{{ t('pool') }}</template>
|
||||
<template #value>{{ poolName }}</template>
|
||||
</VtsCardRowKeyValue>
|
||||
<template v-if="loading">
|
||||
<VtsDivider type="stretch" />
|
||||
<VtsStateHero format="card" type="busy" size="extra-small" horizontal />
|
||||
</template>
|
||||
<template v-else>
|
||||
<V2vMappingCardSection
|
||||
v-for="section of sections"
|
||||
:key="section.label"
|
||||
:label="section.label"
|
||||
:rows="section.rows"
|
||||
>
|
||||
<template #key="{ row }">
|
||||
<InlineParts>
|
||||
<span v-for="part of row.sourceParts" :key="part">{{ part }}</span>
|
||||
</InlineParts>
|
||||
</template>
|
||||
<template #value="{ row }">{{ row.destination }}</template>
|
||||
</V2vMappingCardSection>
|
||||
</template>
|
||||
</UiPanelCard>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import V2vMappingCardSection from '@/modules/v2v/components/mapping/V2vMappingCardSection.vue'
|
||||
import type { V2vActiveMappingRow } from '@/modules/v2v/types/v2v-mapping.type.ts'
|
||||
import InlineParts from '@/shared/components/InlineParts.vue'
|
||||
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
|
||||
import VtsDivider from '@core/components/divider/VtsDivider.vue'
|
||||
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
|
||||
import UiInfo from '@core/components/ui/info/UiInfo.vue'
|
||||
import UiPanelCard from '@core/components/ui/panel-card/UiPanelCard.vue'
|
||||
import UiPanelCardTitle from '@core/components/ui/panel-card-title/UiPanelCardTitle.vue'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
// A resumed VM has no drawer: the source details of its devices are shown here, beside their locked destination
|
||||
const { diskRows, nicRows } = defineProps<{
|
||||
poolName: string
|
||||
diskRows: V2vActiveMappingRow[]
|
||||
nicRows: V2vActiveMappingRow[]
|
||||
loading: boolean
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const sections = computed(() => [
|
||||
{ label: t('disks'), rows: diskRows },
|
||||
{ label: t('network-adapters'), rows: nicRows },
|
||||
])
|
||||
</script>
|
||||
@@ -0,0 +1,45 @@
|
||||
import V2vDestinationMappingCard from '@/modules/v2v/components/mapping/V2vDestinationMappingCard.vue'
|
||||
import { DISKOR_POOL_NAME } from '@/test/create-pool.ts'
|
||||
import { findAllGenericComponents } from '@/test/find-generic-components.ts'
|
||||
import { findCardLabelledValues } from '@/test/find-rendered-values.ts'
|
||||
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
|
||||
import { t } from '@/test/i18n.ts'
|
||||
import { describeV2vMappingCardLayout } from '@/test/v2v-mapping-card.ts'
|
||||
import { V2V_DISK_ROWS, V2V_NIC_ROWS, V2V_TARGET_OPTIONS } from '@/test/v2v-mapping-fixtures.ts'
|
||||
import { mount, type VueWrapper } from '@vue/test-utils'
|
||||
|
||||
function mountCard({ diskRows = V2V_DISK_ROWS, nicRows = V2V_NIC_ROWS, loading = false } = {}) {
|
||||
return mount(V2vDestinationMappingCard, {
|
||||
props: {
|
||||
poolName: DISKOR_POOL_NAME,
|
||||
diskRows,
|
||||
nicRows,
|
||||
srOptions: V2V_TARGET_OPTIONS.srs,
|
||||
networkOptions: V2V_TARGET_OPTIONS.networks,
|
||||
loading,
|
||||
},
|
||||
global: createGlobalTestConfig(),
|
||||
})
|
||||
}
|
||||
|
||||
describeV2vMappingCardLayout({ mountCard, title: t('destination') })
|
||||
|
||||
function findTargetValues(wrapper: VueWrapper) {
|
||||
return findAllGenericComponents(wrapper, 'V2vTargetValue').map(value => value.props())
|
||||
}
|
||||
|
||||
it('names the pool the list sets, then each disk and each network adapter', () => {
|
||||
expect(Object.keys(findCardLabelledValues(mountCard()))).toEqual([
|
||||
t('pool'),
|
||||
...V2V_DISK_ROWS.map(row => row.label),
|
||||
...V2V_NIC_ROWS.map(row => row.label),
|
||||
])
|
||||
expect(findCardLabelledValues(mountCard())[t('pool')]).toBe(DISKOR_POOL_NAME)
|
||||
})
|
||||
|
||||
it('maps each disk to its SR, then each network adapter to its network', () => {
|
||||
expect(findTargetValues(mountCard())).toEqual([
|
||||
...V2V_DISK_ROWS.map(row => ({ target: row.sr, options: V2V_TARGET_OPTIONS.srs })),
|
||||
...V2V_NIC_ROWS.map(row => ({ target: row.network, options: V2V_TARGET_OPTIONS.networks })),
|
||||
])
|
||||
})
|
||||
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<UiPanelCard class="v2v-destination-mapping-card">
|
||||
<UiPanelCardTitle size="medium" :label="t('destination')" />
|
||||
<VtsCardRowKeyValue>
|
||||
<template #key>{{ t('pool') }}</template>
|
||||
<template #value>{{ poolName }}</template>
|
||||
</VtsCardRowKeyValue>
|
||||
<template v-if="loading">
|
||||
<VtsDivider type="stretch" />
|
||||
<VtsStateHero format="card" type="busy" size="extra-small" horizontal />
|
||||
</template>
|
||||
<template v-else>
|
||||
<V2vMappingCardSection :label="t('disks')" :rows="diskRows">
|
||||
<template #key="{ row }">{{ row.label }}</template>
|
||||
<template #value="{ row }">
|
||||
<V2vTargetValue :target="row.sr" :options="srOptions" />
|
||||
</template>
|
||||
</V2vMappingCardSection>
|
||||
<V2vMappingCardSection :label="t('network-adapters')" :rows="nicRows">
|
||||
<template #key="{ row }">{{ row.label }}</template>
|
||||
<template #value="{ row }">
|
||||
<V2vTargetValue :target="row.network" :options="networkOptions" />
|
||||
</template>
|
||||
</V2vMappingCardSection>
|
||||
</template>
|
||||
</UiPanelCard>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FrontXoNetwork } from '@/modules/network/remote-resources/use-xo-network-collection.ts'
|
||||
import type { FrontXoSr } from '@/modules/storage-repository/remote-resources/use-xo-sr-collection.ts'
|
||||
import V2vMappingCardSection from '@/modules/v2v/components/mapping/V2vMappingCardSection.vue'
|
||||
import V2vTargetValue from '@/modules/v2v/components/mapping/V2vTargetValue.vue'
|
||||
import type { V2vDiskMappingRow, V2vNicMappingRow, V2vTargetOption } from '@/modules/v2v/types/v2v-mapping.type.ts'
|
||||
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
|
||||
import VtsDivider from '@core/components/divider/VtsDivider.vue'
|
||||
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
|
||||
import UiPanelCard from '@core/components/ui/panel-card/UiPanelCard.vue'
|
||||
import UiPanelCardTitle from '@core/components/ui/panel-card-title/UiPanelCardTitle.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
// The mapping of a VM as the side panel shows it: the settings drawer edits it. Until the pool offers load, a target
|
||||
// could only be named by its id
|
||||
defineProps<{
|
||||
poolName: string
|
||||
diskRows: V2vDiskMappingRow[]
|
||||
nicRows: V2vNicMappingRow[]
|
||||
srOptions: V2vTargetOption<FrontXoSr['id']>[]
|
||||
networkOptions: V2vTargetOption<FrontXoNetwork['id']>[]
|
||||
loading: boolean
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
</script>
|
||||
@@ -0,0 +1,55 @@
|
||||
import V2vDestinationVmCard from '@/modules/v2v/components/mapping/V2vDestinationVmCard.vue'
|
||||
import type { V2vDestinationVm } from '@/modules/v2v/composables/use-v2v-resumed-vm.composable.ts'
|
||||
import { DISKOR_POOL_NAME } from '@/test/create-pool.ts'
|
||||
import { findAccentedInfos } from '@/test/find-accented-labels.ts'
|
||||
import { findPanelCardTitle } from '@/test/find-card-title.ts'
|
||||
import { findParts } from '@/test/find-parts.ts'
|
||||
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
|
||||
import { t } from '@/test/i18n.ts'
|
||||
import { V2V_RESUMED_IMPORT_VM } from '@/test/v2v-mapping-fixtures.ts'
|
||||
import UiLink from '@core/components/ui/link/UiLink.vue'
|
||||
import { objectIcon } from '@core/icons'
|
||||
import { V2V_PARTIAL_IMPORT_TAG } from '@vates/types'
|
||||
import { mount } from '@vue/test-utils'
|
||||
|
||||
const VM: V2vDestinationVm = {
|
||||
id: V2V_RESUMED_IMPORT_VM.id,
|
||||
name: 'app-03',
|
||||
icon: objectIcon('vm', 'halted'),
|
||||
powerLabel: 'Halted',
|
||||
}
|
||||
|
||||
function mountCard() {
|
||||
return mount(V2vDestinationVmCard, {
|
||||
props: { vm: VM, poolName: DISKOR_POOL_NAME },
|
||||
global: createGlobalTestConfig(),
|
||||
})
|
||||
}
|
||||
|
||||
it('shows its title', () => {
|
||||
expect(findPanelCardTitle(mountCard())).toBe(t('destination-vm'))
|
||||
})
|
||||
|
||||
it('links the VM to its page, with the icon of its power state', () => {
|
||||
const link = mountCard().getComponent(UiLink)
|
||||
|
||||
expect([link.text(), link.attributes('href'), link.props('icon')]).toEqual([
|
||||
'app-03',
|
||||
`/vm/${V2V_RESUMED_IMPORT_VM.id}/dashboard`,
|
||||
objectIcon('vm', 'halted'),
|
||||
])
|
||||
})
|
||||
|
||||
it('lays out the pool of the VM, its power state and its blocked start', () => {
|
||||
expect(findParts(mountCard(), '.inline-parts')).toEqual([
|
||||
t('pool-named', { name: DISKOR_POOL_NAME }),
|
||||
'Halted',
|
||||
t('v2v:start-blocked-partial'),
|
||||
])
|
||||
})
|
||||
|
||||
it('tells the VM is excluded from backups until the cutover', () => {
|
||||
expect(findAccentedInfos(mountCard())).toEqual([
|
||||
{ label: t('v2v:no-bak-info', { tag: V2V_PARTIAL_IMPORT_TAG }), accent: 'info' },
|
||||
])
|
||||
})
|
||||
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<UiPanelCard class="v2v-destination-vm-card">
|
||||
<UiPanelCardTitle size="medium" :label="t('destination-vm')" />
|
||||
<div class="content">
|
||||
<div class="vm">
|
||||
<UiLink size="medium" :icon="vm.icon" :to="{ name: '/vm/[id]/dashboard', params: { id: vm.id } }">
|
||||
{{ vm.name }}
|
||||
</UiLink>
|
||||
<InlineParts class="typo-body-regular-small">
|
||||
<span>{{ t('pool-named', { name: poolName }) }}</span>
|
||||
<span>{{ vm.powerLabel }}</span>
|
||||
<span>{{ t('v2v:start-blocked-partial') }}</span>
|
||||
</InlineParts>
|
||||
</div>
|
||||
<UiInfo accent="info" wrap>{{ t('v2v:no-bak-info', { tag: V2V_PARTIAL_IMPORT_TAG }) }}</UiInfo>
|
||||
</div>
|
||||
</UiPanelCard>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { V2vDestinationVm } from '@/modules/v2v/composables/use-v2v-resumed-vm.composable.ts'
|
||||
import InlineParts from '@/shared/components/InlineParts.vue'
|
||||
import UiInfo from '@core/components/ui/info/UiInfo.vue'
|
||||
import UiLink from '@core/components/ui/link/UiLink.vue'
|
||||
import UiPanelCard from '@core/components/ui/panel-card/UiPanelCard.vue'
|
||||
import UiPanelCardTitle from '@core/components/ui/panel-card-title/UiPanelCardTitle.vue'
|
||||
import { V2V_PARTIAL_IMPORT_TAG } from '@vates/types'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
defineProps<{
|
||||
vm: V2vDestinationVm
|
||||
poolName: string
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.v2v-destination-vm-card {
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.6rem;
|
||||
}
|
||||
|
||||
.vm {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,45 @@
|
||||
import V2vDiskMappingTable from '@/modules/v2v/components/mapping/V2vDiskMappingTable.vue'
|
||||
import { findTableColumn, findTableHeaders } from '@/test/find-table-cells.ts'
|
||||
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
|
||||
import { t } from '@/test/i18n.ts'
|
||||
import { V2V_DISK_ROWS, V2V_TARGET_OPTIONS } from '@/test/v2v-mapping-fixtures.ts'
|
||||
import { describeV2vTargetCells } from '@/test/v2v-target-cells.ts'
|
||||
import { mount } from '@vue/test-utils'
|
||||
|
||||
function mountTable() {
|
||||
return mount(V2vDiskMappingTable, {
|
||||
props: { rows: V2V_DISK_ROWS, srOptions: V2V_TARGET_OPTIONS.srs, loading: true },
|
||||
global: createGlobalTestConfig(),
|
||||
})
|
||||
}
|
||||
|
||||
it('shows the disk, its datastore, its size, its target SR and where it comes from, in this order', () => {
|
||||
expect(findTableHeaders(mountTable())).toEqual([
|
||||
t('disk'),
|
||||
t('datastore'),
|
||||
t('size'),
|
||||
t('target-sr'),
|
||||
t('value-from'),
|
||||
])
|
||||
})
|
||||
|
||||
it('names each disk and its datastore', () => {
|
||||
const wrapper = mountTable()
|
||||
|
||||
expect([findTableColumn(wrapper, t('disk')), findTableColumn(wrapper, t('datastore'))]).toEqual([
|
||||
['Hard disk 1', 'Hard disk 2'],
|
||||
['ds-prod-01', 'ds-prod-01'],
|
||||
])
|
||||
})
|
||||
|
||||
it('shows the size of each disk', () => {
|
||||
expect(findTableColumn(mountTable(), t('size'))).toEqual(['80GiB', '80GiB'])
|
||||
})
|
||||
|
||||
describeV2vTargetCells({
|
||||
mountTable,
|
||||
targetHeader: t('target-sr'),
|
||||
changeEvent: 'changeSr',
|
||||
rows: V2V_DISK_ROWS.map(({ key, sr }) => ({ key, target: sr })),
|
||||
options: V2V_TARGET_OPTIONS.srs,
|
||||
})
|
||||
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<VtsTable>
|
||||
<thead>
|
||||
<tr>
|
||||
<HeadCells />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<VtsRow v-for="row of rows" :key="row.key">
|
||||
<BodyCells :item="row" />
|
||||
</VtsRow>
|
||||
</tbody>
|
||||
</VtsTable>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FrontXoSr } from '@/modules/storage-repository/remote-resources/use-xo-sr-collection.ts'
|
||||
import { renderV2vTargetCells } from '@/modules/v2v/components/mapping/v2v-target-cells.ts'
|
||||
import type { V2vDiskMappingRow, V2vTargetOption } from '@/modules/v2v/types/v2v-mapping.type.ts'
|
||||
import { renderSizeCell } from '@/shared/utils/size-cell.util.ts'
|
||||
import VtsRow from '@core/components/table/VtsRow.vue'
|
||||
import VtsTable from '@core/components/table/VtsTable.vue'
|
||||
import { defineColumns } from '@core/packages/table/define-columns.ts'
|
||||
import { useNumberColumn } from '@core/tables/column-definitions/number-column.ts'
|
||||
import { useTextColumn } from '@core/tables/column-definitions/text-column.ts'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { srOptions, loading } = defineProps<{
|
||||
rows: V2vDiskMappingRow[]
|
||||
srOptions: V2vTargetOption<FrontXoSr['id']>[]
|
||||
loading: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
changeSr: [key: V2vDiskMappingRow['key'], srId: FrontXoSr['id'] | undefined]
|
||||
}>()
|
||||
|
||||
const useDiskMappingColumns = defineColumns(() => {
|
||||
const { t } = useI18n()
|
||||
|
||||
return {
|
||||
disk: useTextColumn({ headerLabel: () => t('disk') }),
|
||||
datastore: useTextColumn({ headerLabel: () => t('datastore') }),
|
||||
size: useNumberColumn({ headerLabel: () => t('size') }),
|
||||
target: useTextColumn({ headerLabel: () => t('target-sr') }),
|
||||
origin: useTextColumn({ headerLabel: () => t('value-from') }),
|
||||
}
|
||||
})
|
||||
|
||||
const { HeadCells, BodyCells } = useDiskMappingColumns({
|
||||
body: (row: V2vDiskMappingRow) => ({
|
||||
disk: renderer => renderer(row.label),
|
||||
datastore: renderer => renderer(row.datastore),
|
||||
size: renderer => renderSizeCell(row.size, renderer),
|
||||
...renderV2vTargetCells(() => ({
|
||||
target: row.sr,
|
||||
options: srOptions,
|
||||
loading,
|
||||
onChange: srId => emit('changeSr', row.key, srId),
|
||||
})),
|
||||
}),
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,36 @@
|
||||
import V2vMappingCardSection from '@/modules/v2v/components/mapping/V2vMappingCardSection.vue'
|
||||
import { findPanelCardTitles } from '@/test/find-card-title.ts'
|
||||
import { findCardLabelledValues } from '@/test/find-rendered-values.ts'
|
||||
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
|
||||
import { countDividers } from '@/test/v2v-mapping-card.ts'
|
||||
import { mount } from '@vue/test-utils'
|
||||
|
||||
const ROWS = [
|
||||
{ key: 'disk-1', source: 'Hard disk 1', target: 'sr-a' },
|
||||
{ key: 'disk-2', source: 'Hard disk 2', target: 'sr-b' },
|
||||
]
|
||||
|
||||
function mountSection(rows = ROWS) {
|
||||
return mount(V2vMappingCardSection<(typeof ROWS)[number]>, {
|
||||
props: { label: 'Disks', rows },
|
||||
slots: {
|
||||
key: `<template #key="{ row }">{{ row.source }}</template>`,
|
||||
value: `<template #value="{ row }">{{ row.target }}</template>`,
|
||||
},
|
||||
global: createGlobalTestConfig(),
|
||||
})
|
||||
}
|
||||
|
||||
it('sets itself apart, under its title', () => {
|
||||
const wrapper = mountSection()
|
||||
|
||||
expect([countDividers(wrapper), findPanelCardTitles(wrapper)]).toEqual([1, ['Disks']])
|
||||
})
|
||||
|
||||
it('shows each row as the key and the value it renders', () => {
|
||||
expect(findCardLabelledValues(mountSection())).toEqual({ 'Hard disk 1': 'sr-a', 'Hard disk 2': 'sr-b' })
|
||||
})
|
||||
|
||||
it('shows nothing, not even its divider, without rows', () => {
|
||||
expect(mountSection([]).html()).toBe('<!--v-if-->')
|
||||
})
|
||||
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<template v-if="rows.length > 0">
|
||||
<VtsDivider type="stretch" />
|
||||
<div class="v2v-mapping-card-section">
|
||||
<UiPanelCardTitle size="small" :label />
|
||||
<VtsCardRowKeyValue v-for="row of rows" :key="row.key">
|
||||
<template #key>
|
||||
<slot name="key" :row />
|
||||
</template>
|
||||
<template #value>
|
||||
<slot name="value" :row />
|
||||
</template>
|
||||
</VtsCardRowKeyValue>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script generic="TRow extends { key: string }" lang="ts" setup>
|
||||
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
|
||||
import VtsDivider from '@core/components/divider/VtsDivider.vue'
|
||||
import UiPanelCardTitle from '@core/components/ui/panel-card-title/UiPanelCardTitle.vue'
|
||||
|
||||
// A section of a mapping card: one row per device of a kind, without the section for a VM without any
|
||||
defineProps<{
|
||||
label: string
|
||||
rows: TRow[]
|
||||
}>()
|
||||
|
||||
defineSlots<{
|
||||
key(props: { row: TRow }): any
|
||||
value(props: { row: TRow }): any
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.v2v-mapping-card-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<V2vVmKindActions v-slot="{ mainAction, moreActions }" :vm :update-vm @remove="id => emit('remove', id)">
|
||||
<VtsActionItems :actions="getV2vVmMenuActions(mainAction, moreActions)" />
|
||||
</V2vVmKindActions>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import V2vVmKindActions from '@/modules/v2v/components/panel/V2vVmKindActions.vue'
|
||||
import type { V2vMappingStepVm } from '@/modules/v2v/composables/use-v2v-mapping-step.composable.ts'
|
||||
import type { V2vVmMappingContext } from '@/modules/v2v/composables/use-v2v-vm-mapping.composable.ts'
|
||||
import { getV2vVmMenuActions } from '@/modules/v2v/utils/v2v-vm-actions.util.ts'
|
||||
import VtsActionItems from '@core/components/menu/VtsActionItems.vue'
|
||||
import type { XoV2vVm } from '@vates/types'
|
||||
|
||||
defineProps<{
|
||||
vm: V2vMappingStepVm
|
||||
updateVm: V2vVmMappingContext['updateVm']
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
remove: [id: XoV2vVm['id']]
|
||||
}>()
|
||||
</script>
|
||||
@@ -0,0 +1,203 @@
|
||||
import V2vMappingStep from '@/modules/v2v/components/mapping/V2vMappingStep.vue'
|
||||
import V2vMappingTable from '@/modules/v2v/components/mapping/V2vMappingTable.vue'
|
||||
import V2vVmSidePanel from '@/modules/v2v/components/panel/V2vVmSidePanel.vue'
|
||||
import { findV2vRouteQuery } from '@/test/create-v2v-wizard-router.ts'
|
||||
import { getButton, getUiButton } from '@/test/get-button.ts'
|
||||
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
|
||||
import { t } from '@/test/i18n.ts'
|
||||
import { deleteModalMock } from '@/test/mock-delete-modal.ts'
|
||||
import { xoCollectionsMock } from '@/test/mock-xo-collections.ts'
|
||||
import { v2vVmRowsCollectionsMock } from '@/test/mock-xo-v2v-vm-rows-collections.ts'
|
||||
import { mountOnV2vWizard } from '@/test/mount-on-v2v-wizard.ts'
|
||||
import { mountV2vMappingStep } from '@/test/mount-v2v-mapping-step.ts'
|
||||
import { readV2vStoredListVm } from '@/test/mount-v2v-vm-mapping.ts'
|
||||
import { V2V_GONE_VM_ID } from '@/test/v2v-mapping-step-fixtures.ts'
|
||||
import { readV2vMigrationListVmIds } from '@/test/v2v-migration-list-storage.ts'
|
||||
import { V2V_DB_VM, V2V_WEB_VM } from '@/test/v2v-vms-fixtures.ts'
|
||||
import VtsContentSidePanel from '@core/components/layout/VtsContentSidePanel.vue'
|
||||
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
|
||||
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
||||
|
||||
vi.mock(
|
||||
import('@/modules/vm/remote-resources/use-xo-vm-collection.ts'),
|
||||
() => import('@/test/mock-xo-v2v-vm-rows-collections.ts')
|
||||
)
|
||||
vi.mock(
|
||||
import('@/modules/task/remote-resources/use-xo-task-collection.ts'),
|
||||
() => import('@/test/mock-xo-v2v-vm-rows-collections.ts')
|
||||
)
|
||||
vi.mock(
|
||||
import('@/modules/snapshot/components/remote-resources/use-xo-vm-snapshot-collection.ts'),
|
||||
() => import('@/test/mock-xo-v2v-vm-rows-collections.ts')
|
||||
)
|
||||
vi.mock(
|
||||
import('@/modules/pool/remote-resources/use-xo-pool-collection.ts'),
|
||||
() => import('@/test/mock-xo-collections.ts')
|
||||
)
|
||||
vi.mock(
|
||||
import('@/modules/storage-repository/remote-resources/use-xo-sr-collection.ts'),
|
||||
() => import('@/test/mock-xo-collections.ts')
|
||||
)
|
||||
vi.mock(
|
||||
import('@/modules/network/remote-resources/use-xo-network-collection.ts'),
|
||||
() => import('@/test/mock-xo-collections.ts')
|
||||
)
|
||||
vi.mock(
|
||||
import('@/modules/vm/remote-resources/use-xo-vm-template-collection.ts'),
|
||||
() => import('@/test/mock-xo-collections.ts')
|
||||
)
|
||||
vi.mock(import('@core/composables/modals/use-delete-modal.ts'), () => import('@/test/mock-delete-modal.ts'))
|
||||
|
||||
// The children render what the step hands them, each covered by its own test: here only the wiring matters
|
||||
async function mountStep(vm?: string) {
|
||||
const { mounted: wrapper, router } = await mountV2vMappingStep(
|
||||
setup =>
|
||||
mountOnV2vWizard(
|
||||
(router, provide) =>
|
||||
mount(V2vMappingStep, {
|
||||
global: createGlobalTestConfig({
|
||||
router,
|
||||
provide,
|
||||
stubs: {
|
||||
V2vMappingTable: true,
|
||||
V2vVmSidePanel: true,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
setup
|
||||
),
|
||||
{ vm }
|
||||
)
|
||||
|
||||
return { wrapper, router }
|
||||
}
|
||||
|
||||
function getTable(wrapper: VueWrapper) {
|
||||
return wrapper.getComponent(V2vMappingTable)
|
||||
}
|
||||
|
||||
function getVmPanel(wrapper: VueWrapper) {
|
||||
return wrapper.getComponent(V2vVmSidePanel)
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
v2vVmRowsCollectionsMock.reset()
|
||||
xoCollectionsMock.reset()
|
||||
deleteModalMock.reset()
|
||||
deleteModalMock.answer('onConfirm')
|
||||
})
|
||||
|
||||
describe('table', () => {
|
||||
it('lists the VMs of the list', async () => {
|
||||
const { wrapper } = await mountStep()
|
||||
|
||||
expect(
|
||||
getTable(wrapper)
|
||||
.props('rows')
|
||||
.map(row => row.vm.id)
|
||||
).toEqual([V2V_WEB_VM.id, V2V_DB_VM.id, V2V_GONE_VM_ID])
|
||||
})
|
||||
|
||||
it('selects a VM of the table', async () => {
|
||||
const { wrapper, router } = await mountStep()
|
||||
|
||||
getTable(wrapper).vm.$emit('select', V2V_DB_VM.id)
|
||||
await flushPromises()
|
||||
|
||||
expect(findV2vRouteQuery(router).vm).toBe(V2V_DB_VM.id)
|
||||
})
|
||||
|
||||
it('shows the step busy, without table, while the VMs load', async () => {
|
||||
v2vVmRowsCollectionsMock.areVmsReady.value = false
|
||||
|
||||
const { wrapper } = await mountStep(V2V_WEB_VM.id)
|
||||
|
||||
expect(wrapper.getComponent(VtsStateHero).props('type')).toBe('busy')
|
||||
expect(wrapper.findComponent(V2vMappingTable).exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('VM panel', () => {
|
||||
it('shows the selected VM in its panel', async () => {
|
||||
const { wrapper } = await mountStep(V2V_DB_VM.id)
|
||||
|
||||
expect(getVmPanel(wrapper).props('vm')?.id).toBe(V2V_DB_VM.id)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['without a VM in the route', undefined],
|
||||
['for a VM out of the list', 'vm-unknown'],
|
||||
])('shows the panel without VM %s', async (_, vm) => {
|
||||
const { wrapper } = await mountStep(vm)
|
||||
|
||||
expect(getVmPanel(wrapper).props('vm')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('closes, selecting no VM', async () => {
|
||||
const { wrapper, router } = await mountStep(V2V_DB_VM.id)
|
||||
|
||||
getVmPanel(wrapper).vm.$emit('close')
|
||||
await flushPromises()
|
||||
|
||||
expect(findV2vRouteQuery(router).vm).toBeUndefined()
|
||||
expect(getVmPanel(wrapper).props('vm')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('lays the panel out as the side panel of the step, beside the table', async () => {
|
||||
const layout = (await mountStep()).wrapper.getComponent(VtsContentSidePanel)
|
||||
|
||||
expect(layout.findComponent(V2vVmSidePanel).exists()).toBe(true)
|
||||
expect(layout.findComponent(V2vMappingTable).exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
// The table and the VM panel edit the settings of a VM and remove a VM of the list, the selected one then closed
|
||||
describe('list changes', () => {
|
||||
it.each([
|
||||
['the table', (wrapper: VueWrapper) => getTable(wrapper).props('updateVm')],
|
||||
['the VM panel', (wrapper: VueWrapper) => getVmPanel(wrapper).props('updateVm')],
|
||||
])('stores the settings of a VM in the list from %s', async (_, getUpdateVm) => {
|
||||
const { wrapper } = await mountStep(V2V_DB_VM.id)
|
||||
|
||||
getUpdateVm(wrapper)(V2V_WEB_VM.id, vm => ({ ...vm, keepMac: false }))
|
||||
|
||||
expect(readV2vStoredListVm()).toEqual({ id: V2V_WEB_VM.id, keepMac: false })
|
||||
})
|
||||
|
||||
it.each([
|
||||
['the table', getTable],
|
||||
['the VM panel', getVmPanel],
|
||||
])('removes the selected VM of the list from %s, selecting no VM', async (_, getRemover) => {
|
||||
const { wrapper, router } = await mountStep(V2V_DB_VM.id)
|
||||
|
||||
getRemover(wrapper).vm.$emit('remove', V2V_DB_VM.id)
|
||||
await flushPromises()
|
||||
|
||||
expect(readV2vMigrationListVmIds()).toEqual([V2V_WEB_VM.id, V2V_GONE_VM_ID])
|
||||
expect(findV2vRouteQuery(router).vm).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('navigation', () => {
|
||||
it('goes back to the VMs', async () => {
|
||||
const { wrapper, router } = await mountStep(V2V_WEB_VM.id)
|
||||
|
||||
await getButton(wrapper, t('action:back')).trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(findV2vRouteQuery(router).step).toBe('vms')
|
||||
})
|
||||
|
||||
it('goes on to the review, even with an incomplete mapping', async () => {
|
||||
const { wrapper, router } = await mountStep(V2V_WEB_VM.id)
|
||||
xoCollectionsMock.srs.value = []
|
||||
await flushPromises()
|
||||
const nextButton = getUiButton(wrapper, t('action:next-review'))
|
||||
|
||||
await nextButton.trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(nextButton.props('disabled')).toBeFalsy()
|
||||
expect(findV2vRouteQuery(router).step).toBe('review')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<VtsContentSidePanel>
|
||||
<div class="v2v-mapping-step">
|
||||
<VtsStateHero v-if="isBusy" format="page" type="busy" size="medium" />
|
||||
<UiCard v-else>
|
||||
<V2vMappingTable :rows :update-vm @select="id => select(id)" @remove="id => remove(id)" />
|
||||
</UiCard>
|
||||
<V2vWizardStepActions @back="back()">
|
||||
<UiButton accent="brand" variant="primary" size="medium" @click="next()">
|
||||
{{ t('action:next-review') }}
|
||||
</UiButton>
|
||||
</V2vWizardStepActions>
|
||||
</div>
|
||||
<V2vVmSidePanel :vm="selectedVm" :update-vm @close="close()" @remove="id => remove(id)" />
|
||||
</VtsContentSidePanel>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import V2vMappingTable from '@/modules/v2v/components/mapping/V2vMappingTable.vue'
|
||||
import V2vVmSidePanel from '@/modules/v2v/components/panel/V2vVmSidePanel.vue'
|
||||
import V2vWizardStepActions from '@/modules/v2v/components/wizard/V2vWizardStepActions.vue'
|
||||
import { useV2vMappingStep } from '@/modules/v2v/composables/use-v2v-mapping-step.composable.ts'
|
||||
import VtsContentSidePanel from '@core/components/layout/VtsContentSidePanel.vue'
|
||||
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
|
||||
import UiButton from '@core/components/ui/button/UiButton.vue'
|
||||
import UiCard from '@core/components/ui/card/UiCard.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { isBusy, rows, selectedVm, updateVm, select, close, remove, back, next } = useV2vMappingStep()
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.v2v-mapping-step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.6rem;
|
||||
padding: 1.6rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,159 @@
|
||||
import V2vMappingTable from '@/modules/v2v/components/mapping/V2vMappingTable.vue'
|
||||
import V2vVmKindActions from '@/modules/v2v/components/panel/V2vVmKindActions.vue'
|
||||
import type { V2vMappingStepRow } from '@/modules/v2v/composables/use-v2v-mapping-step.composable.ts'
|
||||
import { getV2vVmMenuActions } from '@/modules/v2v/utils/v2v-vm-actions.util.ts'
|
||||
import {
|
||||
V2V_NEW_STEP_VM,
|
||||
V2V_NOT_FOUND_STEP_VM,
|
||||
createV2vMappingStepRow,
|
||||
createV2vResumedMappingStepVm,
|
||||
} from '@/test/create-v2v-mapping-step-vm.ts'
|
||||
import { findAccentedInfos, findAccentedTags } from '@/test/find-accented-labels.ts'
|
||||
import { findParts } from '@/test/find-parts.ts'
|
||||
import { findTableColumn, findTableColumnCells, findTableHeaders } from '@/test/find-table-cells.ts'
|
||||
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
|
||||
import { t } from '@/test/i18n.ts'
|
||||
import { v2vVmActionsMock } from '@/test/mock-v2v-vm-actions.ts'
|
||||
import { getV2vVmKindActionList } from '@/test/v2v-vm-kind-actions.ts'
|
||||
import VtsActionItems from '@core/components/menu/VtsActionItems.vue'
|
||||
import VtsActionsMenu from '@core/components/menu/VtsActionsMenu.vue'
|
||||
import VtsActionCell from '@core/components/table/cells/VtsActionCell.vue'
|
||||
import VtsRow from '@core/components/table/VtsRow.vue'
|
||||
import VtsTable from '@core/components/table/VtsTable.vue'
|
||||
import UiButtonIcon from '@core/components/ui/button-icon/UiButtonIcon.vue'
|
||||
import type { XoV2vVm } from '@vates/types'
|
||||
import { mount, type VueWrapper } from '@vue/test-utils'
|
||||
|
||||
vi.mock(
|
||||
import('@/modules/v2v/composables/use-v2v-vm-actions.composable.ts'),
|
||||
() => import('@/test/mock-v2v-vm-actions.ts')
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
v2vVmActionsMock.reset()
|
||||
})
|
||||
|
||||
// The synced VM is the mapped one, resumed: its row needs an id of its own
|
||||
const RESUMED_VM = createV2vResumedMappingStepVm({ id: 'vm-resumed' as XoV2vVm['id'] })
|
||||
|
||||
// The item labels are the table input: read back as given
|
||||
const ROWS: V2vMappingStepRow[] = [
|
||||
createV2vMappingStepRow(V2V_NEW_STEP_VM, {
|
||||
kind: { label: 'New', accent: 'neutral' },
|
||||
mappingParts: ['2 disks → sr-a', '1 NIC → net'],
|
||||
check: { label: 'Mapping incomplete', accent: 'danger' },
|
||||
}),
|
||||
createV2vMappingStepRow(RESUMED_VM, {
|
||||
kind: { label: 'Resume', accent: 'info' },
|
||||
mappingParts: ['Delta from last sync', 'mapping locked'],
|
||||
check: { label: 'Ready', accent: 'success' },
|
||||
isSelected: true,
|
||||
}),
|
||||
createV2vMappingStepRow(V2V_NOT_FOUND_STEP_VM, {
|
||||
kind: undefined,
|
||||
mappingParts: [],
|
||||
check: { label: 'Not found in the source', accent: 'danger' },
|
||||
}),
|
||||
]
|
||||
|
||||
const updateVm = vi.fn()
|
||||
|
||||
function mountTable(rows = ROWS) {
|
||||
return mount(V2vMappingTable, { props: { rows, updateVm }, global: createGlobalTestConfig() })
|
||||
}
|
||||
|
||||
function getActionCell(wrapper: VueWrapper, rowIndex: number) {
|
||||
return wrapper.findAllComponents(VtsActionCell)[rowIndex]
|
||||
}
|
||||
|
||||
// The menu of a row renders its items once opened
|
||||
async function openRowMenu(wrapper: VueWrapper, rowIndex: number) {
|
||||
await getActionCell(wrapper, rowIndex).getComponent(VtsActionsMenu).getComponent(UiButtonIcon).trigger('click')
|
||||
|
||||
return wrapper.getComponent(VtsActionItems)
|
||||
}
|
||||
|
||||
it('heads the columns of the VMs of the list', () => {
|
||||
expect(findTableHeaders(mountTable())).toEqual([t('vm'), t('type'), t('mapping'), t('checks'), ''])
|
||||
})
|
||||
|
||||
it('lists the VMs in the given order, by name', () => {
|
||||
expect(findTableColumn(mountTable(), t('vm'))).toEqual([
|
||||
V2V_NEW_STEP_VM.heading.name,
|
||||
RESUMED_VM.heading.name,
|
||||
V2V_NOT_FOUND_STEP_VM.heading.name,
|
||||
])
|
||||
})
|
||||
|
||||
it('tags each VM with its kind, none for a VM not found in the source, and tells its check, accented', () => {
|
||||
const wrapper = mountTable()
|
||||
|
||||
expect(findAccentedTags(wrapper)).toEqual([ROWS[0].item.kind, ROWS[1].item.kind])
|
||||
expect(findAccentedInfos(wrapper)).toEqual(ROWS.map(row => row.item.check))
|
||||
})
|
||||
|
||||
it('sums the mapping of each VM up in parts, none for a VM without', () => {
|
||||
const mappingCells = findTableColumnCells(mountTable(), t('mapping'))
|
||||
|
||||
expect(mappingCells.map(cell => findParts(cell, '.inline-parts'))).toEqual(ROWS.map(row => row.item.mappingParts))
|
||||
})
|
||||
|
||||
it('marks the row of the selected VM alone', () => {
|
||||
expect(
|
||||
mountTable()
|
||||
.findAllComponents(VtsRow)
|
||||
.map(row => row.props('selected'))
|
||||
).toEqual([false, true, false])
|
||||
})
|
||||
|
||||
it('pages the VMs', () => {
|
||||
expect(mountTable().getComponent(VtsTable).props('paginationBindings')?.total).toBe(ROWS.length)
|
||||
})
|
||||
|
||||
it('tells an empty list', () => {
|
||||
expect(mountTable([]).getComponent(VtsTable).props('state')?.message).toBe(t('v2v:no-vm-in-list'))
|
||||
})
|
||||
|
||||
it('emits the selection of the VM of a row from its eye', async () => {
|
||||
const wrapper = mountTable()
|
||||
|
||||
await getActionCell(wrapper, 1).getComponent(UiButtonIcon).trigger('click')
|
||||
|
||||
expect(wrapper.emitted('select')).toEqual([[RESUMED_VM.id]])
|
||||
})
|
||||
|
||||
describe('row menu', () => {
|
||||
it.each([
|
||||
['a new VM', 0, v2vVmActionsMock.useV2vNewVmActions],
|
||||
['a resumed VM', 1, v2vVmActionsMock.useV2vResumedVmActions],
|
||||
] as const)('leads with the settings edit of %s, the other actions set apart', async (_, rowIndex, useActions) => {
|
||||
const wrapper = mountTable()
|
||||
const items = await openRowMenu(wrapper, rowIndex)
|
||||
const { mainAction, moreActions } = getV2vVmKindActionList(useActions)
|
||||
|
||||
expect(items.props('actions')).toEqual(getV2vVmMenuActions(mainAction, moreActions))
|
||||
})
|
||||
|
||||
it('offers the actions of a VM not found in the source, without separator', async () => {
|
||||
const items = await openRowMenu(mountTable(), 2)
|
||||
|
||||
expect(items.props('actions')).toEqual(getV2vVmKindActionList(v2vVmActionsMock.useV2vNotFoundVmActions).moreActions)
|
||||
})
|
||||
|
||||
it('runs the actions of the VM of the row, stored through the list', async () => {
|
||||
const wrapper = mountTable()
|
||||
|
||||
await openRowMenu(wrapper, 0)
|
||||
|
||||
expect(wrapper.getComponent(V2vVmKindActions).props()).toEqual({ vm: V2V_NEW_STEP_VM, updateVm })
|
||||
})
|
||||
|
||||
it('emits the removal of the VM of a row, for the step to confirm it', async () => {
|
||||
const wrapper = mountTable()
|
||||
|
||||
await openRowMenu(wrapper, 2)
|
||||
v2vVmActionsMock.useV2vNotFoundVmActions.mock.lastCall?.[1].remove(V2V_NOT_FOUND_STEP_VM.id)
|
||||
|
||||
expect(wrapper.emitted('remove')).toEqual([[V2V_NOT_FOUND_STEP_VM.id]])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<div class="v2v-mapping-table">
|
||||
<UiTitle>{{ t('selected-vms') }}</UiTitle>
|
||||
<VtsTable :state :pagination-bindings sticky="right">
|
||||
<thead>
|
||||
<tr>
|
||||
<HeadCells />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<VtsRow v-for="row of pageRows" :key="row.vm.id" :selected="row.item.isSelected">
|
||||
<BodyCells :item="row" />
|
||||
</VtsRow>
|
||||
</tbody>
|
||||
</VtsTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import V2vMappingRowMenu from '@/modules/v2v/components/mapping/V2vMappingRowMenu.vue'
|
||||
import type { V2vMappingStepRow } from '@/modules/v2v/composables/use-v2v-mapping-step.composable.ts'
|
||||
import type { V2vVmMappingContext } from '@/modules/v2v/composables/use-v2v-vm-mapping.composable.ts'
|
||||
import { renderInlinePartsCell } from '@/shared/utils/inline-parts-cell.util.ts'
|
||||
import VtsRow from '@core/components/table/VtsRow.vue'
|
||||
import VtsTable from '@core/components/table/VtsTable.vue'
|
||||
import UiTitle from '@core/components/ui/title/UiTitle.vue'
|
||||
import { usePagination } from '@core/composables/pagination.composable.ts'
|
||||
import { useTableState } from '@core/composables/table-state.composable.ts'
|
||||
import { defineColumns } from '@core/packages/table/define-columns.ts'
|
||||
import { useActionColumn } from '@core/tables/column-definitions/action-column.ts'
|
||||
import { useInfoColumn } from '@core/tables/column-definitions/info-column.ts'
|
||||
import { useTagColumn } from '@core/tables/column-definitions/tag-column.ts'
|
||||
import { useTextColumn } from '@core/tables/column-definitions/text-column.ts'
|
||||
import type { XoV2vVm } from '@vates/types'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { rows, updateVm } = defineProps<{
|
||||
rows: V2vMappingStepRow[]
|
||||
updateVm: V2vVmMappingContext['updateVm']
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
select: [id: XoV2vVm['id']]
|
||||
remove: [id: XoV2vVm['id']]
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { pageRecords: pageRows, paginationBindings } = usePagination('v2v-mapping-vms', () => rows)
|
||||
|
||||
const state = useTableState({
|
||||
empty: () => (rows.length === 0 ? t('v2v:no-vm-in-list') : false),
|
||||
})
|
||||
|
||||
const useMappingColumns = defineColumns(() => ({
|
||||
vm: useTextColumn({ headerLabel: () => t('vm') }),
|
||||
kind: useTagColumn({ headerLabel: () => t('type') }),
|
||||
mapping: useTextColumn({ headerLabel: () => t('mapping') }),
|
||||
check: useInfoColumn({ headerLabel: () => t('checks') }),
|
||||
actions: useActionColumn({}),
|
||||
}))
|
||||
|
||||
// The kind of a VM changes once the import VM of a resumed VM is deleted: its row menu reads the actions of its kind
|
||||
const { HeadCells, BodyCells } = useMappingColumns({
|
||||
body: (row: V2vMappingStepRow) => ({
|
||||
vm: renderer => renderer(row.item.name),
|
||||
kind: renderer => renderer(row.item.kind?.label ?? [], row.item.kind?.accent),
|
||||
mapping: () => renderInlinePartsCell(row.item.mappingParts),
|
||||
check: renderer => renderer(row.item.check.label, row.item.check.accent),
|
||||
actions: renderer =>
|
||||
renderer({
|
||||
onClick: () => emit('select', row.vm.id),
|
||||
component: V2vMappingRowMenu,
|
||||
props: { vm: row.vm, updateVm, onRemove: (id: XoV2vVm['id']) => emit('remove', id) },
|
||||
}),
|
||||
}),
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.v2v-mapping-table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2.4rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,50 @@
|
||||
import V2vNextRunCard from '@/modules/v2v/components/mapping/V2vNextRunCard.vue'
|
||||
import type { V2vNextTransfer } from '@/modules/v2v/types/v2v-vm-row.type.ts'
|
||||
import { findAccentedInfos } from '@/test/find-accented-labels.ts'
|
||||
import { findPanelCardTitle } from '@/test/find-card-title.ts'
|
||||
import { findCardLabelledValues } from '@/test/find-rendered-values.ts'
|
||||
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
|
||||
import { t } from '@/test/i18n.ts'
|
||||
import { formatDateTime } from '@core/utils/time.util.ts'
|
||||
import { mount } from '@vue/test-utils'
|
||||
|
||||
const LAST_SYNC_AT = Date.UTC(2026, 9, 2, 14, 40)
|
||||
|
||||
const REMINDER = { label: t('v2v:next-run-reminder'), accent: 'info' }
|
||||
|
||||
// An explicit `lastSyncAt: undefined` stands for a VM without last sync
|
||||
function mountCard(props: { transfer?: V2vNextTransfer; lastSyncAt?: number } = {}) {
|
||||
return mount(V2vNextRunCard, {
|
||||
props: { transfer: 'delta', lastSyncAt: LAST_SYNC_AT, ...props },
|
||||
global: createGlobalTestConfig(),
|
||||
})
|
||||
}
|
||||
|
||||
it('shows its title', () => {
|
||||
expect(findPanelCardTitle(mountCard())).toBe(t('next-run'))
|
||||
})
|
||||
|
||||
it.each([
|
||||
['delta', t('delta-transfer')],
|
||||
['full', t('full-transfer')],
|
||||
] as const)('tells the %s transfer of the next run, and when the VM was last synced', (transfer, label) => {
|
||||
expect(findCardLabelledValues(mountCard({ transfer }))).toEqual({
|
||||
[t('type')]: label,
|
||||
[t('last-sync')]: formatDateTime(LAST_SYNC_AT),
|
||||
})
|
||||
})
|
||||
|
||||
it('leaves the last sync empty for a VM without one', () => {
|
||||
expect(findCardLabelledValues(mountCard({ lastSyncAt: undefined }))[t('last-sync')]).toBe('')
|
||||
})
|
||||
|
||||
it('reminds to sync shortly before the cutover', () => {
|
||||
expect(findAccentedInfos(mountCard())).toEqual([REMINDER])
|
||||
})
|
||||
|
||||
it('warns a full transfer comes from a source snapshot changed since the last sync', () => {
|
||||
expect(findAccentedInfos(mountCard({ transfer: 'full' }))).toEqual([
|
||||
{ label: t('v2v:source-snapshot-changed'), accent: 'warning' },
|
||||
REMINDER,
|
||||
])
|
||||
})
|
||||
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<UiPanelCard class="v2v-next-run-card">
|
||||
<UiPanelCardTitle size="medium" :label="t('next-run')" />
|
||||
<div class="content">
|
||||
<UiInfo v-if="transfer === 'full'" accent="warning" wrap>{{ t('v2v:source-snapshot-changed') }}</UiInfo>
|
||||
<VtsCardRowKeyValue>
|
||||
<template #key>{{ t('type') }}</template>
|
||||
<template #value>{{ nextTransferLabels[transfer] }}</template>
|
||||
</VtsCardRowKeyValue>
|
||||
<VtsCardRowKeyValue>
|
||||
<template #key>{{ t('last-sync') }}</template>
|
||||
<template #value>{{ lastSync }}</template>
|
||||
</VtsCardRowKeyValue>
|
||||
<UiInfo accent="info" wrap>{{ t('v2v:next-run-reminder') }}</UiInfo>
|
||||
</div>
|
||||
</UiPanelCard>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useV2vVmLabels } from '@/modules/v2v/composables/use-v2v-vm-labels.composable.ts'
|
||||
import type { V2vNextTransfer } from '@/modules/v2v/types/v2v-vm-row.type.ts'
|
||||
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
|
||||
import UiInfo from '@core/components/ui/info/UiInfo.vue'
|
||||
import UiPanelCard from '@core/components/ui/panel-card/UiPanelCard.vue'
|
||||
import UiPanelCardTitle from '@core/components/ui/panel-card-title/UiPanelCardTitle.vue'
|
||||
import { formatDateTime } from '@core/utils/time.util.ts'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { lastSyncAt } = defineProps<{
|
||||
transfer: V2vNextTransfer
|
||||
lastSyncAt?: number
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { nextTransferLabels } = useV2vVmLabels()
|
||||
|
||||
const lastSync = computed(() => (lastSyncAt === undefined ? undefined : formatDateTime(lastSyncAt)))
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.v2v-next-run-card {
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,52 @@
|
||||
import V2vNicMappingTable from '@/modules/v2v/components/mapping/V2vNicMappingTable.vue'
|
||||
import { findParts } from '@/test/find-parts.ts'
|
||||
import { findTableColumn, findTableColumnCells, findTableHeaders } from '@/test/find-table-cells.ts'
|
||||
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
|
||||
import { t } from '@/test/i18n.ts'
|
||||
import { V2V_NIC_ROWS, V2V_TARGET_OPTIONS } from '@/test/v2v-mapping-fixtures.ts'
|
||||
import { describeV2vTargetCells } from '@/test/v2v-target-cells.ts'
|
||||
import { mount } from '@vue/test-utils'
|
||||
|
||||
function mountTable() {
|
||||
return mount(V2vNicMappingTable, {
|
||||
props: {
|
||||
rows: V2V_NIC_ROWS,
|
||||
networkOptions: V2V_TARGET_OPTIONS.networks,
|
||||
loading: true,
|
||||
},
|
||||
global: createGlobalTestConfig(),
|
||||
})
|
||||
}
|
||||
|
||||
it('shows the adapter, its port group, its MAC address, its target network and where it comes from, in this order', () => {
|
||||
expect(findTableHeaders(mountTable())).toEqual([
|
||||
t('adapter'),
|
||||
t('port-group'),
|
||||
t('mac-address'),
|
||||
t('target-network'),
|
||||
t('value-from'),
|
||||
])
|
||||
})
|
||||
|
||||
it('names each adapter with its MAC address', () => {
|
||||
const wrapper = mountTable()
|
||||
|
||||
expect([findTableColumn(wrapper, t('adapter')), findTableColumn(wrapper, t('mac-address'))]).toEqual([
|
||||
['Network adapter 1', 'Network adapter 2'],
|
||||
['00:50:56:ab:cd:01', '00:50:56:00:00:02'],
|
||||
])
|
||||
})
|
||||
|
||||
it('lays out the port group of each adapter, empty without one', () => {
|
||||
const cells = findTableColumnCells(mountTable(), t('port-group'))
|
||||
|
||||
expect(cells.map(cell => findParts(cell, '.inline-parts'))).toEqual(V2V_NIC_ROWS.map(row => row.portGroupParts))
|
||||
})
|
||||
|
||||
describeV2vTargetCells({
|
||||
mountTable,
|
||||
targetHeader: t('target-network'),
|
||||
changeEvent: 'changeNetwork',
|
||||
rows: V2V_NIC_ROWS.map(({ key, network }) => ({ key, target: network })),
|
||||
options: V2V_TARGET_OPTIONS.networks,
|
||||
})
|
||||
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<VtsTable>
|
||||
<thead>
|
||||
<tr>
|
||||
<HeadCells />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<VtsRow v-for="row of rows" :key="row.key">
|
||||
<BodyCells :item="row" />
|
||||
</VtsRow>
|
||||
</tbody>
|
||||
</VtsTable>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FrontXoNetwork } from '@/modules/network/remote-resources/use-xo-network-collection.ts'
|
||||
import { renderV2vTargetCells } from '@/modules/v2v/components/mapping/v2v-target-cells.ts'
|
||||
import type { V2vNicMappingRow, V2vTargetOption } from '@/modules/v2v/types/v2v-mapping.type.ts'
|
||||
import { renderInlinePartsCell } from '@/shared/utils/inline-parts-cell.util.ts'
|
||||
import VtsRow from '@core/components/table/VtsRow.vue'
|
||||
import VtsTable from '@core/components/table/VtsTable.vue'
|
||||
import { defineColumns } from '@core/packages/table/define-columns.ts'
|
||||
import { useTextColumn } from '@core/tables/column-definitions/text-column.ts'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { networkOptions, loading } = defineProps<{
|
||||
rows: V2vNicMappingRow[]
|
||||
networkOptions: V2vTargetOption<FrontXoNetwork['id']>[]
|
||||
loading: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
changeNetwork: [key: V2vNicMappingRow['key'], networkId: FrontXoNetwork['id'] | undefined]
|
||||
}>()
|
||||
|
||||
const useNicMappingColumns = defineColumns(() => {
|
||||
const { t } = useI18n()
|
||||
|
||||
return {
|
||||
adapter: useTextColumn({ headerLabel: () => t('adapter') }),
|
||||
portGroup: useTextColumn({ headerLabel: () => t('port-group') }),
|
||||
mac: useTextColumn({ headerLabel: () => t('mac-address') }),
|
||||
target: useTextColumn({ headerLabel: () => t('target-network') }),
|
||||
origin: useTextColumn({ headerLabel: () => t('value-from') }),
|
||||
}
|
||||
})
|
||||
|
||||
const { HeadCells, BodyCells } = useNicMappingColumns({
|
||||
body: (row: V2vNicMappingRow) => ({
|
||||
adapter: renderer => renderer(row.label),
|
||||
portGroup: () => renderInlinePartsCell(row.portGroupParts),
|
||||
mac: renderer => renderer(row.macAddress),
|
||||
...renderV2vTargetCells(() => ({
|
||||
target: row.network,
|
||||
options: networkOptions,
|
||||
loading,
|
||||
onChange: networkId => emit('changeNetwork', row.key, networkId),
|
||||
})),
|
||||
}),
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,17 @@
|
||||
import V2vTargetSelect from '@/modules/v2v/components/mapping/V2vTargetSelect.vue'
|
||||
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
|
||||
import { openSelect, pickSelectOption } from '@/test/select.ts'
|
||||
import { V2V_SSD_SR, V2V_TARGET_OPTIONS } from '@/test/v2v-mapping-fixtures.ts'
|
||||
import { mount } from '@vue/test-utils'
|
||||
|
||||
it('emits the id of the target picked', async () => {
|
||||
const wrapper = mount(V2vTargetSelect, {
|
||||
props: { options: V2V_TARGET_OPTIONS.srs, modelValue: V2V_TARGET_OPTIONS.srs[0].id },
|
||||
global: createGlobalTestConfig(),
|
||||
})
|
||||
|
||||
await openSelect(wrapper)
|
||||
await pickSelectOption(wrapper, V2V_SSD_SR.name_label)
|
||||
|
||||
expect(wrapper.emitted('update:modelValue')).toEqual([[V2V_SSD_SR.id]])
|
||||
})
|
||||
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<VtsSelect :id accent="brand" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup generic="TId extends string">
|
||||
import type { V2vTargetOption } from '@/modules/v2v/types/v2v-mapping.type.ts'
|
||||
import VtsSelect from '@core/components/select/VtsSelect.vue'
|
||||
import { useFormSelect } from '@core/packages/form-select'
|
||||
|
||||
const { options, loading } = defineProps<{
|
||||
options: V2vTargetOption<TId>[]
|
||||
loading?: boolean
|
||||
}>()
|
||||
|
||||
const model = defineModel<TId>()
|
||||
|
||||
const { id } = useFormSelect(() => options, {
|
||||
model,
|
||||
loading: () => loading,
|
||||
searchable: true,
|
||||
option: { value: 'id' },
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,23 @@
|
||||
import V2vTargetValue from '@/modules/v2v/components/mapping/V2vTargetValue.vue'
|
||||
import V2vValueOrigin from '@/modules/v2v/components/mapping/V2vValueOrigin.vue'
|
||||
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
|
||||
import { t } from '@/test/i18n.ts'
|
||||
import { V2V_SSD_SR, V2V_TARGET_OPTIONS, createV2vOfferedTarget } from '@/test/v2v-mapping-fixtures.ts'
|
||||
import { mount } from '@vue/test-utils'
|
||||
|
||||
const TARGET = createV2vOfferedTarget(V2V_SSD_SR.id, 'overridden')
|
||||
|
||||
function mountValue(options = V2V_TARGET_OPTIONS.srs) {
|
||||
return mount(V2vTargetValue, { props: { target: TARGET, options }, global: createGlobalTestConfig() })
|
||||
}
|
||||
|
||||
it('names the target, and where it comes from, without a reset', () => {
|
||||
const wrapper = mountValue()
|
||||
|
||||
expect(wrapper.text()).toBe(`${V2V_SSD_SR.name_label} ${t('overridden')}`)
|
||||
expect(wrapper.getComponent(V2vValueOrigin).props()).toEqual({ origin: 'overridden', readonly: true })
|
||||
})
|
||||
|
||||
it('names a target the pool does not offer, or not yet, by its id', () => {
|
||||
expect(mountValue([]).text()).toBe(`${V2V_SSD_SR.id} ${t('overridden')}`)
|
||||
})
|
||||
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<div class="v2v-target-value">
|
||||
{{ label }}
|
||||
<V2vValueOrigin :origin="target.origin" readonly />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script generic="TId extends string" lang="ts" setup>
|
||||
import V2vValueOrigin from '@/modules/v2v/components/mapping/V2vValueOrigin.vue'
|
||||
import type { V2vResolvedTarget, V2vTargetOption } from '@/modules/v2v/types/v2v-mapping.type.ts'
|
||||
import { getV2vTargetOptionLabel } from '@/modules/v2v/utils/v2v-mapping.util.ts'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const { target, options } = defineProps<{
|
||||
target: V2vResolvedTarget<TId>
|
||||
options: V2vTargetOption<TId>[]
|
||||
}>()
|
||||
|
||||
const label = computed(() => getV2vTargetOptionLabel(options, target.value))
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.v2v-target-value {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,34 @@
|
||||
import V2vValueOrigin from '@/modules/v2v/components/mapping/V2vValueOrigin.vue'
|
||||
import type { V2vTargetOrigin } from '@/modules/v2v/types/v2v-mapping.type.ts'
|
||||
import { findAccentedTags } from '@/test/find-accented-labels.ts'
|
||||
import { findButton, getButton } from '@/test/get-button.ts'
|
||||
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
|
||||
import { t } from '@/test/i18n.ts'
|
||||
import { mount } from '@vue/test-utils'
|
||||
|
||||
function mountOrigin(origin: V2vTargetOrigin, { readonly = false } = {}) {
|
||||
return mount(V2vValueOrigin, { props: { origin, readonly }, global: createGlobalTestConfig() })
|
||||
}
|
||||
|
||||
it.each([
|
||||
['default', 'global-default', 'neutral'],
|
||||
['overridden', 'overridden', 'warning'],
|
||||
] as const)('tags a %s value', (origin, labelKey, accent) => {
|
||||
expect(findAccentedTags(mountOrigin(origin))).toEqual([{ label: t(labelKey), accent }])
|
||||
})
|
||||
|
||||
it('offers no reset of a default value', () => {
|
||||
expect(findButton(mountOrigin('default'), t('action:reset-to-default'))).toBeUndefined()
|
||||
})
|
||||
|
||||
it('resets an overridden value', async () => {
|
||||
const wrapper = mountOrigin('overridden')
|
||||
|
||||
await getButton(wrapper, t('action:reset-to-default')).trigger('click')
|
||||
|
||||
expect(wrapper.emitted('reset')).toEqual([[]])
|
||||
})
|
||||
|
||||
it('offers no reset of a read-only overridden value', () => {
|
||||
expect(findButton(mountOrigin('overridden', { readonly: true }), t('action:reset-to-default'))).toBeUndefined()
|
||||
})
|
||||
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<div class="v2v-value-origin">
|
||||
<UiTag :accent="tag.accent" variant="secondary">{{ tag.label }}</UiTag>
|
||||
<UiButton
|
||||
v-if="origin === 'overridden' && !readonly"
|
||||
accent="brand"
|
||||
variant="tertiary"
|
||||
size="small"
|
||||
@click="emit('reset')"
|
||||
>
|
||||
{{ t('action:reset-to-default') }}
|
||||
</UiButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useV2vVmLabels } from '@/modules/v2v/composables/use-v2v-vm-labels.composable.ts'
|
||||
import type { V2vTargetOrigin } from '@/modules/v2v/types/v2v-mapping.type.ts'
|
||||
import UiButton from '@core/components/ui/button/UiButton.vue'
|
||||
import UiTag from '@core/components/ui/tag/UiTag.vue'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { origin } = defineProps<{
|
||||
origin: V2vTargetOrigin
|
||||
readonly?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
reset: []
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { getOriginTag } = useV2vVmLabels()
|
||||
|
||||
const tag = computed(() => getOriginTag(origin))
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.v2v-value-origin {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,23 @@
|
||||
import V2vVmChecksCard from '@/modules/v2v/components/mapping/V2vVmChecksCard.vue'
|
||||
import { findAccentedInfos } from '@/test/find-accented-labels.ts'
|
||||
import { findPanelCardTitle } from '@/test/find-card-title.ts'
|
||||
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
|
||||
import { t } from '@/test/i18n.ts'
|
||||
import { mount } from '@vue/test-utils'
|
||||
|
||||
const CHECKS = [
|
||||
{ label: 'No unsupported disk', accent: 'success' },
|
||||
{ label: '2 snapshots on the source VM', accent: 'warning' },
|
||||
] as const
|
||||
|
||||
function mountCard() {
|
||||
return mount(V2vVmChecksCard, { props: { checks: [...CHECKS] }, global: createGlobalTestConfig() })
|
||||
}
|
||||
|
||||
it('shows its title', () => {
|
||||
expect(findPanelCardTitle(mountCard())).toBe(t('checks'))
|
||||
})
|
||||
|
||||
it('tells each check with its accent, in order', () => {
|
||||
expect(findAccentedInfos(mountCard())).toEqual(CHECKS)
|
||||
})
|
||||
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<UiPanelCard class="v2v-vm-checks-card">
|
||||
<UiPanelCardTitle size="medium" :label="t('checks')" />
|
||||
<div class="content">
|
||||
<UiInfo v-for="check of checks" :key="check.label" :accent="check.accent" wrap>{{ check.label }}</UiInfo>
|
||||
</div>
|
||||
</UiPanelCard>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { V2vAccentedLabel } from '@/modules/v2v/composables/use-v2v-vm-labels.composable.ts'
|
||||
import UiInfo, { type InfoAccent } from '@core/components/ui/info/UiInfo.vue'
|
||||
import UiPanelCard from '@core/components/ui/panel-card/UiPanelCard.vue'
|
||||
import UiPanelCardTitle from '@core/components/ui/panel-card-title/UiPanelCardTitle.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
defineProps<{
|
||||
checks: V2vAccentedLabel<InfoAccent>[]
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.v2v-vm-checks-card {
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,93 @@
|
||||
import V2vValueOrigin from '@/modules/v2v/components/mapping/V2vValueOrigin.vue'
|
||||
import V2vVmOptionsFields from '@/modules/v2v/components/mapping/V2vVmOptionsFields.vue'
|
||||
import { findAccentedInfos } from '@/test/find-accented-labels.ts'
|
||||
import { getButton } from '@/test/get-button.ts'
|
||||
import { getCheckbox } from '@/test/get-checkbox.ts'
|
||||
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
|
||||
import { t } from '@/test/i18n.ts'
|
||||
import { V2V_DEBIAN_TEMPLATE, V2V_TARGET_OPTIONS, createV2vOfferedTarget } from '@/test/v2v-mapping-fixtures.ts'
|
||||
import { getV2vTargetSelect } from '@/test/v2v-target-cells.ts'
|
||||
import VtsTagsInput from '@core/components/tags-input/VtsTagsInput.vue'
|
||||
import UiCheckbox from '@core/components/ui/checkbox/UiCheckbox.vue'
|
||||
import { V2V_PARTIAL_IMPORT_TAG } from '@vates/types'
|
||||
import { mount } from '@vue/test-utils'
|
||||
|
||||
const TEMPLATE = createV2vOfferedTarget(V2V_DEBIAN_TEMPLATE.id, 'overridden')
|
||||
|
||||
const TAGS = ['migrated-from:vmware', 'web']
|
||||
|
||||
function mountFields(keepMac = true) {
|
||||
return mount(V2vVmOptionsFields, {
|
||||
props: {
|
||||
keepMac,
|
||||
template: TEMPLATE,
|
||||
templateOptions: V2V_TARGET_OPTIONS.templates,
|
||||
tags: TAGS,
|
||||
loading: true,
|
||||
},
|
||||
global: createGlobalTestConfig(),
|
||||
})
|
||||
}
|
||||
|
||||
describe('keep MAC addresses', () => {
|
||||
it('shows whether the MAC addresses are kept', () => {
|
||||
expect(mountFields().getComponent(UiCheckbox).props('modelValue')).toBe(true)
|
||||
})
|
||||
|
||||
it.each([true, false])('emits the choice of %s', async keepMac => {
|
||||
const wrapper = mountFields(!keepMac)
|
||||
|
||||
await getCheckbox(wrapper, t('keep-mac-addresses')).setValue(keepMac)
|
||||
|
||||
expect(wrapper.emitted('changeKeepMac')).toEqual([[keepMac]])
|
||||
})
|
||||
})
|
||||
|
||||
describe('template', () => {
|
||||
it('shows the template among the offered ones, and where it comes from', () => {
|
||||
const wrapper = mountFields()
|
||||
|
||||
expect(getV2vTargetSelect(wrapper).props()).toEqual({
|
||||
modelValue: TEMPLATE.value,
|
||||
options: V2V_TARGET_OPTIONS.templates,
|
||||
loading: true,
|
||||
})
|
||||
expect(wrapper.getComponent(V2vValueOrigin).props('origin')).toBe(TEMPLATE.origin)
|
||||
})
|
||||
|
||||
it('emits the template chosen', () => {
|
||||
const wrapper = mountFields()
|
||||
|
||||
getV2vTargetSelect(wrapper).vm.$emit('update:modelValue', V2V_TARGET_OPTIONS.templates[0].id)
|
||||
|
||||
expect(wrapper.emitted('changeTemplate')).toEqual([[V2V_TARGET_OPTIONS.templates[0].id]])
|
||||
})
|
||||
|
||||
it('emits the reset of the template', async () => {
|
||||
const wrapper = mountFields()
|
||||
|
||||
await getButton(wrapper, t('action:reset-to-default')).trigger('click')
|
||||
|
||||
expect(wrapper.emitted('changeTemplate')).toEqual([[undefined]])
|
||||
})
|
||||
})
|
||||
|
||||
describe('tags', () => {
|
||||
it('edits the tags of the destination VM', () => {
|
||||
expect(mountFields().getComponent(VtsTagsInput).props('modelValue')).toEqual(TAGS)
|
||||
})
|
||||
|
||||
it('emits the tags edited', () => {
|
||||
const wrapper = mountFields()
|
||||
|
||||
wrapper.getComponent(VtsTagsInput).vm.$emit('update:modelValue', ['web'])
|
||||
|
||||
expect(wrapper.emitted('changeTags')).toEqual([[['web']]])
|
||||
})
|
||||
|
||||
it('tells how the tags and the backups go together until the cutover', () => {
|
||||
expect(findAccentedInfos(mountFields())).toEqual([
|
||||
{ label: t('v2v:tags-backup-info', { tag: V2V_PARTIAL_IMPORT_TAG }), accent: 'info' },
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<div class="v2v-vm-options-fields">
|
||||
<UiCheckbox
|
||||
:model-value="keepMac"
|
||||
accent="brand"
|
||||
@update:model-value="value => emit('changeKeepMac', value === true)"
|
||||
>
|
||||
{{ t('keep-mac-addresses') }}
|
||||
</UiCheckbox>
|
||||
<VtsInputWrapper :label="t('template')">
|
||||
<div class="template">
|
||||
<V2vTargetSelect
|
||||
:model-value="template.value"
|
||||
:options="templateOptions"
|
||||
:loading
|
||||
@update:model-value="templateId => emit('changeTemplate', templateId)"
|
||||
/>
|
||||
<V2vValueOrigin :origin="template.origin" @reset="emit('changeTemplate', undefined)" />
|
||||
</div>
|
||||
</VtsInputWrapper>
|
||||
<VtsInputWrapper
|
||||
:label="t('tags')"
|
||||
:message="{ content: t('v2v:tags-backup-info', { tag: V2V_PARTIAL_IMPORT_TAG }), accent: 'info' }"
|
||||
wrap-message
|
||||
>
|
||||
<VtsTagsInput :model-value="tags" accent="brand" @update:model-value="value => emit('changeTags', value)" />
|
||||
</VtsInputWrapper>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import V2vTargetSelect from '@/modules/v2v/components/mapping/V2vTargetSelect.vue'
|
||||
import V2vValueOrigin from '@/modules/v2v/components/mapping/V2vValueOrigin.vue'
|
||||
import type { V2vResolvedTarget, V2vTargetOption } from '@/modules/v2v/types/v2v-mapping.type.ts'
|
||||
import type { FrontXoVmTemplate } from '@/modules/vm/remote-resources/use-xo-vm-template-collection.ts'
|
||||
import VtsInputWrapper from '@core/components/input-wrapper/VtsInputWrapper.vue'
|
||||
import VtsTagsInput from '@core/components/tags-input/VtsTagsInput.vue'
|
||||
import UiCheckbox from '@core/components/ui/checkbox/UiCheckbox.vue'
|
||||
import { V2V_PARTIAL_IMPORT_TAG } from '@vates/types'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
defineProps<{
|
||||
keepMac: boolean
|
||||
template: V2vResolvedTarget<FrontXoVmTemplate['id']>
|
||||
templateOptions: V2vTargetOption<FrontXoVmTemplate['id']>[]
|
||||
tags: string[]
|
||||
loading: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
changeKeepMac: [value: boolean]
|
||||
changeTemplate: [templateId: FrontXoVmTemplate['id'] | undefined]
|
||||
changeTags: [tags: string[]]
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.v2v-vm-options-fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2.4rem;
|
||||
|
||||
.template {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,40 @@
|
||||
import V2vVmSourceCard from '@/modules/v2v/components/mapping/V2vVmSourceCard.vue'
|
||||
import { createV2vVm } from '@/test/create-v2v-inventory.ts'
|
||||
import { findPanelCardTitle } from '@/test/find-card-title.ts'
|
||||
import { findParts } from '@/test/find-parts.ts'
|
||||
import { findCardLabelledValues } from '@/test/find-rendered-values.ts'
|
||||
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
|
||||
import { t } from '@/test/i18n.ts'
|
||||
import { V2V_MAPPED_VM } from '@/test/v2v-mapping-fixtures.ts'
|
||||
import { mount } from '@vue/test-utils'
|
||||
|
||||
const VM = createV2vVm({
|
||||
...V2V_MAPPED_VM,
|
||||
description: 'Internal API backend',
|
||||
memory: 12.5 * 1024 ** 3,
|
||||
snapshotCount: 2,
|
||||
})
|
||||
|
||||
function mountCard() {
|
||||
return mount(V2vVmSourceCard, { props: { vm: VM, hostName: 'esx01' }, global: createGlobalTestConfig() })
|
||||
}
|
||||
|
||||
it('shows its title', () => {
|
||||
expect(findPanelCardTitle(mountCard())).toBe(t('source'))
|
||||
})
|
||||
|
||||
it('shows the facts of the source VM', () => {
|
||||
expect(findCardLabelledValues(mountCard())).toMatchObject({
|
||||
[t('power-state')]: t('power-state-on-host', { power: t('vm:status:running'), host: 'esx01' }),
|
||||
[t('cpu')]: t('n-vcpus', 4),
|
||||
[t('memory')]: '12.5 GiB',
|
||||
[t('boot-firmware')]: 'uefi',
|
||||
[t('secure-boot')]: t('disabled'),
|
||||
[t('snapshots')]: '2',
|
||||
[t('description')]: 'Internal API backend',
|
||||
})
|
||||
})
|
||||
|
||||
it('lays out the count and the total size of the disks', () => {
|
||||
expect(findParts(mountCard(), '.inline-parts')).toEqual(['2', '160 GiB'])
|
||||
})
|
||||
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<UiPanelCard class="v2v-vm-source-card">
|
||||
<UiPanelCardTitle size="medium" :label="t('source')" />
|
||||
<div class="content">
|
||||
<VtsCardRowKeyValue>
|
||||
<template #key>{{ t('power-state') }}</template>
|
||||
<template #value>{{ getPowerLabel(vm.powerState, hostName) }}</template>
|
||||
</VtsCardRowKeyValue>
|
||||
<VtsCardRowKeyValue>
|
||||
<template #key>{{ t('cpu') }}</template>
|
||||
<template #value>{{ t('n-vcpus', vm.nCpus) }}</template>
|
||||
</VtsCardRowKeyValue>
|
||||
<VtsCardRowKeyValue>
|
||||
<template #key>{{ t('memory') }}</template>
|
||||
<template #value>{{ formatSize(vm.memory, 1) }}</template>
|
||||
</VtsCardRowKeyValue>
|
||||
<VtsCardRowKeyValue>
|
||||
<template #key>{{ t('boot-firmware') }}</template>
|
||||
<template #value>{{ vm.firmware }}</template>
|
||||
</VtsCardRowKeyValue>
|
||||
<VtsCardRowKeyValue>
|
||||
<template #key>{{ t('secure-boot') }}</template>
|
||||
<template #value>
|
||||
<VtsStatus :status="vm.secureBoot" />
|
||||
</template>
|
||||
</VtsCardRowKeyValue>
|
||||
<VtsCardRowKeyValue>
|
||||
<template #key>{{ t('disks') }}</template>
|
||||
<template #value>
|
||||
<InlineParts>
|
||||
<span v-for="part of getDisksParts(vm.disks)" :key="part">{{ part }}</span>
|
||||
</InlineParts>
|
||||
</template>
|
||||
</VtsCardRowKeyValue>
|
||||
<VtsCardRowKeyValue>
|
||||
<template #key>{{ t('snapshots') }}</template>
|
||||
<template #value>{{ getSnapshotCountLabel(vm.snapshotCount) }}</template>
|
||||
</VtsCardRowKeyValue>
|
||||
<VtsCardRowKeyValue>
|
||||
<template #key>{{ t('description') }}</template>
|
||||
<template #value>{{ vm.description }}</template>
|
||||
</VtsCardRowKeyValue>
|
||||
</div>
|
||||
</UiPanelCard>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useV2vVmLabels } from '@/modules/v2v/composables/use-v2v-vm-labels.composable.ts'
|
||||
import InlineParts from '@/shared/components/InlineParts.vue'
|
||||
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
|
||||
import VtsStatus from '@core/components/status/VtsStatus.vue'
|
||||
import UiPanelCard from '@core/components/ui/panel-card/UiPanelCard.vue'
|
||||
import UiPanelCardTitle from '@core/components/ui/panel-card-title/UiPanelCardTitle.vue'
|
||||
import { formatSize } from '@core/utils/size.util.ts'
|
||||
import type { XoV2vVm } from '@vates/types'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
defineProps<{
|
||||
vm: XoV2vVm
|
||||
hostName?: string
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { getPowerLabel, getDisksParts, getSnapshotCountLabel } = useV2vVmLabels()
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.v2v-vm-source-card {
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,32 @@
|
||||
import V2vTargetSelect from '@/modules/v2v/components/mapping/V2vTargetSelect.vue'
|
||||
import V2vValueOrigin from '@/modules/v2v/components/mapping/V2vValueOrigin.vue'
|
||||
import type { V2vResolvedTarget, V2vTargetOption } from '@/modules/v2v/types/v2v-mapping.type.ts'
|
||||
import { renderBodyCell } from '@core/tables/helpers/render-body-cell.ts'
|
||||
import { h } from 'vue'
|
||||
|
||||
export type V2vTargetCellsConfig<TId extends string> = {
|
||||
target: V2vResolvedTarget<TId>
|
||||
options: V2vTargetOption<TId>[]
|
||||
loading: boolean
|
||||
// `undefined` resets the target to its default
|
||||
onChange: (id: TId | undefined) => void
|
||||
}
|
||||
|
||||
// The last two cells of a mapping table row: the target, then where its value comes from. The config is a getter,
|
||||
// read at render time: a table line keeps its cells while its row changes
|
||||
export function renderV2vTargetCells<TId extends string>(getConfig: () => V2vTargetCellsConfig<TId>) {
|
||||
return {
|
||||
target: () => {
|
||||
const { target, options, loading, onChange } = getConfig()
|
||||
|
||||
return renderBodyCell(() =>
|
||||
h(V2vTargetSelect<TId>, { modelValue: target.value, options, loading, 'onUpdate:modelValue': onChange })
|
||||
)
|
||||
},
|
||||
origin: () => {
|
||||
const { target, onChange } = getConfig()
|
||||
|
||||
return renderBodyCell(() => h(V2vValueOrigin, { origin: target.origin, onReset: () => onChange(undefined) }))
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import V2vNewVmActions from '@/modules/v2v/components/panel/V2vNewVmActions.vue'
|
||||
import { createV2vNewMappingStepVm } from '@/test/create-v2v-mapping-step-vm.ts'
|
||||
import { v2vVmActionsMock } from '@/test/mock-v2v-vm-actions.ts'
|
||||
import { mountV2vVmKindActions, describeV2vVmKindActions } from '@/test/v2v-vm-kind-actions.ts'
|
||||
|
||||
vi.mock(
|
||||
import('@/modules/v2v/composables/use-v2v-vm-actions.composable.ts'),
|
||||
() => import('@/test/mock-v2v-vm-actions.ts')
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
v2vVmActionsMock.reset()
|
||||
})
|
||||
|
||||
const VM = createV2vNewMappingStepVm()
|
||||
|
||||
function mountActions(updateVm = vi.fn()) {
|
||||
return mountV2vVmKindActions(V2vNewVmActions, { vm: VM, updateVm })
|
||||
}
|
||||
|
||||
it('stores the settings of its VM through the list', () => {
|
||||
const updateVm = vi.fn()
|
||||
|
||||
mountActions(updateVm)
|
||||
|
||||
expect(v2vVmActionsMock.useV2vNewVmActions.mock.lastCall?.[1].updateVm).toBe(updateVm)
|
||||
})
|
||||
|
||||
describeV2vVmKindActions({
|
||||
mountActions: () => mountActions(),
|
||||
useActions: v2vVmActionsMock.useV2vNewVmActions,
|
||||
vm: VM,
|
||||
isRemovable: true,
|
||||
})
|
||||
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<slot :main-action="editAction" :more-actions />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { V2vNewMappingStepVm } from '@/modules/v2v/composables/use-v2v-mapping-step.composable.ts'
|
||||
import { useV2vNewVmActions, type V2vVmActionList } from '@/modules/v2v/composables/use-v2v-vm-actions.composable.ts'
|
||||
import type { V2vVmMappingContext } from '@/modules/v2v/composables/use-v2v-vm-mapping.composable.ts'
|
||||
import type { XoV2vVm } from '@vates/types'
|
||||
|
||||
const { vm, updateVm } = defineProps<{
|
||||
vm: V2vNewMappingStepVm
|
||||
updateVm: V2vVmMappingContext['updateVm']
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
remove: [id: XoV2vVm['id']]
|
||||
}>()
|
||||
|
||||
defineSlots<{
|
||||
default(props: V2vVmActionList): any
|
||||
}>()
|
||||
|
||||
const { editAction, moreActions } = useV2vNewVmActions(() => vm, { updateVm, remove: id => emit('remove', id) })
|
||||
</script>
|
||||
@@ -0,0 +1,122 @@
|
||||
import V2vDestinationMappingCard from '@/modules/v2v/components/mapping/V2vDestinationMappingCard.vue'
|
||||
import V2vVmChecksCard from '@/modules/v2v/components/mapping/V2vVmChecksCard.vue'
|
||||
import V2vVmSourceCard from '@/modules/v2v/components/mapping/V2vVmSourceCard.vue'
|
||||
import V2vNewVmCards from '@/modules/v2v/components/panel/V2vNewVmCards.vue'
|
||||
import V2vVmOptionsCard from '@/modules/v2v/components/panel/V2vVmOptionsCard.vue'
|
||||
import { createV2vHost } from '@/test/create-v2v-inventory.ts'
|
||||
import { createV2vNewMappingStepVm } from '@/test/create-v2v-mapping-step-vm.ts'
|
||||
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
|
||||
import { xoCollectionsMock } from '@/test/mock-xo-collections.ts'
|
||||
import { mountV2vVmMappingComponent } from '@/test/mount-v2v-vm-mapping.ts'
|
||||
import {
|
||||
V2V_DEFAULT_TEMPLATE,
|
||||
V2V_DISK_ROWS,
|
||||
V2V_MIGRATED_VM_TAGS,
|
||||
V2V_NIC_ROWS,
|
||||
V2V_POOL_OFFERS,
|
||||
V2V_TARGET_OPTIONS,
|
||||
createV2vOfferedTarget,
|
||||
} from '@/test/v2v-mapping-fixtures.ts'
|
||||
import { V2V_LIST_POOL } from '@/test/v2v-wizard-fixtures.ts'
|
||||
import { mount } from '@vue/test-utils'
|
||||
|
||||
vi.mock(
|
||||
import('@/modules/pool/remote-resources/use-xo-pool-collection.ts'),
|
||||
() => import('@/test/mock-xo-collections.ts')
|
||||
)
|
||||
vi.mock(
|
||||
import('@/modules/host/remote-resources/use-xo-host-collection.ts'),
|
||||
() => import('@/test/mock-xo-collections.ts')
|
||||
)
|
||||
vi.mock(
|
||||
import('@/modules/storage-repository/remote-resources/use-xo-sr-collection.ts'),
|
||||
() => import('@/test/mock-xo-collections.ts')
|
||||
)
|
||||
vi.mock(
|
||||
import('@/modules/network/remote-resources/use-xo-network-collection.ts'),
|
||||
() => import('@/test/mock-xo-collections.ts')
|
||||
)
|
||||
vi.mock(
|
||||
import('@/modules/pbd/remote-resources/use-xo-pbd-collection.ts'),
|
||||
() => import('@/test/mock-xo-collections.ts')
|
||||
)
|
||||
vi.mock(
|
||||
import('@/modules/vm/remote-resources/use-xo-vm-template-collection.ts'),
|
||||
() => import('@/test/mock-xo-collections.ts')
|
||||
)
|
||||
|
||||
const LIST_ISSUE = { label: 'Import in progress', accent: 'danger' } as const
|
||||
|
||||
// The cards render what the container hands them, each covered by its own test: here only the wiring matters
|
||||
function mountCards() {
|
||||
return mountV2vVmMappingComponent((list, router, provide) => {
|
||||
const vm = createV2vNewMappingStepVm({ list, listIssue: LIST_ISSUE })
|
||||
|
||||
return {
|
||||
vm,
|
||||
wrapper: mount(V2vNewVmCards, {
|
||||
props: { vm },
|
||||
global: createGlobalTestConfig({
|
||||
router,
|
||||
provide,
|
||||
stubs: {
|
||||
V2vVmSourceCard: true,
|
||||
V2vVmChecksCard: true,
|
||||
V2vDestinationMappingCard: true,
|
||||
V2vVmOptionsCard: true,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
xoCollectionsMock.reset()
|
||||
})
|
||||
|
||||
it('shows the source VM on its host', async () => {
|
||||
const { vm, wrapper } = await mountCards()
|
||||
|
||||
expect(wrapper.getComponent(V2vVmSourceCard).props()).toEqual({ vm: vm.vm, hostName: createV2vHost().name })
|
||||
})
|
||||
|
||||
it('tells the checks of the source VM, then its issue in the list', async () => {
|
||||
const { wrapper } = await mountCards()
|
||||
const checks = wrapper.getComponent(V2vVmChecksCard).props('checks')
|
||||
|
||||
expect([checks.length, checks.at(-1)]).toEqual([3, LIST_ISSUE])
|
||||
})
|
||||
|
||||
it('shows the mapping of the disks and the NICs on the pool of the list, read only', async () => {
|
||||
const { wrapper } = await mountCards()
|
||||
const card = wrapper.getComponent(V2vDestinationMappingCard)
|
||||
|
||||
expect(card.props()).toMatchObject({
|
||||
poolName: V2V_LIST_POOL.name_label,
|
||||
diskRows: V2V_DISK_ROWS,
|
||||
nicRows: V2V_NIC_ROWS,
|
||||
networkOptions: V2V_TARGET_OPTIONS.networks,
|
||||
loading: false,
|
||||
})
|
||||
expect(card.props('srOptions').map(option => option.id)).toEqual(V2V_POOL_OFFERS.srs.map(sr => sr.id))
|
||||
})
|
||||
|
||||
it('shows the options of the VM', async () => {
|
||||
const { wrapper } = await mountCards()
|
||||
|
||||
expect(wrapper.getComponent(V2vVmOptionsCard).props()).toEqual({
|
||||
keepMac: true,
|
||||
template: createV2vOfferedTarget(V2V_DEFAULT_TEMPLATE.id),
|
||||
templateOptions: V2V_TARGET_OPTIONS.templates,
|
||||
tags: V2V_MIGRATED_VM_TAGS,
|
||||
})
|
||||
})
|
||||
|
||||
it('shows the targets loading while the pool offers load', async () => {
|
||||
xoCollectionsMock.areTemplatesReady.value = false
|
||||
|
||||
const { wrapper } = await mountCards()
|
||||
|
||||
expect(wrapper.getComponent(V2vDestinationMappingCard).props('loading')).toBe(true)
|
||||
})
|
||||
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<V2vVmSourceCard :vm="vm.vm" :host-name />
|
||||
<V2vVmChecksCard :checks />
|
||||
<V2vDestinationMappingCard :pool-name :disk-rows :nic-rows :sr-options :network-options :loading="!areOffersReady" />
|
||||
<V2vVmOptionsCard :keep-mac :template :template-options :tags />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import V2vDestinationMappingCard from '@/modules/v2v/components/mapping/V2vDestinationMappingCard.vue'
|
||||
import V2vVmChecksCard from '@/modules/v2v/components/mapping/V2vVmChecksCard.vue'
|
||||
import V2vVmSourceCard from '@/modules/v2v/components/mapping/V2vVmSourceCard.vue'
|
||||
import V2vVmOptionsCard from '@/modules/v2v/components/panel/V2vVmOptionsCard.vue'
|
||||
import type { V2vNewMappingStepVm } from '@/modules/v2v/composables/use-v2v-mapping-step.composable.ts'
|
||||
import { useV2vVmMapping } from '@/modules/v2v/composables/use-v2v-vm-mapping.composable.ts'
|
||||
|
||||
const { vm } = defineProps<{
|
||||
vm: V2vNewMappingStepVm
|
||||
}>()
|
||||
|
||||
const {
|
||||
poolName,
|
||||
hostName,
|
||||
checks,
|
||||
diskRows,
|
||||
nicRows,
|
||||
template,
|
||||
srOptions,
|
||||
networkOptions,
|
||||
templateOptions,
|
||||
areOffersReady,
|
||||
keepMac,
|
||||
tags,
|
||||
} = useV2vVmMapping(() => vm.vm, { list: () => vm.list, listIssue: () => vm.listIssue })
|
||||
</script>
|
||||
@@ -0,0 +1,22 @@
|
||||
import V2vNotFoundVmActions from '@/modules/v2v/components/panel/V2vNotFoundVmActions.vue'
|
||||
import { createV2vNotFoundMappingStepVm } from '@/test/create-v2v-mapping-step-vm.ts'
|
||||
import { v2vVmActionsMock } from '@/test/mock-v2v-vm-actions.ts'
|
||||
import { mountV2vVmKindActions, describeV2vVmKindActions } from '@/test/v2v-vm-kind-actions.ts'
|
||||
|
||||
vi.mock(
|
||||
import('@/modules/v2v/composables/use-v2v-vm-actions.composable.ts'),
|
||||
() => import('@/test/mock-v2v-vm-actions.ts')
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
v2vVmActionsMock.reset()
|
||||
})
|
||||
|
||||
const VM = createV2vNotFoundMappingStepVm()
|
||||
|
||||
describeV2vVmKindActions({
|
||||
mountActions: () => mountV2vVmKindActions(V2vNotFoundVmActions, { vm: VM }),
|
||||
useActions: v2vVmActionsMock.useV2vNotFoundVmActions,
|
||||
vm: VM,
|
||||
isRemovable: true,
|
||||
})
|
||||
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<slot :more-actions />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { V2vNotFoundMappingStepVm } from '@/modules/v2v/composables/use-v2v-mapping-step.composable.ts'
|
||||
import {
|
||||
useV2vNotFoundVmActions,
|
||||
type V2vVmActionList,
|
||||
} from '@/modules/v2v/composables/use-v2v-vm-actions.composable.ts'
|
||||
import type { XoV2vVm } from '@vates/types'
|
||||
|
||||
const { vm } = defineProps<{
|
||||
vm: V2vNotFoundMappingStepVm
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
remove: [id: XoV2vVm['id']]
|
||||
}>()
|
||||
|
||||
defineSlots<{
|
||||
default(props: V2vVmActionList): any
|
||||
}>()
|
||||
|
||||
const { moreActions } = useV2vNotFoundVmActions(() => vm, { remove: id => emit('remove', id) })
|
||||
</script>
|
||||
@@ -0,0 +1,22 @@
|
||||
import V2vResumedVmActions from '@/modules/v2v/components/panel/V2vResumedVmActions.vue'
|
||||
import { createV2vResumedMappingStepVm } from '@/test/create-v2v-mapping-step-vm.ts'
|
||||
import { v2vVmActionsMock } from '@/test/mock-v2v-vm-actions.ts'
|
||||
import { mountV2vVmKindActions, describeV2vVmKindActions } from '@/test/v2v-vm-kind-actions.ts'
|
||||
|
||||
vi.mock(
|
||||
import('@/modules/v2v/composables/use-v2v-vm-actions.composable.ts'),
|
||||
() => import('@/test/mock-v2v-vm-actions.ts')
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
v2vVmActionsMock.reset()
|
||||
})
|
||||
|
||||
const VM = createV2vResumedMappingStepVm()
|
||||
|
||||
describeV2vVmKindActions({
|
||||
mountActions: () => mountV2vVmKindActions(V2vResumedVmActions, { vm: VM }),
|
||||
useActions: v2vVmActionsMock.useV2vResumedVmActions,
|
||||
vm: VM,
|
||||
isRemovable: false,
|
||||
})
|
||||
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<slot :main-action="editAction" :more-actions />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { V2vResumedMappingStepVm } from '@/modules/v2v/composables/use-v2v-mapping-step.composable.ts'
|
||||
import {
|
||||
useV2vResumedVmActions,
|
||||
type V2vVmActionList,
|
||||
} from '@/modules/v2v/composables/use-v2v-vm-actions.composable.ts'
|
||||
|
||||
const { vm } = defineProps<{
|
||||
vm: V2vResumedMappingStepVm
|
||||
}>()
|
||||
|
||||
defineSlots<{
|
||||
default(props: V2vVmActionList): any
|
||||
}>()
|
||||
|
||||
const { editAction, moreActions } = useV2vResumedVmActions(() => vm)
|
||||
</script>
|
||||
@@ -0,0 +1,101 @@
|
||||
import V2vActiveMappingCard from '@/modules/v2v/components/mapping/V2vActiveMappingCard.vue'
|
||||
import V2vDestinationVmCard from '@/modules/v2v/components/mapping/V2vDestinationVmCard.vue'
|
||||
import V2vNextRunCard from '@/modules/v2v/components/mapping/V2vNextRunCard.vue'
|
||||
import V2vResumedVmCards from '@/modules/v2v/components/panel/V2vResumedVmCards.vue'
|
||||
import { createV2vResumedMappingStepVm } from '@/test/create-v2v-mapping-step-vm.ts'
|
||||
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
|
||||
import { xoCollectionsMock } from '@/test/mock-xo-collections.ts'
|
||||
import { vmDeviceCollectionsMock } from '@/test/mock-xo-vm-device-collections.ts'
|
||||
import { mountOnV2vWizard } from '@/test/mount-on-v2v-wizard.ts'
|
||||
import { seedV2vResumedVmCollections } from '@/test/seed-v2v-resumed-vm.ts'
|
||||
import {
|
||||
V2V_ACTIVE_DISK_ROWS,
|
||||
V2V_ACTIVE_NIC_ROWS,
|
||||
V2V_MAPPING_INVENTORY,
|
||||
V2V_RESUMED_IMPORT_VM,
|
||||
} from '@/test/v2v-mapping-fixtures.ts'
|
||||
import { V2V_LIST_POOL } from '@/test/v2v-wizard-fixtures.ts'
|
||||
import { mount } from '@vue/test-utils'
|
||||
|
||||
vi.mock(
|
||||
import('@/modules/pool/remote-resources/use-xo-pool-collection.ts'),
|
||||
() => import('@/test/mock-xo-collections.ts')
|
||||
)
|
||||
vi.mock(
|
||||
import('@/modules/storage-repository/remote-resources/use-xo-sr-collection.ts'),
|
||||
() => import('@/test/mock-xo-collections.ts')
|
||||
)
|
||||
vi.mock(
|
||||
import('@/modules/network/remote-resources/use-xo-network-collection.ts'),
|
||||
() => import('@/test/mock-xo-collections.ts')
|
||||
)
|
||||
vi.mock(
|
||||
import('@/modules/vbd/remote-resources/use-xo-vbd-collection.ts'),
|
||||
() => import('@/test/mock-xo-vm-device-collections.ts')
|
||||
)
|
||||
vi.mock(
|
||||
import('@/modules/vdi/remote-resources/use-xo-vdi-collection.ts'),
|
||||
() => import('@/test/mock-xo-vm-device-collections.ts')
|
||||
)
|
||||
vi.mock(
|
||||
import('@/modules/vif/remote-resources/use-xo-vif-collection.ts'),
|
||||
() => import('@/test/mock-xo-vm-device-collections.ts')
|
||||
)
|
||||
|
||||
const VM = createV2vResumedMappingStepVm()
|
||||
|
||||
// The cards render what the container hands them, each covered by its own test: here only the wiring matters
|
||||
async function mountCards() {
|
||||
seedV2vResumedVmCollections()
|
||||
|
||||
const { mounted: wrapper } = await mountOnV2vWizard(
|
||||
(router, provide) =>
|
||||
mount(V2vResumedVmCards, {
|
||||
props: { vm: VM },
|
||||
global: createGlobalTestConfig({
|
||||
router,
|
||||
provide,
|
||||
stubs: { V2vDestinationVmCard: true, V2vNextRunCard: true, V2vActiveMappingCard: true },
|
||||
}),
|
||||
}),
|
||||
{ step: 'mapping', source: { inventory: V2V_MAPPING_INVENTORY } }
|
||||
)
|
||||
|
||||
return wrapper
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
xoCollectionsMock.reset()
|
||||
vmDeviceCollectionsMock.reset()
|
||||
})
|
||||
|
||||
it('shows the import VM on the pool it was synced to', async () => {
|
||||
const card = (await mountCards()).getComponent(V2vDestinationVmCard)
|
||||
|
||||
expect([card.props('vm').id, card.props('poolName')]).toEqual([V2V_RESUMED_IMPORT_VM.id, V2V_LIST_POOL.name_label])
|
||||
})
|
||||
|
||||
it('tells the next run, since the last sync', async () => {
|
||||
const card = (await mountCards()).getComponent(V2vNextRunCard)
|
||||
|
||||
expect(card.props()).toEqual({ transfer: 'delta', lastSyncAt: VM.lastSyncAt })
|
||||
})
|
||||
|
||||
it('shows the mapping of the first sync, locked on the pool', async () => {
|
||||
const card = (await mountCards()).getComponent(V2vActiveMappingCard)
|
||||
|
||||
expect(card.props()).toEqual({
|
||||
poolName: V2V_LIST_POOL.name_label,
|
||||
diskRows: V2V_ACTIVE_DISK_ROWS,
|
||||
nicRows: V2V_ACTIVE_NIC_ROWS,
|
||||
loading: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('shows the active mapping loading while the devices of the import VM load', async () => {
|
||||
vmDeviceCollectionsMock.areVdisReady.value = false
|
||||
|
||||
const card = (await mountCards()).getComponent(V2vActiveMappingCard)
|
||||
|
||||
expect(card.props('loading')).toBe(true)
|
||||
})
|
||||
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<V2vDestinationVmCard :vm="destinationVm" :pool-name />
|
||||
<V2vNextRunCard :transfer="nextTransfer" :last-sync-at="vm.lastSyncAt" />
|
||||
<V2vActiveMappingCard
|
||||
:pool-name
|
||||
:disk-rows="activeDiskRows"
|
||||
:nic-rows="activeNicRows"
|
||||
:loading="!isActiveMappingReady"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import V2vActiveMappingCard from '@/modules/v2v/components/mapping/V2vActiveMappingCard.vue'
|
||||
import V2vDestinationVmCard from '@/modules/v2v/components/mapping/V2vDestinationVmCard.vue'
|
||||
import V2vNextRunCard from '@/modules/v2v/components/mapping/V2vNextRunCard.vue'
|
||||
import type { V2vResumedMappingStepVm } from '@/modules/v2v/composables/use-v2v-mapping-step.composable.ts'
|
||||
import { useV2vResumedVm } from '@/modules/v2v/composables/use-v2v-resumed-vm.composable.ts'
|
||||
|
||||
const { vm } = defineProps<{
|
||||
vm: V2vResumedMappingStepVm
|
||||
}>()
|
||||
|
||||
const { poolName, destinationVm, activeDiskRows, activeNicRows, isActiveMappingReady, nextTransfer } = useV2vResumedVm(
|
||||
() => vm.vm,
|
||||
() => vm.importVm
|
||||
)
|
||||
</script>
|
||||
@@ -0,0 +1,65 @@
|
||||
import V2vVmActions from '@/modules/v2v/components/panel/V2vVmActions.vue'
|
||||
import { createActionItem } from '@/test/create-action-item.ts'
|
||||
import { findTooltipContent } from '@/test/find-tooltip-content.ts'
|
||||
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
|
||||
import type { LeafActionItem } from '@core/components/menu/VtsActionsMenu.vue'
|
||||
import VtsActionsMenu from '@core/components/menu/VtsActionsMenu.vue'
|
||||
import UiButton from '@core/components/ui/button/UiButton.vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
|
||||
const MORE_ACTIONS = [createActionItem({ label: 'Remove from list', accent: 'danger' })]
|
||||
|
||||
function mountActions(mainAction: Partial<LeafActionItem> | null = {}) {
|
||||
return mount(V2vVmActions, {
|
||||
props: {
|
||||
mainAction: mainAction === null ? undefined : createActionItem({ label: 'Edit settings', ...mainAction }),
|
||||
moreActions: MORE_ACTIONS,
|
||||
},
|
||||
global: createGlobalTestConfig(),
|
||||
})
|
||||
}
|
||||
|
||||
function getMainButton(wrapper: ReturnType<typeof mountActions>) {
|
||||
return wrapper.getComponent(UiButton)
|
||||
}
|
||||
|
||||
it('offers the main action as a button', () => {
|
||||
const button = getMainButton(mountActions({ icon: 'action:edit' }))
|
||||
|
||||
expect([button.text(), button.props('leftIcon'), button.props('variant')]).toEqual([
|
||||
'Edit settings',
|
||||
'action:edit',
|
||||
'primary',
|
||||
])
|
||||
})
|
||||
|
||||
it('runs the main action on click', async () => {
|
||||
const onClick = vi.fn()
|
||||
|
||||
await getMainButton(mountActions({ onClick })).trigger('click')
|
||||
|
||||
expect(onClick).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('disables the main action, with the reason as tooltip, when it cannot run', async () => {
|
||||
const button = getMainButton(mountActions({ disabled: true, hint: 'Locked since the first sync' }))
|
||||
|
||||
expect(button.props('disabled')).toBe(true)
|
||||
expect(await findTooltipContent(button.element)).toBe('Locked since the first sync')
|
||||
})
|
||||
|
||||
it('has no tooltip when the main action can run', async () => {
|
||||
expect(await findTooltipContent(getMainButton(mountActions()).element)).toBe(false)
|
||||
})
|
||||
|
||||
it('shows the main action busy while it runs', () => {
|
||||
expect(getMainButton(mountActions({ busy: true })).props('busy')).toBe(true)
|
||||
})
|
||||
|
||||
it('offers the other actions in its menu', () => {
|
||||
expect(mountActions().getComponent(VtsActionsMenu).props('actions')).toEqual(MORE_ACTIONS)
|
||||
})
|
||||
|
||||
it('has no button without a main action', () => {
|
||||
expect(mountActions(null).findComponent(UiButton).exists()).toBe(false)
|
||||
})
|
||||
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<UiButton
|
||||
v-if="mainAction"
|
||||
v-tooltip="mainAction.hint ?? false"
|
||||
size="medium"
|
||||
variant="primary"
|
||||
accent="brand"
|
||||
:disabled="mainAction.disabled"
|
||||
:left-icon="mainAction.icon"
|
||||
:busy="mainAction.busy"
|
||||
@click="mainAction.onClick()"
|
||||
>
|
||||
{{ mainAction.label }}
|
||||
</UiButton>
|
||||
<VtsActionsMenu size="medium" :actions="moreActions" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { V2vVmActionList } from '@/modules/v2v/composables/use-v2v-vm-actions.composable.ts'
|
||||
import VtsActionsMenu from '@core/components/menu/VtsActionsMenu.vue'
|
||||
import UiButton from '@core/components/ui/button/UiButton.vue'
|
||||
import { vTooltip } from '@core/directives/tooltip.directive.ts'
|
||||
|
||||
// A VM gone from the source has no main action, only the ones of the menu
|
||||
defineProps<V2vVmActionList>()
|
||||
</script>
|
||||
@@ -0,0 +1,39 @@
|
||||
import V2vVmHeadingCard from '@/modules/v2v/components/panel/V2vVmHeadingCard.vue'
|
||||
import type { V2vMappingVmHeading } from '@/modules/v2v/composables/use-v2v-mapping-step.composable.ts'
|
||||
import { createV2vNewMappingStepVm } from '@/test/create-v2v-mapping-step-vm.ts'
|
||||
import { createV2vMappingVmHeading } from '@/test/create-v2v-mapping-vm-heading.ts'
|
||||
import { findAccentedTags } from '@/test/find-accented-labels.ts'
|
||||
import { findParts } from '@/test/find-parts.ts'
|
||||
import { findCardLabelledValues } from '@/test/find-rendered-values.ts'
|
||||
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
|
||||
import { t } from '@/test/i18n.ts'
|
||||
import UiPanelCardTitle from '@core/components/ui/panel-card-title/UiPanelCardTitle.vue'
|
||||
import type { XoV2vVm } from '@vates/types'
|
||||
import { mount } from '@vue/test-utils'
|
||||
|
||||
const VM = createV2vNewMappingStepVm()
|
||||
|
||||
function mountCard({ id = VM.id, heading = VM.heading }: { id?: XoV2vVm['id']; heading?: V2vMappingVmHeading } = {}) {
|
||||
return mount(V2vVmHeadingCard, { props: { id, heading }, global: createGlobalTestConfig() })
|
||||
}
|
||||
|
||||
it('titles the card with a VM icon, the name of the VM and its copyable id, without a link', () => {
|
||||
const title = mountCard().getComponent(UiPanelCardTitle)
|
||||
|
||||
expect(title.props()).toMatchObject({ label: VM.heading.name, id: VM.id, icon: 'object:vm', to: undefined })
|
||||
})
|
||||
|
||||
it('tags the VM with its status, then locates it on the source', () => {
|
||||
const wrapper = mountCard()
|
||||
|
||||
expect(Object.keys(findCardLabelledValues(wrapper))).toEqual([t('status'), t('location')])
|
||||
expect(findAccentedTags(wrapper)).toEqual([VM.heading.statusTag])
|
||||
expect(findParts(wrapper, '.inline-parts')).toEqual(VM.heading.locationParts)
|
||||
})
|
||||
|
||||
it('leaves out the status of a VM without one', () => {
|
||||
const wrapper = mountCard({ heading: createV2vMappingVmHeading({ statusTag: undefined }) })
|
||||
|
||||
expect(Object.keys(findCardLabelledValues(wrapper))).toEqual([t('location')])
|
||||
expect(findAccentedTags(wrapper)).toEqual([])
|
||||
})
|
||||
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<UiPanelCard class="v2v-vm-heading-card">
|
||||
<UiPanelCardTitle :id :label="heading.name" icon="object:vm" size="medium" />
|
||||
<div class="content">
|
||||
<VtsCardRowKeyValue v-if="heading.statusTag">
|
||||
<template #key>{{ t('status') }}</template>
|
||||
<template #value>
|
||||
<UiTag :accent="heading.statusTag.accent" variant="secondary">{{ heading.statusTag.label }}</UiTag>
|
||||
</template>
|
||||
</VtsCardRowKeyValue>
|
||||
<VtsCardRowKeyValue>
|
||||
<template #key>{{ t('location') }}</template>
|
||||
<template #value>
|
||||
<InlineParts separator="chevron">
|
||||
<span v-for="part of heading.locationParts" :key="part">{{ part }}</span>
|
||||
</InlineParts>
|
||||
</template>
|
||||
</VtsCardRowKeyValue>
|
||||
</div>
|
||||
</UiPanelCard>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { V2vMappingVmHeading } from '@/modules/v2v/composables/use-v2v-mapping-step.composable.ts'
|
||||
import InlineParts from '@/shared/components/InlineParts.vue'
|
||||
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
|
||||
import UiPanelCard from '@core/components/ui/panel-card/UiPanelCard.vue'
|
||||
import UiPanelCardTitle from '@core/components/ui/panel-card-title/UiPanelCardTitle.vue'
|
||||
import UiTag from '@core/components/ui/tag/UiTag.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
// A source VM has no page in XO to link to; its power state is not part of its heading, hence the plain VM icon
|
||||
defineProps<{
|
||||
// Not `XoV2vVm['id']`: Vue resolves the branded type to a runtime `Object` check and warns on the string id
|
||||
id: string
|
||||
heading: V2vMappingVmHeading
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.v2v-vm-heading-card {
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,71 @@
|
||||
import V2vNewVmActions from '@/modules/v2v/components/panel/V2vNewVmActions.vue'
|
||||
import V2vNotFoundVmActions from '@/modules/v2v/components/panel/V2vNotFoundVmActions.vue'
|
||||
import V2vResumedVmActions from '@/modules/v2v/components/panel/V2vResumedVmActions.vue'
|
||||
import V2vVmKindActions from '@/modules/v2v/components/panel/V2vVmKindActions.vue'
|
||||
import type { V2vMappingStepVm } from '@/modules/v2v/composables/use-v2v-mapping-step.composable.ts'
|
||||
import { V2V_NEW_STEP_VM, V2V_RESUMED_STEP_VM, V2V_NOT_FOUND_STEP_VM } from '@/test/create-v2v-mapping-step-vm.ts'
|
||||
import { findShownComponents } from '@/test/find-shown-components.ts'
|
||||
import { v2vVmActionsMock } from '@/test/mock-v2v-vm-actions.ts'
|
||||
import { mountV2vVmKindActions, findV2vVmSlotActions, getV2vVmKindActionList } from '@/test/v2v-vm-kind-actions.ts'
|
||||
import { type VueWrapper } from '@vue/test-utils'
|
||||
|
||||
vi.mock(
|
||||
import('@/modules/v2v/composables/use-v2v-vm-actions.composable.ts'),
|
||||
() => import('@/test/mock-v2v-vm-actions.ts')
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
v2vVmActionsMock.reset()
|
||||
})
|
||||
|
||||
const updateVm = vi.fn()
|
||||
|
||||
function mountKindActions(vm: V2vMappingStepVm) {
|
||||
return mountV2vVmKindActions(V2vVmKindActions, { vm, updateVm })
|
||||
}
|
||||
|
||||
const KIND_ACTIONS = [V2vNewVmActions, V2vResumedVmActions, V2vNotFoundVmActions]
|
||||
|
||||
function findKindActions(wrapper: VueWrapper) {
|
||||
return findShownComponents(wrapper, KIND_ACTIONS)
|
||||
}
|
||||
|
||||
it.each([
|
||||
['a new VM, stored through the list', V2V_NEW_STEP_VM, V2vNewVmActions, { vm: V2V_NEW_STEP_VM, updateVm }],
|
||||
['a resumed VM', V2V_RESUMED_STEP_VM, V2vResumedVmActions, { vm: V2V_RESUMED_STEP_VM }],
|
||||
['a VM not found in the source', V2V_NOT_FOUND_STEP_VM, V2vNotFoundVmActions, { vm: V2V_NOT_FOUND_STEP_VM }],
|
||||
] as const)('runs the actions of %s', (_, vm, kindActions, props) => {
|
||||
const wrapper = mountKindActions(vm)
|
||||
|
||||
expect(findKindActions(wrapper)).toEqual([kindActions])
|
||||
expect(wrapper.getComponent(kindActions).props()).toEqual(props)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['a new VM', V2V_NEW_STEP_VM, v2vVmActionsMock.useV2vNewVmActions],
|
||||
['a resumed VM', V2V_RESUMED_STEP_VM, v2vVmActionsMock.useV2vResumedVmActions],
|
||||
['a VM not found in the source', V2V_NOT_FOUND_STEP_VM, v2vVmActionsMock.useV2vNotFoundVmActions],
|
||||
] as const)('hands its slot the actions of %s', (_, vm, useActions) => {
|
||||
const wrapper = mountKindActions(vm)
|
||||
|
||||
expect(findV2vVmSlotActions(wrapper)).toEqual(getV2vVmKindActionList(useActions))
|
||||
})
|
||||
|
||||
it('switches to the actions of the new VM a resumed one becomes once its import VM is deleted', async () => {
|
||||
const wrapper = mountKindActions(V2V_RESUMED_STEP_VM)
|
||||
|
||||
await wrapper.setProps({ vm: V2V_NEW_STEP_VM })
|
||||
|
||||
expect(findKindActions(wrapper)).toEqual([V2vNewVmActions])
|
||||
})
|
||||
|
||||
it.each([
|
||||
['a new VM', V2V_NEW_STEP_VM, V2vNewVmActions],
|
||||
['a VM not found in the source', V2V_NOT_FOUND_STEP_VM, V2vNotFoundVmActions],
|
||||
] as const)('emits the removal of %s', async (_, vm, kindActions) => {
|
||||
const wrapper = mountKindActions(vm)
|
||||
|
||||
await wrapper.getComponent(kindActions).vm.$emit('remove', vm.id)
|
||||
|
||||
expect(wrapper.emitted('remove')).toEqual([[vm.id]])
|
||||
})
|
||||
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<V2vNewVmActions v-if="vm.kind === 'new'" v-slot="actions" :vm :update-vm @remove="id => emit('remove', id)">
|
||||
<slot v-bind="actions" />
|
||||
</V2vNewVmActions>
|
||||
<V2vResumedVmActions v-else-if="vm.kind === 'resume'" v-slot="actions" :vm>
|
||||
<slot v-bind="actions" />
|
||||
</V2vResumedVmActions>
|
||||
<V2vNotFoundVmActions v-else v-slot="actions" :vm @remove="id => emit('remove', id)">
|
||||
<slot v-bind="actions" />
|
||||
</V2vNotFoundVmActions>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import V2vNewVmActions from '@/modules/v2v/components/panel/V2vNewVmActions.vue'
|
||||
import V2vNotFoundVmActions from '@/modules/v2v/components/panel/V2vNotFoundVmActions.vue'
|
||||
import V2vResumedVmActions from '@/modules/v2v/components/panel/V2vResumedVmActions.vue'
|
||||
import type { V2vMappingStepVm } from '@/modules/v2v/composables/use-v2v-mapping-step.composable.ts'
|
||||
import type { V2vVmActionList } from '@/modules/v2v/composables/use-v2v-vm-actions.composable.ts'
|
||||
import type { V2vVmMappingContext } from '@/modules/v2v/composables/use-v2v-vm-mapping.composable.ts'
|
||||
import type { XoV2vVm } from '@vates/types'
|
||||
|
||||
// The actions of the kind of the VM, for the caller to lay out: the side panel header and a table row menu read the
|
||||
// same ones. The kind of a VM changes once the import VM of a resumed VM is deleted: its actions then switch with it
|
||||
defineProps<{
|
||||
vm: V2vMappingStepVm
|
||||
updateVm: V2vVmMappingContext['updateVm']
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
remove: [id: XoV2vVm['id']]
|
||||
}>()
|
||||
|
||||
defineSlots<{
|
||||
default(props: V2vVmActionList): any
|
||||
}>()
|
||||
</script>
|
||||
@@ -0,0 +1,60 @@
|
||||
import V2vVmOptionsCard from '@/modules/v2v/components/panel/V2vVmOptionsCard.vue'
|
||||
import { findPanelCardTitle } from '@/test/find-card-title.ts'
|
||||
import { getGenericComponent } from '@/test/find-generic-components.ts'
|
||||
import { findCardLabelledValues } from '@/test/find-rendered-values.ts'
|
||||
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
|
||||
import { t } from '@/test/i18n.ts'
|
||||
import {
|
||||
V2V_DEBIAN_TEMPLATE,
|
||||
V2V_MIGRATED_VM_TAGS,
|
||||
V2V_TARGET_OPTIONS,
|
||||
createV2vOfferedTarget,
|
||||
} from '@/test/v2v-mapping-fixtures.ts'
|
||||
import VtsTag from '@core/components/tag/VtsTag.vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import type { ComponentProps } from 'vue-component-type-helpers'
|
||||
|
||||
const TEMPLATE = createV2vOfferedTarget(V2V_DEBIAN_TEMPLATE.id, 'overridden')
|
||||
|
||||
function mountCard(props: Partial<ComponentProps<typeof V2vVmOptionsCard>> = {}) {
|
||||
return mount(V2vVmOptionsCard, {
|
||||
props: {
|
||||
keepMac: true,
|
||||
template: TEMPLATE,
|
||||
templateOptions: V2V_TARGET_OPTIONS.templates,
|
||||
tags: V2V_MIGRATED_VM_TAGS,
|
||||
...props,
|
||||
},
|
||||
global: createGlobalTestConfig(),
|
||||
})
|
||||
}
|
||||
|
||||
it('shows its title', () => {
|
||||
expect(findPanelCardTitle(mountCard())).toBe(t('options'))
|
||||
})
|
||||
|
||||
it.each([
|
||||
[true, 'enabled'],
|
||||
[false, 'disabled'],
|
||||
] as const)('tells whether the MAC addresses are kept (%s)', (keepMac, statusKey) => {
|
||||
expect(findCardLabelledValues(mountCard({ keepMac }))[t('keep-mac-addresses')]).toBe(t(statusKey))
|
||||
})
|
||||
|
||||
it('names the template among the offered ones, and where it comes from', () => {
|
||||
expect(getGenericComponent(mountCard(), 'V2vTargetValue').props()).toEqual({
|
||||
target: TEMPLATE,
|
||||
options: V2V_TARGET_OPTIONS.templates,
|
||||
})
|
||||
})
|
||||
|
||||
it('lists the tags of the VM', () => {
|
||||
const tags = mountCard({ tags: ['web', 'prod'] })
|
||||
.findAllComponents(VtsTag)
|
||||
.map(tag => tag.props('value'))
|
||||
|
||||
expect(tags).toEqual(['web', 'prod'])
|
||||
})
|
||||
|
||||
it('lists no tag for a VM without any', () => {
|
||||
expect(mountCard({ tags: [] }).findAllComponents(VtsTag)).toEqual([])
|
||||
})
|
||||
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<UiPanelCard class="v2v-vm-options-card">
|
||||
<UiPanelCardTitle size="medium" :label="t('options')" />
|
||||
<div class="content">
|
||||
<VtsCardRowKeyValue>
|
||||
<template #key>{{ t('keep-mac-addresses') }}</template>
|
||||
<template #value>
|
||||
<VtsStatus :status="keepMac" />
|
||||
</template>
|
||||
</VtsCardRowKeyValue>
|
||||
<VtsCardRowKeyValue>
|
||||
<template #key>{{ t('template') }}</template>
|
||||
<template #value>
|
||||
<V2vTargetValue :target="template" :options="templateOptions" />
|
||||
</template>
|
||||
</VtsCardRowKeyValue>
|
||||
<VtsCardRowKeyValue>
|
||||
<template #key>{{ t('tags') }}</template>
|
||||
<template #value>
|
||||
<UiTagsList v-if="tags.length > 0">
|
||||
<VtsTag v-for="tag of tags" :key="tag" :value="tag" />
|
||||
</UiTagsList>
|
||||
</template>
|
||||
</VtsCardRowKeyValue>
|
||||
</div>
|
||||
</UiPanelCard>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import V2vTargetValue from '@/modules/v2v/components/mapping/V2vTargetValue.vue'
|
||||
import type { V2vResolvedTarget, V2vTargetOption } from '@/modules/v2v/types/v2v-mapping.type.ts'
|
||||
import type { FrontXoVmTemplate } from '@/modules/vm/remote-resources/use-xo-vm-template-collection.ts'
|
||||
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
|
||||
import VtsStatus from '@core/components/status/VtsStatus.vue'
|
||||
import VtsTag from '@core/components/tag/VtsTag.vue'
|
||||
import UiPanelCard from '@core/components/ui/panel-card/UiPanelCard.vue'
|
||||
import UiPanelCardTitle from '@core/components/ui/panel-card-title/UiPanelCardTitle.vue'
|
||||
import UiTagsList from '@core/components/ui/tag/UiTagsList.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
defineProps<{
|
||||
keepMac: boolean
|
||||
template: V2vResolvedTarget<FrontXoVmTemplate['id']>
|
||||
templateOptions: V2vTargetOption<FrontXoVmTemplate['id']>[]
|
||||
tags: string[]
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.v2v-vm-options-card {
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,156 @@
|
||||
import V2vNewVmCards from '@/modules/v2v/components/panel/V2vNewVmCards.vue'
|
||||
import V2vResumedVmCards from '@/modules/v2v/components/panel/V2vResumedVmCards.vue'
|
||||
import V2vVmActions from '@/modules/v2v/components/panel/V2vVmActions.vue'
|
||||
import V2vVmHeadingCard from '@/modules/v2v/components/panel/V2vVmHeadingCard.vue'
|
||||
import V2vVmKindActions from '@/modules/v2v/components/panel/V2vVmKindActions.vue'
|
||||
import V2vVmSidePanel from '@/modules/v2v/components/panel/V2vVmSidePanel.vue'
|
||||
import type { V2vMappingStepVm } from '@/modules/v2v/composables/use-v2v-mapping-step.composable.ts'
|
||||
import { V2V_NEW_STEP_VM, V2V_RESUMED_STEP_VM, V2V_NOT_FOUND_STEP_VM } from '@/test/create-v2v-mapping-step-vm.ts'
|
||||
import { findShownComponents } from '@/test/find-shown-components.ts'
|
||||
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
|
||||
import { t } from '@/test/i18n.ts'
|
||||
import { v2vVmActionsMock } from '@/test/mock-v2v-vm-actions.ts'
|
||||
import { getV2vVmKindActionList } from '@/test/v2v-vm-kind-actions.ts'
|
||||
import VtsSidePanel from '@core/components/panel/VtsSidePanel.vue'
|
||||
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
|
||||
import { mount, type VueWrapper } from '@vue/test-utils'
|
||||
|
||||
vi.mock(
|
||||
import('@/modules/v2v/composables/use-v2v-vm-actions.composable.ts'),
|
||||
() => import('@/test/mock-v2v-vm-actions.ts')
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
v2vVmActionsMock.reset()
|
||||
})
|
||||
|
||||
const updateVm = vi.fn()
|
||||
|
||||
// The actions and the cards of each kind are covered by their own tests: here only which ones the panel shows matters
|
||||
function mountPanel(vm: V2vMappingStepVm | undefined) {
|
||||
return mount(V2vVmSidePanel, {
|
||||
props: { vm, updateVm },
|
||||
global: createGlobalTestConfig({
|
||||
stubs: {
|
||||
V2vNewVmCards: true,
|
||||
V2vResumedVmCards: true,
|
||||
V2vVmHeadingCard: true,
|
||||
},
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
const KIND_COMPONENTS = [V2vNewVmCards, V2vResumedVmCards]
|
||||
|
||||
function findKindComponents(wrapper: VueWrapper) {
|
||||
return findShownComponents(wrapper, KIND_COMPONENTS)
|
||||
}
|
||||
|
||||
function findKindActionsVm(wrapper: VueWrapper) {
|
||||
return wrapper.getComponent(V2vVmKindActions).props('vm')
|
||||
}
|
||||
|
||||
function findNotFoundHero(wrapper: VueWrapper) {
|
||||
return wrapper.findAllComponents(VtsStateHero).find(hero => hero.props('type') === 'not-found')
|
||||
}
|
||||
|
||||
it.each([
|
||||
['holds a selection, for the side panel to show it and offer to close it', V2V_NEW_STEP_VM, true],
|
||||
['holds no selection without VM, for the side panel to show its empty state', undefined, false],
|
||||
])('%s', (_, vm, hasSelection) => {
|
||||
expect(mountPanel(vm).getComponent(VtsSidePanel).props('hasSelection')).toBe(hasSelection)
|
||||
})
|
||||
|
||||
it('shows neither heading, cards nor actions without VM', () => {
|
||||
const wrapper = mountPanel(undefined)
|
||||
|
||||
expect(wrapper.findComponent(V2vVmHeadingCard).exists()).toBe(false)
|
||||
expect(findKindComponents(wrapper)).toEqual([])
|
||||
expect(wrapper.findComponent(V2vVmKindActions).exists()).toBe(false)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['a new VM', V2V_NEW_STEP_VM],
|
||||
['a resumed VM', V2V_RESUMED_STEP_VM],
|
||||
['a VM not found in the source', V2V_NOT_FOUND_STEP_VM],
|
||||
])('heads the body of %s with its heading, first', (_, vm) => {
|
||||
const wrapper = mountPanel(vm)
|
||||
const headingCard = wrapper.getComponent(V2vVmHeadingCard)
|
||||
|
||||
expect(headingCard.props()).toEqual({ id: vm.id, heading: vm.heading })
|
||||
expect(wrapper.get('.ui-panel > .content').element.firstElementChild).toBe(headingCard.element)
|
||||
})
|
||||
|
||||
it('closes on request', async () => {
|
||||
const wrapper = mountPanel(V2V_NEW_STEP_VM)
|
||||
|
||||
await wrapper.getComponent(VtsSidePanel).vm.$emit('close')
|
||||
|
||||
expect(wrapper.emitted('close')).toEqual([[]])
|
||||
})
|
||||
|
||||
it('lays the actions of the VM out as a main button and a menu', () => {
|
||||
const wrapper = mountPanel(V2V_NEW_STEP_VM)
|
||||
|
||||
expect(wrapper.getComponent(V2vVmActions).props()).toEqual(
|
||||
getV2vVmKindActionList(v2vVmActionsMock.useV2vNewVmActions)
|
||||
)
|
||||
})
|
||||
|
||||
describe('new VM', () => {
|
||||
it('heads the panel with the actions of a new VM, stored through the list', () => {
|
||||
expect(mountPanel(V2V_NEW_STEP_VM).getComponent(V2vVmKindActions).props()).toEqual({
|
||||
vm: V2V_NEW_STEP_VM,
|
||||
updateVm,
|
||||
})
|
||||
})
|
||||
|
||||
it('shows the mapping of the VM', () => {
|
||||
const wrapper = mountPanel(V2V_NEW_STEP_VM)
|
||||
|
||||
expect(wrapper.getComponent(V2vNewVmCards).props()).toEqual({ vm: V2V_NEW_STEP_VM })
|
||||
expect(findKindComponents(wrapper)).toEqual([V2vNewVmCards])
|
||||
})
|
||||
})
|
||||
|
||||
describe('resumed VM', () => {
|
||||
it('heads the panel with the actions of a resumed VM, and shows its first sync', () => {
|
||||
const wrapper = mountPanel(V2V_RESUMED_STEP_VM)
|
||||
|
||||
expect([findKindActionsVm(wrapper), wrapper.getComponent(V2vResumedVmCards).props('vm')]).toEqual([
|
||||
V2V_RESUMED_STEP_VM,
|
||||
V2V_RESUMED_STEP_VM,
|
||||
])
|
||||
expect(findKindComponents(wrapper)).toEqual([V2vResumedVmCards])
|
||||
})
|
||||
|
||||
it('switches to the cards of the new VM it becomes once its import VM is deleted', async () => {
|
||||
const wrapper = mountPanel(V2V_RESUMED_STEP_VM)
|
||||
|
||||
await wrapper.setProps({ vm: V2V_NEW_STEP_VM })
|
||||
|
||||
expect(findKindComponents(wrapper)).toEqual([V2vNewVmCards])
|
||||
})
|
||||
})
|
||||
|
||||
describe('VM not found in the source', () => {
|
||||
it('tells the VM is not found in the source, with its actions', () => {
|
||||
const wrapper = mountPanel(V2V_NOT_FOUND_STEP_VM)
|
||||
|
||||
expect(findNotFoundHero(wrapper)?.text()).toBe(t('v2v:not-found-in-source'))
|
||||
expect(findKindActionsVm(wrapper)).toEqual(V2V_NOT_FOUND_STEP_VM)
|
||||
expect(findKindComponents(wrapper)).toEqual([])
|
||||
})
|
||||
|
||||
it('has no not-found hero for a VM of the source', () => {
|
||||
expect(findNotFoundHero(mountPanel(V2V_NEW_STEP_VM))).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
it('emits the removal of its VM', async () => {
|
||||
const wrapper = mountPanel(V2V_NEW_STEP_VM)
|
||||
|
||||
await wrapper.getComponent(V2vVmKindActions).vm.$emit('remove', V2V_NEW_STEP_VM.id)
|
||||
|
||||
expect(wrapper.emitted('remove')).toEqual([[V2V_NEW_STEP_VM.id]])
|
||||
})
|
||||
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<VtsSidePanel :has-selection="!!vm" @close="emit('close')">
|
||||
<template v-if="vm" #actions>
|
||||
<V2vVmKindActions v-slot="actions" :vm :update-vm @remove="id => emit('remove', id)">
|
||||
<V2vVmActions v-bind="actions" />
|
||||
</V2vVmKindActions>
|
||||
</template>
|
||||
<template v-if="vm" #default>
|
||||
<V2vVmHeadingCard :id="vm.id" :heading="vm.heading" />
|
||||
<V2vNewVmCards v-if="vm.kind === 'new'" :vm />
|
||||
<V2vResumedVmCards v-else-if="vm.kind === 'resume'" :vm />
|
||||
<VtsStateHero v-else format="panel" type="not-found" size="medium">
|
||||
{{ t('v2v:not-found-in-source') }}
|
||||
</VtsStateHero>
|
||||
</template>
|
||||
</VtsSidePanel>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import V2vNewVmCards from '@/modules/v2v/components/panel/V2vNewVmCards.vue'
|
||||
import V2vResumedVmCards from '@/modules/v2v/components/panel/V2vResumedVmCards.vue'
|
||||
import V2vVmActions from '@/modules/v2v/components/panel/V2vVmActions.vue'
|
||||
import V2vVmHeadingCard from '@/modules/v2v/components/panel/V2vVmHeadingCard.vue'
|
||||
import V2vVmKindActions from '@/modules/v2v/components/panel/V2vVmKindActions.vue'
|
||||
import type { V2vMappingStepVm } from '@/modules/v2v/composables/use-v2v-mapping-step.composable.ts'
|
||||
import type { V2vVmMappingContext } from '@/modules/v2v/composables/use-v2v-vm-mapping.composable.ts'
|
||||
import VtsSidePanel from '@core/components/panel/VtsSidePanel.vue'
|
||||
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
|
||||
import type { XoV2vVm } from '@vates/types'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
// Each kind has its own actions and cards, for each runs its own composables: the kind of the VM changes once the
|
||||
// import VM of a resumed VM is deleted, and the panel then shows the new VM it became
|
||||
defineProps<{
|
||||
vm?: V2vMappingStepVm
|
||||
updateVm: V2vVmMappingContext['updateVm']
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: []
|
||||
remove: [id: XoV2vVm['id']]
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
</script>
|
||||
@@ -0,0 +1,31 @@
|
||||
import V2vListPairSummary from '@/modules/v2v/components/selected-vms/V2vListPairSummary.vue'
|
||||
import type { V2vListPair } from '@/modules/v2v/composables/use-v2v-selected-vms.composable.ts'
|
||||
import { createV2vListPair } from '@/test/create-v2v-selected-vms.ts'
|
||||
import { findCardLabelledValues } from '@/test/find-rendered-values.ts'
|
||||
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
|
||||
import { t } from '@/test/i18n.ts'
|
||||
import { mount } from '@vue/test-utils'
|
||||
|
||||
const LIST = createV2vListPair()
|
||||
|
||||
function mountListPair(props: V2vListPair = LIST) {
|
||||
return mount(V2vListPairSummary, { props, global: createGlobalTestConfig() })
|
||||
}
|
||||
|
||||
it('names the source and the destination of the list', () => {
|
||||
expect(findCardLabelledValues(mountListPair())).toEqual({
|
||||
[t('source')]: LIST.sourceHost,
|
||||
[t('destination')]: LIST.poolName,
|
||||
})
|
||||
})
|
||||
|
||||
it('leaves the values empty while unknown', () => {
|
||||
expect(findCardLabelledValues(mountListPair({}))).toEqual({
|
||||
[t('source')]: '',
|
||||
[t('destination')]: '',
|
||||
})
|
||||
})
|
||||
|
||||
it('explains that another source or pool has its own list', () => {
|
||||
expect(mountListPair().get('.note').text()).toBe(t('v2v:list-pair-info'))
|
||||
})
|
||||
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<div class="v2v-list-pair-summary">
|
||||
<VtsCardRowKeyValue>
|
||||
<template #key>{{ t('source') }}</template>
|
||||
<template #value>{{ sourceHost }}</template>
|
||||
</VtsCardRowKeyValue>
|
||||
<VtsCardRowKeyValue>
|
||||
<template #key>{{ t('destination') }}</template>
|
||||
<template #value>{{ poolName }}</template>
|
||||
</VtsCardRowKeyValue>
|
||||
<p class="note typo-body-regular-small">{{ t('v2v:list-pair-info') }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { V2vListPair } from '@/modules/v2v/composables/use-v2v-selected-vms.composable.ts'
|
||||
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
defineProps<V2vListPair>()
|
||||
|
||||
const { t } = useI18n()
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.v2v-list-pair-summary {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
|
||||
.note {
|
||||
color: var(--color-neutral-txt-secondary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,63 @@
|
||||
import V2vSelectedVmItem from '@/modules/v2v/components/selected-vms/V2vSelectedVmItem.vue'
|
||||
import type { V2vSelectedVm } from '@/modules/v2v/composables/use-v2v-selected-vms.composable.ts'
|
||||
import { createV2vSelectedVm } from '@/test/create-v2v-selected-vms.ts'
|
||||
import { createV2vWizardRouter } from '@/test/create-v2v-wizard-router.ts'
|
||||
import { findAccentedInfos, findAccentedTags } from '@/test/find-accented-labels.ts'
|
||||
import { findParts } from '@/test/find-parts.ts'
|
||||
import { getButton } from '@/test/get-button.ts'
|
||||
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
|
||||
import { t } from '@/test/i18n.ts'
|
||||
import UiLink from '@core/components/ui/link/UiLink.vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
|
||||
const ITEM = createV2vSelectedVm()
|
||||
|
||||
async function mountItem(item: V2vSelectedVm = ITEM) {
|
||||
const router = await createV2vWizardRouter('vms')
|
||||
|
||||
return mount(V2vSelectedVmItem, { props: { item }, global: createGlobalTestConfig({ router }) })
|
||||
}
|
||||
|
||||
it('links the name of the VM to its mapping', async () => {
|
||||
const link = (await mountItem()).getComponent(UiLink)
|
||||
|
||||
expect(link.text()).toBe(ITEM.name)
|
||||
expect(link.attributes('href')).toBe(`/admin/v2v/migrate?step=mapping&vm=${ITEM.id}`)
|
||||
})
|
||||
|
||||
it('tags the VM with its kind', async () => {
|
||||
const kind = { label: 'Resume', accent: 'info' } as const
|
||||
|
||||
expect(findAccentedTags(await mountItem(createV2vSelectedVm({ kind })))).toEqual([kind])
|
||||
})
|
||||
|
||||
it('has no kind tag without a kind', async () => {
|
||||
expect(findAccentedTags(await mountItem(createV2vSelectedVm({ kind: undefined })))).toEqual([])
|
||||
})
|
||||
|
||||
it('lays out its mapping', async () => {
|
||||
expect(findParts(await mountItem(), '.mapping')).toEqual(ITEM.mappingParts)
|
||||
})
|
||||
|
||||
it('has no mapping line without mapping parts', async () => {
|
||||
expect((await mountItem(createV2vSelectedVm({ mappingParts: [] }))).find('.mapping').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('tells its check with its accent', async () => {
|
||||
const check = { label: 'Import in progress', accent: 'danger' } as const
|
||||
|
||||
expect(findAccentedInfos(await mountItem(createV2vSelectedVm({ check })))).toEqual([check])
|
||||
})
|
||||
|
||||
// The router alone marks every mapping link as current: they only differ by their query
|
||||
it('does not mark its link as the current page', async () => {
|
||||
expect((await mountItem()).get('a').attributes('aria-current')).toBe('false')
|
||||
})
|
||||
|
||||
it('removes the VM from the list', async () => {
|
||||
const wrapper = await mountItem()
|
||||
|
||||
await getButton(wrapper, t('action:remove-name-from-list', { name: ITEM.name })).trigger('click')
|
||||
|
||||
expect(wrapper.emitted('remove')).toEqual([[ITEM.id]])
|
||||
})
|
||||
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<UiPanelCard class="v2v-selected-vm-item">
|
||||
<UiPanelCardTitle size="small">
|
||||
<!-- The router alone marks every mapping link as current: they only differ by their query -->
|
||||
<template #label>
|
||||
<UiLink size="small" :to="item.mappingRoute" :aria-current="false">
|
||||
{{ item.name }}
|
||||
</UiLink>
|
||||
</template>
|
||||
<template v-if="item.kind" #action>
|
||||
<UiTag :accent="item.kind.accent" variant="secondary">{{ item.kind.label }}</UiTag>
|
||||
</template>
|
||||
<template #more-actions>
|
||||
<UiButtonIcon
|
||||
v-tooltip="removeLabel"
|
||||
icon="action:close-cancel-clear"
|
||||
accent="brand"
|
||||
size="small"
|
||||
:aria-label="removeLabel"
|
||||
@click="emit('remove', item.id)"
|
||||
/>
|
||||
</template>
|
||||
</UiPanelCardTitle>
|
||||
<InlineParts v-if="item.mappingParts.length > 0" class="mapping typo-body-regular-small">
|
||||
<span v-for="part of item.mappingParts" :key="part">{{ part }}</span>
|
||||
</InlineParts>
|
||||
<UiInfo :accent="item.check.accent" size="small">{{ item.check.label }}</UiInfo>
|
||||
</UiPanelCard>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { V2vSelectedVm } from '@/modules/v2v/composables/use-v2v-selected-vms.composable.ts'
|
||||
import InlineParts from '@/shared/components/InlineParts.vue'
|
||||
import UiButtonIcon from '@core/components/ui/button-icon/UiButtonIcon.vue'
|
||||
import UiInfo from '@core/components/ui/info/UiInfo.vue'
|
||||
import UiLink from '@core/components/ui/link/UiLink.vue'
|
||||
import UiPanelCard from '@core/components/ui/panel-card/UiPanelCard.vue'
|
||||
import UiPanelCardTitle from '@core/components/ui/panel-card-title/UiPanelCardTitle.vue'
|
||||
import UiTag from '@core/components/ui/tag/UiTag.vue'
|
||||
import { vTooltip } from '@core/directives/tooltip.directive.ts'
|
||||
import type { XoV2vVm } from '@vates/types'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { item } = defineProps<{
|
||||
item: V2vSelectedVm
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
remove: [id: XoV2vVm['id']]
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const removeLabel = computed(() => t('action:remove-name-from-list', { name: item.name }))
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.v2v-selected-vm-item {
|
||||
.mapping {
|
||||
color: var(--color-neutral-txt-secondary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,152 @@
|
||||
import V2vListPairSummary from '@/modules/v2v/components/selected-vms/V2vListPairSummary.vue'
|
||||
import V2vSelectedVmItem from '@/modules/v2v/components/selected-vms/V2vSelectedVmItem.vue'
|
||||
import V2vSelectedVmsPanel from '@/modules/v2v/components/selected-vms/V2vSelectedVmsPanel.vue'
|
||||
import type { V2vAccentedLabel } from '@/modules/v2v/composables/use-v2v-vm-labels.composable.ts'
|
||||
import { createV2vListPair, createV2vSelectedVm } from '@/test/create-v2v-selected-vms.ts'
|
||||
import { findAccentedTags } from '@/test/find-accented-labels.ts'
|
||||
import { findParts } from '@/test/find-parts.ts'
|
||||
import { findButton, getButton } from '@/test/get-button.ts'
|
||||
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
|
||||
import { t } from '@/test/i18n.ts'
|
||||
import type { TagAccent } from '@core/components/ui/tag/UiTag.vue'
|
||||
import VtsSidePanel from '@core/components/panel/VtsSidePanel.vue'
|
||||
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
|
||||
import UiCounter from '@core/components/ui/counter/UiCounter.vue'
|
||||
import UiPanelCard from '@core/components/ui/panel-card/UiPanelCard.vue'
|
||||
import UiPanelCardTitle from '@core/components/ui/panel-card-title/UiPanelCardTitle.vue'
|
||||
import type { XoV2vVm } from '@vates/types'
|
||||
import { mount, type VueWrapper } from '@vue/test-utils'
|
||||
import type { ComponentProps } from 'vue-component-type-helpers'
|
||||
|
||||
const ITEMS = [
|
||||
createV2vSelectedVm({ id: 'vm-1' as XoV2vVm['id'], name: 'web-01' }),
|
||||
createV2vSelectedVm({ id: 'vm-2' as XoV2vVm['id'], name: 'db-01' }),
|
||||
]
|
||||
|
||||
const READINESS_TAGS: V2vAccentedLabel<TagAccent>[] = [
|
||||
{ label: '1 ready', accent: 'success' },
|
||||
{ label: '1 blocking', accent: 'danger' },
|
||||
]
|
||||
|
||||
const LIST_PAIR = createV2vListPair()
|
||||
|
||||
const EMPTY = {
|
||||
items: [],
|
||||
count: 0,
|
||||
isEmpty: true,
|
||||
readinessTags: [],
|
||||
listPair: LIST_PAIR,
|
||||
}
|
||||
|
||||
function mountPanel(props: Partial<ComponentProps<typeof V2vSelectedVmsPanel>> = {}) {
|
||||
return mount(V2vSelectedVmsPanel, {
|
||||
props: {
|
||||
items: ITEMS,
|
||||
count: ITEMS.length,
|
||||
isEmpty: false,
|
||||
isBusy: false,
|
||||
readinessTags: READINESS_TAGS,
|
||||
listPair: LIST_PAIR,
|
||||
...props,
|
||||
},
|
||||
global: createGlobalTestConfig(),
|
||||
})
|
||||
}
|
||||
|
||||
// The actions of a side panel sit in its header, above its cards
|
||||
function getHeaderClearButton(wrapper: VueWrapper) {
|
||||
return getButton(wrapper.getComponent(VtsSidePanel).get('.ui-panel > .header'), t('action:clear'))
|
||||
}
|
||||
|
||||
function getSummaryCard(wrapper: VueWrapper) {
|
||||
return wrapper.getComponent(UiPanelCard)
|
||||
}
|
||||
|
||||
function findStateHeroTypes(wrapper: VueWrapper) {
|
||||
return wrapper.findAllComponents(VtsStateHero).map(stateHero => stateHero.props('type'))
|
||||
}
|
||||
|
||||
describe('with VMs', () => {
|
||||
// Its VMs open it like a selection, but are none to clear: it only closes where closing collapses it
|
||||
it('is the persistent side panel of the page, holding its VMs as a selection', () => {
|
||||
expect(mountPanel().getComponent(VtsSidePanel).props()).toMatchObject({ hasSelection: true, persistent: true })
|
||||
})
|
||||
|
||||
it('titles its summary card with the selected VMs and their count', () => {
|
||||
const title = getSummaryCard(mountPanel()).getComponent(UiPanelCardTitle)
|
||||
|
||||
expect(title.props('label')).toBe(t('selected-vms'))
|
||||
expect(title.getComponent(UiCounter).text()).toBe('2')
|
||||
})
|
||||
|
||||
it('tells where the list is kept', () => {
|
||||
expect(findParts(getSummaryCard(mountPanel()), '.storage-info')).toEqual([
|
||||
t('v2v:saved-in-this-browser'),
|
||||
t('v2v:sent-to-xo-at-launch'),
|
||||
])
|
||||
})
|
||||
|
||||
it('summarizes the source and pool of the list', () => {
|
||||
expect(getSummaryCard(mountPanel()).getComponent(V2vListPairSummary).props()).toEqual(LIST_PAIR)
|
||||
})
|
||||
|
||||
it('tags the readiness of the VMs', () => {
|
||||
expect(findAccentedTags(getSummaryCard(mountPanel()))).toEqual(READINESS_TAGS)
|
||||
})
|
||||
|
||||
it('shows one card per VM after the summary, in order', () => {
|
||||
const wrapper = mountPanel()
|
||||
const items = wrapper.findAllComponents(V2vSelectedVmItem)
|
||||
|
||||
expect(items.map(item => item.props('item'))).toEqual(ITEMS)
|
||||
expect(wrapper.findAllComponents(UiPanelCard)).toHaveLength(1 + ITEMS.length)
|
||||
})
|
||||
|
||||
it('removes the VM of an item', () => {
|
||||
const wrapper = mountPanel()
|
||||
|
||||
wrapper.findAllComponents(V2vSelectedVmItem)[1].vm.$emit('remove', ITEMS[1].id)
|
||||
|
||||
expect(wrapper.emitted('remove')).toEqual([[ITEMS[1].id]])
|
||||
})
|
||||
|
||||
it('clears the list from its actions', async () => {
|
||||
const wrapper = mountPanel()
|
||||
|
||||
await getHeaderClearButton(wrapper).trigger('click')
|
||||
|
||||
expect(wrapper.emitted('clear')).toEqual([[]])
|
||||
})
|
||||
|
||||
it('has no state hero', () => {
|
||||
expect(findStateHeroTypes(mountPanel())).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('busy', () => {
|
||||
it('shows the loading state instead of the VMs, their readiness and the clear', () => {
|
||||
const wrapper = mountPanel({ isBusy: true })
|
||||
|
||||
expect(findStateHeroTypes(wrapper)).toEqual(['busy'])
|
||||
expect(wrapper.findAllComponents(V2vSelectedVmItem)).toEqual([])
|
||||
expect(findAccentedTags(getSummaryCard(wrapper))).toEqual([])
|
||||
expect(findButton(wrapper, t('action:clear'))).toBeUndefined()
|
||||
})
|
||||
|
||||
it('still titles the selected VMs with their count and summarizes the source and pool of the list', () => {
|
||||
const wrapper = mountPanel({ isBusy: true })
|
||||
|
||||
expect(getSummaryCard(wrapper).getComponent(UiCounter).text()).toBe('2')
|
||||
expect(getSummaryCard(wrapper).getComponent(V2vListPairSummary).props()).toEqual(LIST_PAIR)
|
||||
})
|
||||
})
|
||||
|
||||
describe('empty', () => {
|
||||
it('holds no selection', () => {
|
||||
expect(mountPanel(EMPTY).getComponent(VtsSidePanel).props('hasSelection')).toBe(false)
|
||||
})
|
||||
|
||||
it('offers no clear', () => {
|
||||
expect(findButton(mountPanel(EMPTY), t('action:clear'))).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<VtsSidePanel class="v2v-selected-vms-panel" :has-selection="!isEmpty" persistent>
|
||||
<template v-if="areItemsShown" #actions>
|
||||
<UiButton
|
||||
accent="brand"
|
||||
variant="tertiary"
|
||||
size="medium"
|
||||
left-icon="action:close-cancel-clear"
|
||||
@click="emit('clear')"
|
||||
>
|
||||
{{ t('action:clear') }}
|
||||
</UiButton>
|
||||
</template>
|
||||
<UiPanelCard>
|
||||
<UiPanelCardTitle
|
||||
size="medium"
|
||||
icon="fa:list"
|
||||
:label="t('selected-vms')"
|
||||
:counter="{ value: count, accent: 'brand' }"
|
||||
/>
|
||||
<InlineParts class="storage-info typo-body-regular-small">
|
||||
<span>{{ t('v2v:saved-in-this-browser') }}</span>
|
||||
<span>{{ t('v2v:sent-to-xo-at-launch') }}</span>
|
||||
</InlineParts>
|
||||
<V2vListPairSummary v-bind="listPair" />
|
||||
<UiTagsList v-if="areItemsShown">
|
||||
<UiTag v-for="tag of readinessTags" :key="tag.label" :accent="tag.accent" variant="secondary">
|
||||
{{ tag.label }}
|
||||
</UiTag>
|
||||
</UiTagsList>
|
||||
</UiPanelCard>
|
||||
<VtsStateHero v-if="isBusy" format="panel" type="busy" size="small" />
|
||||
<template v-else>
|
||||
<V2vSelectedVmItem v-for="item of items" :key="item.id" :item @remove="id => emit('remove', id)" />
|
||||
</template>
|
||||
</VtsSidePanel>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import V2vListPairSummary from '@/modules/v2v/components/selected-vms/V2vListPairSummary.vue'
|
||||
import V2vSelectedVmItem from '@/modules/v2v/components/selected-vms/V2vSelectedVmItem.vue'
|
||||
import type { V2vSelectedVmsPanelProps } from '@/modules/v2v/composables/use-v2v-selected-vms.composable.ts'
|
||||
import InlineParts from '@/shared/components/InlineParts.vue'
|
||||
import VtsSidePanel from '@core/components/panel/VtsSidePanel.vue'
|
||||
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
|
||||
import UiButton from '@core/components/ui/button/UiButton.vue'
|
||||
import UiPanelCard from '@core/components/ui/panel-card/UiPanelCard.vue'
|
||||
import UiPanelCardTitle from '@core/components/ui/panel-card-title/UiPanelCardTitle.vue'
|
||||
import UiTag from '@core/components/ui/tag/UiTag.vue'
|
||||
import UiTagsList from '@core/components/ui/tag/UiTagsList.vue'
|
||||
import type { XoV2vVm } from '@vates/types'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { isEmpty, isBusy } = defineProps<V2vSelectedVmsPanelProps>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
remove: [id: XoV2vVm['id']]
|
||||
clear: []
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const areItemsShown = computed(() => !isEmpty && !isBusy)
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.v2v-selected-vms-panel {
|
||||
.storage-info {
|
||||
color: var(--color-neutral-txt-secondary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user